Plateforme de decisions collectives pour Duniter/G1. Backend FastAPI async + PostgreSQL (14 tables, 8 routers, 6 services, moteur de vote avec formule d'inertie WoT/Smith/TechComm). Frontend Nuxt 4 + Nuxt UI v3 + Pinia (9 pages, 5 stores). Infrastructure Docker + Woodpecker CI + Traefik. Documentation technique et utilisateur (15 fichiers). Seed : Licence G1, Engagement Forgeron v2.0.0, 4 protocoles de vote. 30 tests unitaires (formules, mode params, vote nuance) -- tous verts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
178 lines
5.4 KiB
Vue
178 lines
5.4 KiB
Vue
<script setup lang="ts">
|
|
const auth = useAuthStore()
|
|
const router = useRouter()
|
|
|
|
const address = ref('')
|
|
const step = ref<'input' | 'challenge' | 'signing' | 'success'>('input')
|
|
const errorMessage = ref('')
|
|
|
|
async function handleLogin() {
|
|
if (!address.value.trim()) {
|
|
errorMessage.value = 'Veuillez entrer votre adresse Duniter'
|
|
return
|
|
}
|
|
|
|
errorMessage.value = ''
|
|
step.value = 'challenge'
|
|
|
|
try {
|
|
step.value = 'signing'
|
|
await auth.login(address.value.trim())
|
|
step.value = 'success'
|
|
|
|
// Redirect to home after a brief moment
|
|
setTimeout(() => {
|
|
router.push('/')
|
|
}, 1000)
|
|
} catch (err: any) {
|
|
errorMessage.value = err?.data?.detail || err?.message || 'Erreur lors de la connexion'
|
|
step.value = 'input'
|
|
}
|
|
}
|
|
|
|
const steps = computed(() => [
|
|
{
|
|
title: 'Adresse Duniter',
|
|
description: 'Entrez votre adresse SS58 Duniter V2',
|
|
icon: 'i-lucide-user',
|
|
active: step.value === 'input',
|
|
complete: step.value !== 'input',
|
|
},
|
|
{
|
|
title: 'Challenge cryptographique',
|
|
description: 'Un challenge aleatoire est genere par le serveur',
|
|
icon: 'i-lucide-shield',
|
|
active: step.value === 'challenge',
|
|
complete: step.value === 'signing' || step.value === 'success',
|
|
},
|
|
{
|
|
title: 'Signature Ed25519',
|
|
description: 'Signez le challenge avec votre cle privee',
|
|
icon: 'i-lucide-key',
|
|
active: step.value === 'signing',
|
|
complete: step.value === 'success',
|
|
},
|
|
{
|
|
title: 'Connexion',
|
|
description: 'Votre identite est verifiee et la session creee',
|
|
icon: 'i-lucide-check-circle',
|
|
active: step.value === 'success',
|
|
complete: false,
|
|
},
|
|
])
|
|
|
|
// Redirect if already authenticated
|
|
onMounted(() => {
|
|
if (auth.isAuthenticated) {
|
|
router.push('/')
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="max-w-lg mx-auto space-y-8 py-8">
|
|
<div class="text-center">
|
|
<UIcon name="i-lucide-vote" class="text-5xl text-primary mb-4" />
|
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
|
Connexion a Glibredecision
|
|
</h1>
|
|
<p class="mt-2 text-gray-600 dark:text-gray-400">
|
|
Authentification via votre identite Duniter V2
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Login form -->
|
|
<UCard>
|
|
<div class="space-y-6">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Adresse Duniter (SS58)
|
|
</label>
|
|
<UInput
|
|
v-model="address"
|
|
placeholder="5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
|
|
size="lg"
|
|
icon="i-lucide-wallet"
|
|
:disabled="auth.loading || step !== 'input'"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Error message -->
|
|
<div v-if="errorMessage || auth.error" class="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
|
<div class="flex items-center gap-2">
|
|
<UIcon name="i-lucide-alert-circle" class="text-red-500" />
|
|
<p class="text-sm text-red-700 dark:text-red-400">
|
|
{{ errorMessage || auth.error }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Success message -->
|
|
<div v-if="step === 'success'" class="p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
|
<div class="flex items-center gap-2">
|
|
<UIcon name="i-lucide-check-circle" class="text-green-500" />
|
|
<p class="text-sm text-green-700 dark:text-green-400">
|
|
Connexion reussie ! Redirection en cours...
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<UButton
|
|
:label="auth.loading ? 'Connexion en cours...' : 'Se connecter avec Duniter'"
|
|
icon="i-lucide-log-in"
|
|
size="lg"
|
|
block
|
|
:loading="auth.loading"
|
|
:disabled="!address.trim() || step === 'success'"
|
|
@click="handleLogin"
|
|
/>
|
|
</div>
|
|
</UCard>
|
|
|
|
<!-- Challenge flow steps -->
|
|
<UCard>
|
|
<div class="space-y-4">
|
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">
|
|
Processus d'authentification
|
|
</h3>
|
|
<div class="space-y-3">
|
|
<div
|
|
v-for="(s, index) in steps"
|
|
:key="index"
|
|
class="flex items-start gap-3"
|
|
:class="{ 'opacity-40': !s.active && !s.complete }"
|
|
>
|
|
<div
|
|
class="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0"
|
|
:class="{
|
|
'bg-primary text-white': s.active,
|
|
'bg-green-500 text-white': s.complete,
|
|
'bg-gray-200 dark:bg-gray-700 text-gray-500': !s.active && !s.complete,
|
|
}"
|
|
>
|
|
<UIcon v-if="s.complete" name="i-lucide-check" class="text-sm" />
|
|
<span v-else class="text-xs font-bold">{{ index + 1 }}</span>
|
|
</div>
|
|
<div>
|
|
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
|
{{ s.title }}
|
|
</p>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
{{ s.description }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
|
|
<!-- Info note -->
|
|
<div class="text-center">
|
|
<p class="text-xs text-gray-400">
|
|
L'authentification utilise la cryptographie Ed25519 de Duniter V2.
|
|
Aucun mot de passe n'est transmis au serveur.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</template>
|