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

@ -156,7 +156,13 @@ func (h *oauthHandler) Callback(w http.ResponseWriter, r *http.Request) {
redirectWithError(w, r, redirectBase, "db_error")
return
}
token, err := auth.SignJWT(h.jwtSecret, user.ID, team.ID, user.Email)
membership, err := h.db.GetTeamMembership(ctx, db.GetTeamMembershipParams{UserID: user.ID, TeamID: team.ID})
if err != nil {
slog.Error("oauth login: failed to get membership", "error", err)
redirectWithError(w, r, redirectBase, "db_error")
return
}
token, err := auth.SignJWT(h.jwtSecret, user.ID, team.ID, user.Email, membership.Role)
if err != nil {
slog.Error("oauth login: failed to sign jwt", "error", err)
redirectWithError(w, r, redirectBase, "internal_error")
@ -219,6 +225,7 @@ func (h *oauthHandler) Callback(w http.ResponseWriter, r *http.Request) {
if _, err := qtx.InsertTeam(ctx, db.InsertTeamParams{
ID: teamID,
Name: teamName,
Slug: id.NewTeamSlug(),
}); err != nil {
slog.Error("oauth: failed to create team", "error", err)
redirectWithError(w, r, redirectBase, "db_error")
@ -253,7 +260,7 @@ func (h *oauthHandler) Callback(w http.ResponseWriter, r *http.Request) {
return
}
token, err := auth.SignJWT(h.jwtSecret, userID, teamID, email)
token, err := auth.SignJWT(h.jwtSecret, userID, teamID, email, "owner")
if err != nil {
slog.Error("oauth: failed to sign jwt", "error", err)
redirectWithError(w, r, redirectBase, "internal_error")
@ -288,7 +295,13 @@ func (h *oauthHandler) retryAsLogin(w http.ResponseWriter, r *http.Request, prov
redirectWithError(w, r, redirectBase, "db_error")
return
}
token, err := auth.SignJWT(h.jwtSecret, user.ID, team.ID, user.Email)
membership, err := h.db.GetTeamMembership(ctx, db.GetTeamMembershipParams{UserID: user.ID, TeamID: team.ID})
if err != nil {
slog.Error("oauth: retry login: failed to get membership", "error", err)
redirectWithError(w, r, redirectBase, "db_error")
return
}
token, err := auth.SignJWT(h.jwtSecret, user.ID, team.ID, user.Email, membership.Role)
if err != nil {
slog.Error("oauth: retry login: failed to sign jwt", "error", err)
redirectWithError(w, r, redirectBase, "internal_error")