SPB Git

spb/uqo-chat

Public
4.7 KB · 129 lines python
1"""User accounts, magic links, consent, data deletion (Loi 25)."""23from __future__ import annotations45from datetime import timedelta67from sqlalchemy import delete, select89from app.core.config import Settings10from app.core.security import new_magic_token, role_for_email11from app.db import SessionLocal12from app.models import (13    Conversation,14    MagicLink,15    Message,16    Quiz,17    QuizAttempt,18    ToolCall,19    User,20    utcnow,21)22from app.services import files as file_service232425async def get_or_create_user(email: str, settings: Settings) -> User:26    email = email.lower().strip()27    async with SessionLocal() as session:28        user = await session.scalar(select(User).where(User.email == email))29        role = role_for_email(email, settings)30        if user is None:31            user = User(email=email, role=role, display_name=email.split("@")[0],32                        preferences={"tutoiement": True, "course": settings.courses[0],33                                     "deep": False, "locale": "fr-CA"})34            session.add(user)35        elif role != "student" and user.role == "student":36            user.role = role  # promoted via env37        user.last_seen_at = utcnow()38        await session.commit()39        await session.refresh(user)40        return user414243async def get_user(user_id: str) -> User | None:44    async with SessionLocal() as session:45        return await session.get(User, user_id)464748async def touch(user_id: str) -> None:49    async with SessionLocal() as session:50        user = await session.get(User, user_id)51        if user:52            user.last_seen_at = utcnow()53            await session.commit()545556async def update_preferences(user_id: str, prefs: dict, display_name: str | None = None) -> User:57    async with SessionLocal() as session:58        user = await session.get(User, user_id)59        if user is None:60            raise ValueError("user not found")61        merged = dict(user.preferences or {})62        merged.update({k: v for k, v in prefs.items() if k in63                       {"tutoiement", "course", "deep", "locale", "font_scale"}})64        user.preferences = merged65        if display_name is not None:66            user.display_name = display_name[:120]67        await session.commit()68        await session.refresh(user)69        return user707172async def set_consent(user_id: str) -> None:73    async with SessionLocal() as session:74        user = await session.get(User, user_id)75        if user and not user.consent_at:76            user.consent_at = utcnow()77            await session.commit()787980async def create_magic_link(email: str, ttl_minutes: int = 20) -> str:81    token = new_magic_token()82    async with SessionLocal() as session:83        session.add(MagicLink(token=token, email=email.lower().strip(),84                              expires_at=utcnow() + timedelta(minutes=ttl_minutes)))85        await session.commit()86    return token878889async def consume_magic_link(token: str) -> str | None:90    async with SessionLocal() as session:91        link = await session.get(MagicLink, token)92        if not link or link.used_at or link.expires_at < utcnow():93            return None94        link.used_at = utcnow()95        await session.commit()96        return link.email979899async def set_role(email: str, role: str) -> bool:100    async with SessionLocal() as session:101        user = await session.scalar(select(User).where(User.email == email.lower().strip()))102        if not user:103            return False104        user.role = role105        await session.commit()106        return True107108109async def delete_user_data(user_id: str) -> None:110    """Purge everything about a user (conversations, files, quizzes) then the account."""111    await file_service.purge_user(user_id)112    async with SessionLocal() as session:113        conv_ids = [c for c in (await session.execute(114            select(Conversation.id).where(Conversation.user_id == user_id))).scalars()]115        if conv_ids:116            msg_ids = [m for m in (await session.execute(117                select(Message.id).where(Message.conversation_id.in_(conv_ids)))).scalars()]118            if msg_ids:119                await session.execute(delete(ToolCall).where(ToolCall.message_id.in_(msg_ids)))120                await session.execute(delete(Message).where(Message.id.in_(msg_ids)))121            await session.execute(delete(Conversation).where(Conversation.id.in_(conv_ids)))122        quiz_ids = [q for q in (await session.execute(123            select(Quiz.id).where(Quiz.user_id == user_id))).scalars()]124        if quiz_ids:125            await session.execute(delete(QuizAttempt).where(QuizAttempt.quiz_id.in_(quiz_ids)))126            await session.execute(delete(Quiz).where(Quiz.id.in_(quiz_ids)))127        await session.execute(delete(User).where(User.id == user_id))128        await session.commit()129
Files · afb5ca448565f356efc1532400a8ac8b8eba5d6a
Menu

Theme