# retoor import re from fastapi import APIRouter, Request from molodetz.constants import ROLE_MEMBER from molodetz.database import ( claim_invite, create_session, create_user, get_int_setting, get_invite_by_token, get_user_by_email, get_user_by_username, invite_is_live, update_join_request, ) from molodetz.dependencies import json_or_form from molodetz.models import InviteClaimForm from molodetz.responses import action_result, json_error, respond, wants_json from molodetz.schemas.joins import InvitePageOut from molodetz.seo import base_seo_context from molodetz.templating import templates from molodetz.utils.audit import record from molodetz.utils.auth import hash_password_async from molodetz.utils.request_meta import is_secure_request router = APIRouter() EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$") INVALID_MESSAGE = "This invite link is invalid or has expired." def _seo(request): return base_seo_context(request, "Accept invite", robots="noindex,nofollow") def _invalid(request): record(request, "invite.claim.invalid", result="denied") if wants_json(request): return json_error(404, INVALID_MESSAGE) return templates.TemplateResponse( request, "invite.html", {"valid": False, "errors": [INVALID_MESSAGE], "form": {}, "email_locked": False, "seo": _seo(request)}, status_code=404, ) def _form_errors(data, invite): errors = [] email = (data.email or "").lower() if not EMAIL_PATTERN.match(email): errors.append("Enter a valid email address.") elif invite.get("email") and email != invite["email"]: errors.append("This invite was issued for a different email address.") if get_user_by_username(data.username): errors.append("That name is already taken.") if get_user_by_email(email): errors.append("That email address is already in use.") if data.password != data.password_confirm: errors.append("The passwords do not match.") if not data.terms: errors.append("Accept the house rules to continue.") return errors @router.get("/{token}") async def invite_page(request: Request, token: str): invite = get_invite_by_token(token) if not invite_is_live(invite): return _invalid(request) ctx = { "valid": True, "username": invite.get("username") or "", "email": invite.get("email") or "", "errors": [], "form": {"username": invite.get("username") or "", "email": invite.get("email") or ""}, "email_locked": bool(invite.get("email")), "seo": _seo(request), } return respond(request, "invite.html", ctx, model=InvitePageOut) @router.post("/{token}") async def invite_claim(request: Request, token: str): invite = get_invite_by_token(token) if not invite_is_live(invite): return _invalid(request) data = await json_or_form(request, InviteClaimForm) errors = _form_errors(data, invite) if errors: record(request, "invite.claim.rejected", result="denied", payload={"errors": len(errors)}) ctx = { "valid": True, "errors": errors, "form": {"username": data.username, "email": data.email}, "email_locked": bool(invite.get("email")), "seo": _seo(request), } if wants_json(request): return json_error(400, errors[0], errors=errors) return templates.TemplateResponse(request, "invite.html", ctx, status_code=400) email = data.email.lower() password_hash = await hash_password_async(data.password) user = create_user(data.username, email, password_hash, role=ROLE_MEMBER) claim_invite(invite["uid"], user["uid"]) update_join_request(invite["join_request_uid"], status="accepted") record( request, "invite.claimed", targets=[("user", user["uid"]), ("join_request", invite["join_request_uid"])], user=user, ) days = get_int_setting("session_max_age_days", 7) max_age = max(1, days) * 86400 session_token = create_session(user["uid"], max_age) response = action_result(request, "/", data={"username": user["username"]}) response.set_cookie( key="session", value=session_token, max_age=max_age, httponly=True, samesite="lax", secure=is_secure_request(request), ) return response