* fix(jellyfin): vérifier que le jeton Jellyfin appartient à l'utilisateur annoncé
L'échange du plugin (POST /api/v1/auth/jellyfin) validait le jeton avec
GET /Users/{jellyfinUserId}, que Jellyfin accepte avec le jeton de
n'importe quel utilisateur. Un utilisateur Jellyfin pouvait ainsi obtenir
le jeton d'API FanKarr d'un autre compte, administrateur compris.
Le jeton est maintenant vérifié avec GET /Users/Me, et son propriétaire
doit correspondre à jellyfinUserId.
* feat(auth): connexion avec un compte Jellyfin
Les utilisateurs Jellyfin se connectent à l'interface web avec leurs
identifiants Jellyfin, vérifiés par Jellyfin (POST /Users/AuthenticateByName).
- Import des utilisateurs Jellyfin au choix depuis Paramètres › Jellyfin et
API ; l'import automatique horaire devient un réglage (activé par défaut).
Un compte FanKarr du même nom est lié au lieu d'être recréé.
- Seuls les comptes importés peuvent se connecter avec Jellyfin, sauf si
« Autoriser les comptes pas encore importés » est activé (désactivé par
défaut) : le compte invité est alors créé à la première connexion, depuis
l'interface web ou le plugin.
- Interrupteur « Connexion à FanKarr avec un compte Jellyfin » (activé par
défaut).
- L'identifiant du serveur Jellyfin est enregistré et vérifié à chaque
connexion ; la session Jellyfin ouverte pour la vérification est fermée
aussitôt.
- Connexion limitée à 5 échecs en 15 minutes par nom d'utilisateur et adresse.
- Les comptes importés n'ont pas de mot de passe FanKarr : la page Profil
l'indique, l'administrateur peut toujours en définir un.
80 lines
3.4 KiB
Vue
80 lines
3.4 KiB
Vue
<template>
|
|
<div class="relative min-h-screen bg-main flex items-center justify-center px-4 py-10 overflow-hidden">
|
|
<div class="dot-grid [--dot-grid-angle:160deg] [--dot-grid-fade:45%]" />
|
|
<div class="relative w-full max-w-sm">
|
|
|
|
<div class="flex items-center justify-center gap-3 mb-8">
|
|
<FankarrLogo class="w-9 h-9" />
|
|
<span class="font-display text-[30px] font-bold text-primary tracking-[0.01em]">FanKarr</span>
|
|
</div>
|
|
|
|
<div class="card px-7 py-7 flex flex-col gap-5 shadow-[0_24px_60px_rgb(0_0_0/0.4)]">
|
|
<div class="flex flex-col gap-1">
|
|
<p v-if="isSetup" class="tag-label text-accent">Premier lancement</p>
|
|
<h1 class="font-display text-[26px] font-bold text-primary leading-tight">
|
|
{{ isSetup ? 'Créer le compte administrateur' : 'Connexion' }}
|
|
</h1>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-4">
|
|
<div class="flex flex-col gap-[7px]">
|
|
<label for="auth-username" class="field-label">Nom d'utilisateur</label>
|
|
<input id="auth-username" v-model="username" type="text" placeholder="admin" class="field" @keyup.enter="submit" />
|
|
</div>
|
|
<div class="flex flex-col gap-[7px]">
|
|
<label for="auth-password" class="field-label">Mot de passe</label>
|
|
<input id="auth-password" v-model="password" type="password" placeholder="••••••••" class="field" @keyup.enter="submit" />
|
|
</div>
|
|
<div v-if="isSetup">
|
|
<label class="field-label mb-1.5">Confirmer le mot de passe</label>
|
|
<input v-model="confirm" type="password" placeholder="••••••••" class="field" @keyup.enter="submit" />
|
|
</div>
|
|
|
|
<p v-if="error" class="text-meta text-err" role="alert">{{ error }}</p>
|
|
|
|
<button @click="submit" :disabled="submitting" class="btn-primary w-full h-11 mt-1">
|
|
{{ submitting ? (isSetup ? 'Création…' : 'Connexion…') : isSetup ? 'Créer le compte' : 'Se connecter' }}
|
|
</button>
|
|
<p v-if="!isSetup && auth.jellyfinLogin" class="text-meta text-muted text-center">
|
|
Vous pouvez aussi vous connecter avec votre compte Jellyfin.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import FankarrLogo from '@/components/FankarrLogo.vue'
|
|
|
|
const router = useRouter()
|
|
const auth = useAuthStore()
|
|
|
|
const username = ref('')
|
|
const password = ref('')
|
|
const confirm = ref('')
|
|
const error = ref<string | null>(null)
|
|
const submitting = ref(false)
|
|
|
|
const isSetup = computed(() => !auth.setup)
|
|
|
|
async function submit() {
|
|
error.value = null
|
|
if (!username.value || !password.value) { error.value = 'Tous les champs sont requis.'; return }
|
|
if (isSetup.value && password.value.length < 6) { error.value = 'Le mot de passe doit contenir au moins 6 caractères.'; return }
|
|
if (isSetup.value && password.value !== confirm.value) { error.value = 'Les mots de passe ne correspondent pas.'; return }
|
|
|
|
submitting.value = true
|
|
const err = isSetup.value
|
|
? await auth.setupAccount(username.value, password.value)
|
|
: await auth.login(username.value, password.value)
|
|
submitting.value = false
|
|
|
|
if (err) { error.value = err; return }
|
|
await router.push('/')
|
|
}
|
|
</script> |