feat(tenancy): add team invitation acceptance page
Some checks failed
CI / ruff (push) Successful in 14s
CI / pytest (push) Has been cancelled
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled

New standalone page at /store/{store_code}/invitation/accept?token=xxx
where invited team members can:
- Review their name and email (pre-filled from invitation)
- Set their password
- Accept the invitation

Page handles all routing modes (dev path, platform path, prod subdomain,
custom domain) via store context middleware. After acceptance, redirects
to the platform-aware store login page.

New service method get_invitation_info() validates the token and returns
invitation details without modifying anything.

Error states: expired token, already accepted, invalid token.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 23:05:23 +02:00
parent 01f7add8dd
commit 11dcfdad73
3 changed files with 308 additions and 1 deletions

View File

@@ -228,6 +228,52 @@ class StoreTeamService:
logger.error(f"Error inviting team member: {str(e)}")
raise
def get_invitation_info(self, db: Session, token: str) -> dict[str, Any] | None:
"""
Get invitation details without accepting it.
Used by the acceptance page to pre-fill the form.
Returns None if token is invalid.
"""
store_user = (
db.query(StoreUser)
.execution_options(include_deleted=True)
.filter(StoreUser.invitation_token == token)
.first()
)
if not store_user:
return None
user = store_user.user
store = store_user.store
role_name = store_user.role.name if store_user.role else "member"
# Check if already accepted
if store_user.invitation_accepted_at is not None:
return {
"valid": False,
"error": "already_accepted",
"store_name": store.name,
}
# Check expiration (7 days)
is_expired = False
if store_user.invitation_sent_at:
elapsed = datetime.utcnow() - store_user.invitation_sent_at
is_expired = elapsed > timedelta(days=7)
return {
"valid": not is_expired,
"error": "expired" if is_expired else None,
"email": user.email,
"first_name": user.first_name,
"last_name": user.last_name,
"store_name": store.name,
"store_code": store.store_code,
"role_name": role_name,
}
def accept_invitation(
self,
db: Session,