diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 3549eec..dadd844 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -27,6 +27,38 @@ from app.services.auth_service import ( router = APIRouter() +# ── Dev profiles (only available when ENVIRONMENT == "development") ───────── +DEV_PROFILES = [ + { + "address": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", + "display_name": "Alice (Membre WoT)", + "wot_status": "member", + "is_smith": False, + "is_techcomm": False, + }, + { + "address": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty", + "display_name": "Bob (Forgeron)", + "wot_status": "member", + "is_smith": True, + "is_techcomm": False, + }, + { + "address": "5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmkP7j4bJa3zN7d8tY", + "display_name": "Charlie (Comite Tech)", + "wot_status": "member", + "is_smith": True, + "is_techcomm": True, + }, + { + "address": "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy", + "display_name": "Dave (Observateur)", + "wot_status": "unknown", + "is_smith": False, + "is_techcomm": False, + }, +] + # ── In-memory challenge store (short-lived, no persistence needed) ────────── # Structure: { address: { "challenge": str, "expires_at": datetime } } _pending_challenges: dict[str, dict] = {} @@ -113,8 +145,11 @@ async def verify_challenge( # 5. Consume the challenge del _pending_challenges[payload.address] - # 6. Get or create identity - identity = await get_or_create_identity(db, payload.address) + # 6. Get or create identity (apply dev profile if available) + dev_profile = None + if settings.ENVIRONMENT == "development": + dev_profile = next((p for p in DEV_PROFILES if p["address"] == payload.address), None) + identity = await get_or_create_identity(db, payload.address, dev_profile=dev_profile) # 7. Create session token token = await create_session(db, identity) @@ -125,6 +160,14 @@ async def verify_challenge( ) +@router.get("/dev/profiles") +async def list_dev_profiles(): + """List available dev profiles for quick login. Only available in development.""" + if settings.ENVIRONMENT != "development": + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not available") + return DEV_PROFILES + + @router.get("/me", response_model=IdentityOut) async def get_me( identity: DuniterIdentity = Depends(get_current_identity), diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py index 1bbf36d..ae33162 100644 --- a/backend/app/services/auth_service.py +++ b/backend/app/services/auth_service.py @@ -82,15 +82,38 @@ async def get_current_identity( return identity -async def get_or_create_identity(db: AsyncSession, address: str) -> DuniterIdentity: - """Get an existing identity by address or create a new one.""" +async def get_or_create_identity( + db: AsyncSession, + address: str, + dev_profile: dict | None = None, +) -> DuniterIdentity: + """Get an existing identity by address or create a new one. + + If dev_profile is provided, apply the profile attributes on create or update. + """ result = await db.execute(select(DuniterIdentity).where(DuniterIdentity.address == address)) identity = result.scalar_one_or_none() if identity is None: - identity = DuniterIdentity(address=address) + kwargs: dict = {"address": address} + if dev_profile: + kwargs.update({ + "display_name": dev_profile.get("display_name"), + "wot_status": dev_profile.get("wot_status", "unknown"), + "is_smith": dev_profile.get("is_smith", False), + "is_techcomm": dev_profile.get("is_techcomm", False), + }) + identity = DuniterIdentity(**kwargs) db.add(identity) await db.commit() await db.refresh(identity) + elif dev_profile: + # Update existing identity with dev profile data + identity.display_name = dev_profile.get("display_name", identity.display_name) + identity.wot_status = dev_profile.get("wot_status", identity.wot_status) + identity.is_smith = dev_profile.get("is_smith", identity.is_smith) + identity.is_techcomm = dev_profile.get("is_techcomm", identity.is_techcomm) + await db.commit() + await db.refresh(identity) return identity diff --git a/frontend/app/pages/login.vue b/frontend/app/pages/login.vue index f57807c..47a4db9 100644 --- a/frontend/app/pages/login.vue +++ b/frontend/app/pages/login.vue @@ -1,11 +1,65 @@ @@ -121,6 +176,30 @@ onMounted(() => { {{ auth.loading ? 'Verification...' : 'Se connecter' }} + +
+
+ + Mode Dev — Connexion rapide +
+
+ +
+
+

Aucun mot de passe. Authentification par signature cryptographique. @@ -373,6 +452,93 @@ onMounted(() => { cursor: not-allowed; } +/* Dev panel */ +.dev-panel { + border: 2px dashed var(--mood-warning, #f59e0b); + border-radius: 16px; + padding: 1rem; + background: rgba(245, 158, 11, 0.04); +} + +.dev-panel__header { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.8125rem; + font-weight: 700; + color: var(--mood-warning, #f59e0b); + margin-bottom: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.dev-panel__profiles { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.dev-profile { + display: flex; + align-items: center; + gap: 0.625rem; + width: 100%; + padding: 0.625rem 0.75rem; + background: var(--mood-accent-soft); + border-radius: 12px; + cursor: pointer; + transition: transform 0.1s ease, box-shadow 0.1s ease; + text-align: left; +} + +.dev-profile:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: 0 3px 12px var(--mood-shadow, rgba(0,0,0,0.08)); +} + +.dev-profile:active:not(:disabled) { + transform: translateY(0); +} + +.dev-profile:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.dev-profile__dot { + width: 0.625rem; + height: 0.625rem; + border-radius: 50%; + flex-shrink: 0; +} + +.dev-profile__info { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; +} + +.dev-profile__name { + font-size: 0.8125rem; + font-weight: 700; + color: var(--mood-text); +} + +.dev-profile__status { + font-size: 0.6875rem; + color: var(--mood-text-muted); + font-weight: 600; +} + +.dev-profile__addr { + font-size: 0.6875rem; + font-family: ui-monospace, SFMono-Regular, monospace; + color: var(--mood-text-muted); + opacity: 0.6; + flex-shrink: 0; +} + /* Note */ .login-card__note { text-align: center;