Skip to content

feat(admin-teams): magic link invite overhaul + single-team enforcement #231

@sage-ali

Description

@sage-ali

Context

Derived from the audit in #205. Depends on #228 (schema) and #230 (force-password-change gate).

The current POST /admin/teams/invitations/accept requires password and full_name from the recipient. This is replaced with a magic link flow — no credentials in email, no password form on acceptance. The recipient is logged straight in and forced to set their password on first use.

Also removes the now-redundant create-team and delete-team endpoints — there is one implicit admin team, it is seeded (see #228), and it is never deleted.

What's needed

1. TeamRole enum + DTO validation

Create src/modules/admin/teams/enums/team-role.enum.ts:

export enum TeamRole {
  ADMIN = 'admin',
  SUPER_ADMIN = 'super_admin',
  OWNER = 'owner',
  DEV = 'dev',
  DESIGNER = 'designer',
}

Apply @IsEnum(TeamRole) to InviteMembersDto.role — remove the // TODO comment.

2. Refactor POST /admin/teams/invitations/accept

AcceptInviteDto — remove password and full_name entirely. Only token remains.

acceptInvite service method:

  • New user path: auto-generate a 16-char random password (crypto.randomBytes), hash and store it, set force_password_change = true
  • Existing user path: skip account creation entirely — just assign role + membership
  • Both paths: issue a short-lived (15 min) one-time JWT and return it so the frontend can log the user straight in
  • Return { accessToken, accountCreated: boolean } so the frontend knows whether to show "welcome, set your password" vs "you've been added"

3. Remove redundant endpoints

From AdminTeamsController:

  • Remove GET /admin/teams (list teams)
  • Remove POST /admin/teams (create team)
  • Remove DELETE /admin/teams/:teamId (delete team)

Remove corresponding service methods, docs, and DTOs.

Acceptance criteria

  • POST /admin/teams/invitations/accept accepts only { token } — no password or name fields
  • New user is created with force_password_change = true and a random internal password
  • Existing user is linked without touching their credentials
  • Returned JWT is short-lived (15 min) and cannot be refreshed (one-time use)
  • InviteMembersDto.role rejects any value outside TeamRole enum
  • Create-team, list-teams, delete-team endpoints are removed from Swagger and the controller
  • Existing unit tests updated to reflect new flow

Depends on

#228, #230

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions