This repository has been archived on 2026-02-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
federation/bp/identity/routes.py
giles b2b853c052
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 51s
Fix actors.profile → activitypub.actor_profile endpoint references
The AP blueprint was consolidated but 4 references to the old
'actors.profile' endpoint remained, causing BuildError on social pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 12:46:04 +00:00

109 lines
3.6 KiB
Python

"""Username selection flow.
Users must choose a preferred_username before they can publish.
This creates their ActorProfile with RSA keys.
"""
from __future__ import annotations
import re
from quart import (
Blueprint, request, render_template, redirect, url_for, g, abort,
)
from shared.services.registry import services
# Username rules: 3-32 chars, lowercase alphanumeric + underscores
USERNAME_RE = re.compile(r"^[a-z][a-z0-9_]{2,31}$")
# Reserved usernames
RESERVED = frozenset({
"admin", "administrator", "root", "system", "moderator", "mod",
"support", "help", "info", "postmaster", "webmaster", "abuse",
"federation", "activitypub", "api", "static", "media", "assets",
"well-known", "nodeinfo", "inbox", "outbox", "followers", "following",
})
def register(url_prefix="/identity"):
bp = Blueprint("identity", __name__, url_prefix=url_prefix)
@bp.get("/choose-username")
async def choose_username_form():
if not g.get("user"):
return redirect(url_for("auth.login_form"))
# Already has a username?
actor = await services.federation.get_actor_by_user_id(g.s, g.user.id)
if actor:
return redirect(url_for("activitypub.actor_profile", username=actor.preferred_username))
return await render_template("federation/choose_username.html")
@bp.post("/choose-username")
async def choose_username():
if not g.get("user"):
abort(401)
# Already has a username?
existing = await services.federation.get_actor_by_user_id(g.s, g.user.id)
if existing:
return redirect(url_for("activitypub.actor_profile", username=existing.preferred_username))
form = await request.form
username = (form.get("username") or "").strip().lower()
# Validate format
error = None
if not USERNAME_RE.match(username):
error = (
"Username must be 3-32 characters, start with a letter, "
"and contain only lowercase letters, numbers, and underscores."
)
elif username in RESERVED:
error = "This username is reserved."
elif not await services.federation.username_available(g.s, username):
error = "This username is already taken."
if error:
return await render_template(
"federation/choose_username.html",
error=error,
username=username,
), 400
# Create ActorProfile with RSA keys
display_name = g.user.name or username
actor = await services.federation.create_actor(
g.s, g.user.id, username,
display_name=display_name,
)
# Redirect to where they were going, or their new profile
next_url = request.args.get("next")
if next_url:
return redirect(next_url)
return redirect(url_for("activitypub.actor_profile", username=actor.preferred_username))
@bp.get("/check-username")
async def check_username():
"""HTMX endpoint to check username availability."""
username = (request.args.get("username") or "").strip().lower()
if not username:
return ""
if not USERNAME_RE.match(username):
return '<span class="text-red-600">Invalid format</span>'
if username in RESERVED:
return '<span class="text-red-600">Reserved</span>'
available = await services.federation.username_available(g.s, username)
if available:
return '<span class="text-green-600">Available</span>'
return '<span class="text-red-600">Taken</span>'
return bp