1
0
forked from wrenn/wrenn

Add team management endpoints

- Three-role model (owner/admin/member) with owner protection invariants
- Team CRUD: create, rename (admin+), soft-delete with VM cleanup (owner only)
- Member management: add by email, remove, role updates (admin+), leave
- Switch-team endpoint re-issues JWT after DB membership verification
- User email prefix search for add-member UI autocomplete
- JWT carries role as a hint; all authorization decisions verified from DB
- Team slug: immutable 12-char hex (e.g. a1b2c3-d1e2f3), reserved on soft-delete
- Migration adds slug + deleted_at to teams; backfills existing rows
This commit is contained in:
2026-03-24 13:29:54 +06:00
parent 4e26d7a292
commit 8e5d426638
21 changed files with 1601 additions and 53 deletions

View File

@ -42,6 +42,47 @@ paths:
schema:
$ref: "#/components/schemas/Error"
/v1/auth/switch-team:
post:
summary: Switch active team
operationId: switchTeam
tags: [auth]
security:
- bearerAuth: []
description: |
Re-issues a JWT scoped to a different team. The user must be a member of
the target team (verified from DB). Use the returned token for subsequent
requests to that team's resources.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [team_id]
properties:
team_id:
type: string
responses:
"200":
description: New JWT issued for the target team
content:
application/json:
schema:
$ref: "#/components/schemas/AuthResponse"
"403":
description: Not a member of this team
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
description: Team not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/auth/login:
post:
summary: Log in with email and password
@ -195,6 +236,340 @@ paths:
"204":
description: API key deleted
/v1/users/search:
get:
summary: Search users by email prefix
operationId: searchUsers
tags: [users]
security:
- bearerAuth: []
description: |
Returns up to 10 users whose email starts with the given prefix.
The prefix must contain "@". Intended for the add-member UI autocomplete.
parameters:
- name: email
in: query
required: true
schema:
type: string
description: Email prefix (must contain "@", e.g. "alice@")
responses:
"200":
description: Matching users
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/UserSearchResult"
"400":
description: Prefix does not contain "@"
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/teams:
get:
summary: List teams for the authenticated user
operationId: listTeams
tags: [teams]
security:
- bearerAuth: []
responses:
"200":
description: Teams the user belongs to, each with their role
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/TeamWithRole"
post:
summary: Create a new team
operationId: createTeam
tags: [teams]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [name]
properties:
name:
type: string
description: 1-128 chars; A-Z a-z 0-9 space _
responses:
"201":
description: Team created (caller is owner)
content:
application/json:
schema:
$ref: "#/components/schemas/TeamWithRole"
"400":
description: Invalid team name
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/teams/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
description: Team ID (must match the JWT's team_id)
get:
summary: Get team info and member list
operationId: getTeam
tags: [teams]
security:
- bearerAuth: []
responses:
"200":
description: Team details with members
content:
application/json:
schema:
$ref: "#/components/schemas/TeamDetail"
"403":
description: JWT team does not match requested team
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
description: Team not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
patch:
summary: Rename the team
operationId: renameTeam
tags: [teams]
security:
- bearerAuth: []
description: Admin or owner role required (verified from DB).
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [name]
properties:
name:
type: string
responses:
"204":
description: Renamed
"400":
description: Invalid team name
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
description: Insufficient role
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
delete:
summary: Delete the team
operationId: deleteTeam
tags: [teams]
security:
- bearerAuth: []
description: |
Owner only. Soft-deletes the team and destroys all running/paused/starting
sandboxes. All DB records are preserved. The team slug is permanently reserved.
responses:
"204":
description: Team deleted
"403":
description: Caller is not the owner
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/teams/{id}/members:
parameters:
- name: id
in: path
required: true
schema:
type: string
get:
summary: List team members
operationId: listTeamMembers
tags: [teams]
security:
- bearerAuth: []
responses:
"200":
description: Members with roles
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/TeamMember"
post:
summary: Add a member by email
operationId: addTeamMember
tags: [teams]
security:
- bearerAuth: []
description: Admin or owner role required. User is added instantly as a member.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email]
properties:
email:
type: string
format: email
responses:
"201":
description: Member added
content:
application/json:
schema:
$ref: "#/components/schemas/TeamMember"
"403":
description: Insufficient role
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
description: No account with that email
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"400":
description: User is already a member
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/teams/{id}/members/{uid}:
parameters:
- name: id
in: path
required: true
schema:
type: string
- name: uid
in: path
required: true
schema:
type: string
description: Target user ID
patch:
summary: Update member role
operationId: updateMemberRole
tags: [teams]
security:
- bearerAuth: []
description: |
Admin or owner required. Valid target roles: admin, member.
The owner's role cannot be changed.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [role]
properties:
role:
type: string
enum: [admin, member]
responses:
"204":
description: Role updated
"403":
description: Insufficient role or attempt to modify owner
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
description: User is not a member
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
delete:
summary: Remove a member
operationId: removeTeamMember
tags: [teams]
security:
- bearerAuth: []
description: Admin or owner required. Owner cannot be removed.
responses:
"204":
description: Member removed
"403":
description: Insufficient role or attempt to remove owner
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
description: User is not a member
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/teams/{id}/leave:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
summary: Leave the team
operationId: leaveTeam
tags: [teams]
security:
- bearerAuth: []
description: The owner cannot leave; they must delete the team instead.
responses:
"204":
description: Left the team
"403":
description: Owner cannot leave
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/v1/sandboxes:
post:
summary: Create a sandbox
@ -1338,6 +1713,61 @@ components:
tag:
type: string
UserSearchResult:
type: object
properties:
user_id:
type: string
email:
type: string
Team:
type: object
properties:
id:
type: string
name:
type: string
slug:
type: string
description: Immutable 12-char hex slug (e.g. a1b2c3-d1e2f3)
created_at:
type: string
format: date-time
TeamWithRole:
allOf:
- $ref: "#/components/schemas/Team"
- type: object
properties:
role:
type: string
enum: [owner, admin, member]
TeamMember:
type: object
properties:
user_id:
type: string
email:
type: string
role:
type: string
enum: [owner, admin, member]
joined_at:
type: string
format: date-time
TeamDetail:
type: object
properties:
team:
$ref: "#/components/schemas/Team"
members:
type: array
items:
$ref: "#/components/schemas/TeamMember"
Error:
type: object
properties: