"""Account activation and password recovery by e-mail. - Invitation: the professor adds an address → a "choose your password" link (INVITE_TTL_DAYS). - Forgot / first login: the student asks for a link from the sign-in page (RESET_TTL_MINUTES). Both land on /mot-de-passe?token=… where the password is chosen and the session opened. """ from __future__ import annotations from app.core.config import Settings from app.models import User from app.services import mail, users def password_link(settings: Settings, token: str) -> str: return f"{settings.APP_URL}/mot-de-passe?token={token}" async def invite_users(targets: list[User], settings: Settings) -> dict: """Create one invitation token per user, e-mail them (batch) and stamp invited_at.""" if not targets: return {"sent": [], "failed": [], "links": {}} mails: list[mail.Mail] = [] links: dict[str, str] = {} for u in targets: token = await users.create_magic_link(u.email, ttl_minutes=settings.INVITE_TTL_DAYS * 24 * 60, purpose="invite") link = password_link(settings, token) links[u.id] = link mails.append(mail.invitation_mail(u.email, link, u.display_name, settings.INVITE_TTL_DAYS, settings.TERM_LABEL)) outcome = await mail.send_batch(settings, mails) if settings.mail_enabled else {} sent = [u for u in targets if outcome.get(u.email)] failed = [u for u in targets if not outcome.get(u.email)] await users.mark_invited([u.id for u in sent]) return {"sent": [u.email for u in sent], "failed": [u.email for u in failed], "links": links} async def send_password_link(user: User, settings: Settings) -> tuple[bool, str]: """Forgot-password / first-login link. Returns (sent, link).""" first_time = not user.password_hash token = await users.create_magic_link(user.email, ttl_minutes=settings.RESET_TTL_MINUTES, purpose="reset") link = password_link(settings, token) sent = await mail.send(settings, mail.reset_mail(user.email, link, settings.RESET_TTL_MINUTES, first_time)) return sent, link