Files
FanKarr/src/views/AuthView.vue
T
ElPouki bd8c233e45 feat(auth): connexion avec un compte Jellyfin (#154)
* 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.
2026-09-21 16:27:04 +02:00

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>