Sprint 1 : scaffolding complet de Glibredecision

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>
This commit is contained in:
Yvv
2026-02-28 12:46:11 +01:00
commit 25437f24e3
100 changed files with 10236 additions and 0 deletions

View File

@@ -0,0 +1,177 @@
<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>