forked from wrenn/wrenn
Add user names, team-scoped sandbox guard, and login robustness fixes
- Add name column to users (migration + sqlc regen); propagate through JWT claims, auth context, all auth/OAuth handlers, service layer, and frontend - Sidebar and team page show name instead of email; team page splits Name/Email into separate columns - Block sandbox creation in UI and API when user has no active team context - loginTeam helper falls back to first active team when no default is set, fixing login for invited users with no is_default membership - Exclude soft-deleted teams from GetDefaultTeamForUser, GetBYOCTeams queries - Guard host creation against soft-deleted teams in service/host.go - SwitchTeam re-fetches name from DB instead of trusting stale JWT claim - Reset teams store on login so stale data from a previous session never persists - Update openapi.yaml: add name to SignupRequest and AuthResponse schemas
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
@ -15,6 +16,41 @@ import (
|
||||
"git.omukk.dev/wrenn/sandbox/internal/id"
|
||||
)
|
||||
|
||||
// loginTeam returns the team and role to stamp into a login JWT.
|
||||
// It prefers the user's default team; if none is flagged as default it falls
|
||||
// back to the earliest-joined team. Returns pgx.ErrNoRows when the user has
|
||||
// no team memberships at all.
|
||||
func loginTeam(ctx context.Context, q *db.Queries, userID string) (db.Team, string, error) {
|
||||
team, err := q.GetDefaultTeamForUser(ctx, userID)
|
||||
if err == nil {
|
||||
membership, err := q.GetTeamMembership(ctx, db.GetTeamMembershipParams{UserID: userID, TeamID: team.ID})
|
||||
if err != nil {
|
||||
return db.Team{}, "", err
|
||||
}
|
||||
return team, membership.Role, nil
|
||||
}
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return db.Team{}, "", err
|
||||
}
|
||||
// No default set — fall back to earliest-joined team.
|
||||
rows, err := q.GetTeamsForUser(ctx, userID)
|
||||
if err != nil {
|
||||
return db.Team{}, "", err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return db.Team{}, "", pgx.ErrNoRows
|
||||
}
|
||||
first := rows[0]
|
||||
return db.Team{
|
||||
ID: first.ID,
|
||||
Name: first.Name,
|
||||
Slug: first.Slug,
|
||||
IsByoc: first.IsByoc,
|
||||
CreatedAt: first.CreatedAt,
|
||||
DeletedAt: first.DeletedAt,
|
||||
}, first.Role, nil
|
||||
}
|
||||
|
||||
type switchTeamRequest struct {
|
||||
TeamID string `json:"team_id"`
|
||||
}
|
||||
@ -32,6 +68,7 @@ func newAuthHandler(db *db.Queries, pool *pgxpool.Pool, jwtSecret []byte) *authH
|
||||
type signupRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type loginRequest struct {
|
||||
@ -44,6 +81,7 @@ type authResponse struct {
|
||||
UserID string `json:"user_id"`
|
||||
TeamID string `json:"team_id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Signup handles POST /v1/auth/signup.
|
||||
@ -55,6 +93,7 @@ func (h *authHandler) Signup(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
req.Email = strings.TrimSpace(strings.ToLower(req.Email))
|
||||
req.Name = strings.TrimSpace(req.Name)
|
||||
if !strings.Contains(req.Email, "@") || len(req.Email) < 3 {
|
||||
writeError(w, http.StatusBadRequest, "invalid_request", "invalid email address")
|
||||
return
|
||||
@ -63,6 +102,10 @@ func (h *authHandler) Signup(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusBadRequest, "invalid_request", "password must be at least 8 characters")
|
||||
return
|
||||
}
|
||||
if req.Name == "" || len(req.Name) > 100 {
|
||||
writeError(w, http.StatusBadRequest, "invalid_request", "name must be between 1 and 100 characters")
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
@ -87,6 +130,7 @@ func (h *authHandler) Signup(w http.ResponseWriter, r *http.Request) {
|
||||
ID: userID,
|
||||
Email: req.Email,
|
||||
PasswordHash: pgtype.Text{String: passwordHash, Valid: true},
|
||||
Name: req.Name,
|
||||
})
|
||||
if err != nil {
|
||||
var pgErr *pgconn.PgError
|
||||
@ -102,7 +146,7 @@ func (h *authHandler) Signup(w http.ResponseWriter, r *http.Request) {
|
||||
teamID := id.NewTeamID()
|
||||
if _, err := qtx.InsertTeam(ctx, db.InsertTeamParams{
|
||||
ID: teamID,
|
||||
Name: req.Email + "'s Team",
|
||||
Name: req.Name + "'s Team",
|
||||
Slug: id.NewTeamSlug(),
|
||||
}); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "db_error", "failed to create team")
|
||||
@ -124,7 +168,7 @@ func (h *authHandler) Signup(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
token, err := auth.SignJWT(h.jwtSecret, userID, teamID, req.Email, "owner")
|
||||
token, err := auth.SignJWT(h.jwtSecret, userID, teamID, req.Email, req.Name, "owner")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal_error", "failed to generate token")
|
||||
return
|
||||
@ -135,6 +179,7 @@ func (h *authHandler) Signup(w http.ResponseWriter, r *http.Request) {
|
||||
UserID: userID,
|
||||
TeamID: teamID,
|
||||
Email: req.Email,
|
||||
Name: req.Name,
|
||||
})
|
||||
}
|
||||
|
||||
@ -173,19 +218,17 @@ func (h *authHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
team, err := h.db.GetDefaultTeamForUser(ctx, user.ID)
|
||||
team, role, err := loginTeam(ctx, h.db, user.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
writeError(w, http.StatusForbidden, "no_team", "user is not a member of any team")
|
||||
return
|
||||
}
|
||||
writeError(w, http.StatusInternalServerError, "db_error", "failed to look up team")
|
||||
return
|
||||
}
|
||||
|
||||
membership, err := h.db.GetTeamMembership(ctx, db.GetTeamMembershipParams{UserID: user.ID, TeamID: team.ID})
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "db_error", "failed to look up membership")
|
||||
return
|
||||
}
|
||||
|
||||
token, err := auth.SignJWT(h.jwtSecret, user.ID, team.ID, user.Email, membership.Role)
|
||||
token, err := auth.SignJWT(h.jwtSecret, user.ID, team.ID, user.Email, user.Name, role)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal_error", "failed to generate token")
|
||||
return
|
||||
@ -196,6 +239,7 @@ func (h *authHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
UserID: user.ID,
|
||||
TeamID: team.ID,
|
||||
Email: user.Email,
|
||||
Name: user.Name,
|
||||
})
|
||||
}
|
||||
|
||||
@ -247,7 +291,14 @@ func (h *authHandler) SwitchTeam(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
token, err := auth.SignJWT(h.jwtSecret, ac.UserID, req.TeamID, ac.Email, membership.Role)
|
||||
// Fetch current name from DB — JWT name is not trusted here (may be stale or empty for old tokens).
|
||||
user, err := h.db.GetUserByID(ctx, ac.UserID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "db_error", "failed to look up user")
|
||||
return
|
||||
}
|
||||
|
||||
token, err := auth.SignJWT(h.jwtSecret, ac.UserID, req.TeamID, ac.Email, user.Name, membership.Role)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal_error", "failed to generate token")
|
||||
return
|
||||
@ -258,5 +309,6 @@ func (h *authHandler) SwitchTeam(w http.ResponseWriter, r *http.Request) {
|
||||
UserID: ac.UserID,
|
||||
TeamID: req.TeamID,
|
||||
Email: ac.Email,
|
||||
Name: user.Name,
|
||||
})
|
||||
}
|
||||
|
||||
@ -150,25 +150,19 @@ func (h *oauthHandler) Callback(w http.ResponseWriter, r *http.Request) {
|
||||
redirectWithError(w, r, redirectBase, "db_error")
|
||||
return
|
||||
}
|
||||
team, err := h.db.GetDefaultTeamForUser(ctx, user.ID)
|
||||
team, role, err := loginTeam(ctx, h.db, user.ID)
|
||||
if err != nil {
|
||||
slog.Error("oauth login: failed to get team", "error", err)
|
||||
redirectWithError(w, r, redirectBase, "db_error")
|
||||
return
|
||||
}
|
||||
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)
|
||||
token, err := auth.SignJWT(h.jwtSecret, user.ID, team.ID, user.Email, user.Name, role)
|
||||
if err != nil {
|
||||
slog.Error("oauth login: failed to sign jwt", "error", err)
|
||||
redirectWithError(w, r, redirectBase, "internal_error")
|
||||
return
|
||||
}
|
||||
redirectWithToken(w, r, redirectBase, token, user.ID, team.ID, user.Email)
|
||||
redirectWithToken(w, r, redirectBase, token, user.ID, team.ID, user.Email, user.Name)
|
||||
return
|
||||
}
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
@ -205,6 +199,7 @@ func (h *oauthHandler) Callback(w http.ResponseWriter, r *http.Request) {
|
||||
_, err = qtx.InsertUserOAuth(ctx, db.InsertUserOAuthParams{
|
||||
ID: userID,
|
||||
Email: email,
|
||||
Name: profile.Name,
|
||||
})
|
||||
if err != nil {
|
||||
var pgErr *pgconn.PgError
|
||||
@ -260,14 +255,14 @@ func (h *oauthHandler) Callback(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
token, err := auth.SignJWT(h.jwtSecret, userID, teamID, email, "owner")
|
||||
token, err := auth.SignJWT(h.jwtSecret, userID, teamID, email, profile.Name, "owner")
|
||||
if err != nil {
|
||||
slog.Error("oauth: failed to sign jwt", "error", err)
|
||||
redirectWithError(w, r, redirectBase, "internal_error")
|
||||
return
|
||||
}
|
||||
|
||||
redirectWithToken(w, r, redirectBase, token, userID, teamID, email)
|
||||
redirectWithToken(w, r, redirectBase, token, userID, teamID, email, profile.Name)
|
||||
}
|
||||
|
||||
// retryAsLogin handles the race where a concurrent request already created the user.
|
||||
@ -289,33 +284,28 @@ func (h *oauthHandler) retryAsLogin(w http.ResponseWriter, r *http.Request, prov
|
||||
redirectWithError(w, r, redirectBase, "db_error")
|
||||
return
|
||||
}
|
||||
team, err := h.db.GetDefaultTeamForUser(ctx, user.ID)
|
||||
team, role, err := loginTeam(ctx, h.db, user.ID)
|
||||
if err != nil {
|
||||
slog.Error("oauth: retry login: failed to get team", "error", err)
|
||||
redirectWithError(w, r, redirectBase, "db_error")
|
||||
return
|
||||
}
|
||||
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)
|
||||
token, err := auth.SignJWT(h.jwtSecret, user.ID, team.ID, user.Email, user.Name, role)
|
||||
if err != nil {
|
||||
slog.Error("oauth: retry login: failed to sign jwt", "error", err)
|
||||
redirectWithError(w, r, redirectBase, "internal_error")
|
||||
return
|
||||
}
|
||||
redirectWithToken(w, r, redirectBase, token, user.ID, team.ID, user.Email)
|
||||
redirectWithToken(w, r, redirectBase, token, user.ID, team.ID, user.Email, user.Name)
|
||||
}
|
||||
|
||||
func redirectWithToken(w http.ResponseWriter, r *http.Request, base, token, userID, teamID, email string) {
|
||||
func redirectWithToken(w http.ResponseWriter, r *http.Request, base, token, userID, teamID, email, name string) {
|
||||
u := base + "?" + url.Values{
|
||||
"token": {token},
|
||||
"user_id": {userID},
|
||||
"team_id": {teamID},
|
||||
"email": {email},
|
||||
"name": {name},
|
||||
}.Encode()
|
||||
http.Redirect(w, r, u, http.StatusFound)
|
||||
}
|
||||
|
||||
@ -79,6 +79,10 @@ func (h *sandboxHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
ac := auth.MustFromContext(r.Context())
|
||||
if ac.TeamID == "" {
|
||||
writeError(w, http.StatusForbidden, "no_team", "no active team context; re-authenticate")
|
||||
return
|
||||
}
|
||||
|
||||
sb, err := h.svc.Create(r.Context(), service.SandboxCreateParams{
|
||||
TeamID: ac.TeamID,
|
||||
|
||||
@ -36,6 +36,7 @@ type teamWithRoleResponse struct {
|
||||
|
||||
type memberResponse struct {
|
||||
UserID string `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
JoinedAt string `json:"joined_at,omitempty"`
|
||||
@ -56,6 +57,7 @@ func teamToResponse(t db.Team) teamResponse {
|
||||
func memberInfoToResponse(m service.MemberInfo) memberResponse {
|
||||
return memberResponse{
|
||||
UserID: m.UserID,
|
||||
Name: m.Name,
|
||||
Email: m.Email,
|
||||
Role: m.Role,
|
||||
JoinedAt: m.JoinedAt.Format(time.RFC3339),
|
||||
|
||||
@ -45,6 +45,7 @@ func requireAPIKeyOrJWT(queries *db.Queries, jwtSecret []byte) func(http.Handler
|
||||
TeamID: claims.TeamID,
|
||||
UserID: claims.Subject,
|
||||
Email: claims.Email,
|
||||
Name: claims.Name,
|
||||
Role: claims.Role,
|
||||
})
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
|
||||
@ -29,6 +29,7 @@ func requireJWT(secret []byte) func(http.Handler) http.Handler {
|
||||
TeamID: claims.TeamID,
|
||||
UserID: claims.Subject,
|
||||
Email: claims.Email,
|
||||
Name: claims.Name,
|
||||
Role: claims.Role,
|
||||
})
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
|
||||
@ -1410,7 +1410,7 @@ components:
|
||||
schemas:
|
||||
SignupRequest:
|
||||
type: object
|
||||
required: [email, password]
|
||||
required: [email, password, name]
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
@ -1418,6 +1418,9 @@ components:
|
||||
password:
|
||||
type: string
|
||||
minLength: 8
|
||||
name:
|
||||
type: string
|
||||
maxLength: 100
|
||||
|
||||
LoginRequest:
|
||||
type: object
|
||||
@ -1441,6 +1444,8 @@ components:
|
||||
type: string
|
||||
email:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
|
||||
CreateAPIKeyRequest:
|
||||
type: object
|
||||
|
||||
@ -11,6 +11,7 @@ type AuthContext struct {
|
||||
TeamID string
|
||||
UserID string // empty when authenticated via API key
|
||||
Email string // empty when authenticated via API key
|
||||
Name string // empty when authenticated via API key
|
||||
Role string // owner, admin, or member; empty when authenticated via API key
|
||||
}
|
||||
|
||||
|
||||
@ -16,16 +16,18 @@ type Claims struct {
|
||||
TeamID string `json:"team_id"`
|
||||
Role string `json:"role"` // owner, admin, or member within TeamID
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// SignJWT signs a new 6-hour JWT for the given user.
|
||||
func SignJWT(secret []byte, userID, teamID, email, role string) (string, error) {
|
||||
func SignJWT(secret []byte, userID, teamID, email, name, role string) (string, error) {
|
||||
now := time.Now()
|
||||
claims := Claims{
|
||||
TeamID: teamID,
|
||||
Role: role,
|
||||
Email: email,
|
||||
Name: name,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Subject: userID,
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
|
||||
@ -112,6 +112,7 @@ type User struct {
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type UsersTeam struct {
|
||||
|
||||
@ -26,7 +26,7 @@ func (q *Queries) DeleteTeamMember(ctx context.Context, arg DeleteTeamMemberPara
|
||||
}
|
||||
|
||||
const getBYOCTeams = `-- name: GetBYOCTeams :many
|
||||
SELECT id, name, created_at, is_byoc, slug, deleted_at FROM teams WHERE is_byoc = TRUE ORDER BY created_at
|
||||
SELECT id, name, created_at, is_byoc, slug, deleted_at FROM teams WHERE is_byoc = TRUE AND deleted_at IS NULL ORDER BY created_at
|
||||
`
|
||||
|
||||
func (q *Queries) GetBYOCTeams(ctx context.Context) ([]Team, error) {
|
||||
@ -59,7 +59,7 @@ func (q *Queries) GetBYOCTeams(ctx context.Context) ([]Team, error) {
|
||||
const getDefaultTeamForUser = `-- name: GetDefaultTeamForUser :one
|
||||
SELECT t.id, t.name, t.created_at, t.is_byoc, t.slug, t.deleted_at FROM teams t
|
||||
JOIN users_teams ut ON ut.team_id = t.id
|
||||
WHERE ut.user_id = $1 AND ut.is_default = TRUE
|
||||
WHERE ut.user_id = $1 AND ut.is_default = TRUE AND t.deleted_at IS NULL
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
@ -114,7 +114,7 @@ func (q *Queries) GetTeamBySlug(ctx context.Context, slug string) (Team, error)
|
||||
}
|
||||
|
||||
const getTeamMembers = `-- name: GetTeamMembers :many
|
||||
SELECT u.id, u.email, ut.role, ut.created_at AS joined_at
|
||||
SELECT u.id, u.name, u.email, ut.role, ut.created_at AS joined_at
|
||||
FROM users_teams ut
|
||||
JOIN users u ON u.id = ut.user_id
|
||||
WHERE ut.team_id = $1
|
||||
@ -123,6 +123,7 @@ ORDER BY ut.created_at
|
||||
|
||||
type GetTeamMembersRow struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
JoinedAt pgtype.Timestamptz `json:"joined_at"`
|
||||
@ -139,6 +140,7 @@ func (q *Queries) GetTeamMembers(ctx context.Context, teamID string) ([]GetTeamM
|
||||
var i GetTeamMembersRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Email,
|
||||
&i.Role,
|
||||
&i.JoinedAt,
|
||||
|
||||
@ -55,7 +55,7 @@ func (q *Queries) GetAdminPermissions(ctx context.Context, userID string) ([]Adm
|
||||
}
|
||||
|
||||
const getAdminUsers = `-- name: GetAdminUsers :many
|
||||
SELECT id, email, password_hash, created_at, updated_at, is_admin FROM users WHERE is_admin = TRUE ORDER BY created_at
|
||||
SELECT id, email, password_hash, created_at, updated_at, is_admin, name FROM users WHERE is_admin = TRUE ORDER BY created_at
|
||||
`
|
||||
|
||||
func (q *Queries) GetAdminUsers(ctx context.Context) ([]User, error) {
|
||||
@ -74,6 +74,7 @@ func (q *Queries) GetAdminUsers(ctx context.Context) ([]User, error) {
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.IsAdmin,
|
||||
&i.Name,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -86,7 +87,7 @@ func (q *Queries) GetAdminUsers(ctx context.Context) ([]User, error) {
|
||||
}
|
||||
|
||||
const getUserByEmail = `-- name: GetUserByEmail :one
|
||||
SELECT id, email, password_hash, created_at, updated_at, is_admin FROM users WHERE email = $1
|
||||
SELECT id, email, password_hash, created_at, updated_at, is_admin, name FROM users WHERE email = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) {
|
||||
@ -99,12 +100,13 @@ func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.IsAdmin,
|
||||
&i.Name,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByID = `-- name: GetUserByID :one
|
||||
SELECT id, email, password_hash, created_at, updated_at, is_admin FROM users WHERE id = $1
|
||||
SELECT id, email, password_hash, created_at, updated_at, is_admin, name FROM users WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByID(ctx context.Context, id string) (User, error) {
|
||||
@ -117,6 +119,7 @@ func (q *Queries) GetUserByID(ctx context.Context, id string) (User, error) {
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.IsAdmin,
|
||||
&i.Name,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@ -156,19 +159,25 @@ func (q *Queries) InsertAdminPermission(ctx context.Context, arg InsertAdminPerm
|
||||
}
|
||||
|
||||
const insertUser = `-- name: InsertUser :one
|
||||
INSERT INTO users (id, email, password_hash)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, email, password_hash, created_at, updated_at, is_admin
|
||||
INSERT INTO users (id, email, password_hash, name)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, email, password_hash, created_at, updated_at, is_admin, name
|
||||
`
|
||||
|
||||
type InsertUserParams struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash pgtype.Text `json:"password_hash"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (q *Queries) InsertUser(ctx context.Context, arg InsertUserParams) (User, error) {
|
||||
row := q.db.QueryRow(ctx, insertUser, arg.ID, arg.Email, arg.PasswordHash)
|
||||
row := q.db.QueryRow(ctx, insertUser,
|
||||
arg.ID,
|
||||
arg.Email,
|
||||
arg.PasswordHash,
|
||||
arg.Name,
|
||||
)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
@ -177,23 +186,25 @@ func (q *Queries) InsertUser(ctx context.Context, arg InsertUserParams) (User, e
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.IsAdmin,
|
||||
&i.Name,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const insertUserOAuth = `-- name: InsertUserOAuth :one
|
||||
INSERT INTO users (id, email)
|
||||
VALUES ($1, $2)
|
||||
RETURNING id, email, password_hash, created_at, updated_at, is_admin
|
||||
INSERT INTO users (id, email, name)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, email, password_hash, created_at, updated_at, is_admin, name
|
||||
`
|
||||
|
||||
type InsertUserOAuthParams struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (q *Queries) InsertUserOAuth(ctx context.Context, arg InsertUserOAuthParams) (User, error) {
|
||||
row := q.db.QueryRow(ctx, insertUserOAuth, arg.ID, arg.Email)
|
||||
row := q.db.QueryRow(ctx, insertUserOAuth, arg.ID, arg.Email, arg.Name)
|
||||
var i User
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
@ -202,6 +213,7 @@ func (q *Queries) InsertUserOAuth(ctx context.Context, arg InsertUserOAuthParams
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.IsAdmin,
|
||||
&i.Name,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@ -248,3 +260,17 @@ func (q *Queries) SetUserAdmin(ctx context.Context, arg SetUserAdminParams) erro
|
||||
_, err := q.db.Exec(ctx, setUserAdmin, arg.ID, arg.IsAdmin)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateUserName = `-- name: UpdateUserName :exec
|
||||
UPDATE users SET name = $2, updated_at = NOW() WHERE id = $1
|
||||
`
|
||||
|
||||
type UpdateUserNameParams struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateUserName(ctx context.Context, arg UpdateUserNameParams) error {
|
||||
_, err := q.db.Exec(ctx, updateUserName, arg.ID, arg.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -96,9 +96,10 @@ func (s *HostService) Create(ctx context.Context, p HostCreateParams) (HostCreat
|
||||
}
|
||||
}
|
||||
|
||||
// Validate team exists for BYOC hosts.
|
||||
// Validate team exists and is not deleted for BYOC hosts.
|
||||
if p.TeamID != "" {
|
||||
if _, err := s.DB.GetTeam(ctx, p.TeamID); err != nil {
|
||||
team, err := s.DB.GetTeam(ctx, p.TeamID)
|
||||
if err != nil || team.DeletedAt.Valid {
|
||||
return HostCreateResult{}, fmt.Errorf("invalid request: team not found")
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,9 +33,10 @@ type TeamWithRole struct {
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
// MemberInfo is a team member with resolved email.
|
||||
// MemberInfo is a team member with resolved user details.
|
||||
type MemberInfo struct {
|
||||
UserID string `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
JoinedAt time.Time `json:"joined_at"`
|
||||
@ -215,6 +216,7 @@ func (s *TeamService) GetMembers(ctx context.Context, teamID string) ([]MemberIn
|
||||
}
|
||||
members[i] = MemberInfo{
|
||||
UserID: r.ID,
|
||||
Name: r.Name,
|
||||
Email: r.Email,
|
||||
Role: r.Role,
|
||||
JoinedAt: joinedAt,
|
||||
@ -262,7 +264,7 @@ func (s *TeamService) AddMember(ctx context.Context, teamID, callerUserID, email
|
||||
return MemberInfo{}, fmt.Errorf("insert member: %w", err)
|
||||
}
|
||||
|
||||
return MemberInfo{UserID: target.ID, Email: target.Email, Role: "member"}, nil
|
||||
return MemberInfo{UserID: target.ID, Name: target.Name, Email: target.Email, Role: "member"}, nil
|
||||
}
|
||||
|
||||
// RemoveMember removes a user from the team.
|
||||
|
||||
Reference in New Issue
Block a user