2026-08-09 22:47:43 +02:00
|
|
|
package httpapi
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
|
|
|
|
"net/http"
|
|
|
|
|
|
|
|
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/auth"
|
|
|
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/company"
|
|
|
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/mail"
|
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func (s *Server) handleGetCompany(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
|
|
|
|
var (
|
|
|
|
|
id uuid.UUID
|
|
|
|
|
name, language string
|
|
|
|
|
merge bool
|
|
|
|
|
contentLangs []string
|
2026-08-17 00:39:25 +02:00
|
|
|
ownerUserID *uuid.UUID
|
2026-08-09 22:47:43 +02:00
|
|
|
)
|
|
|
|
|
err := s.Pool.QueryRow(r.Context(), `
|
2026-08-17 00:39:25 +02:00
|
|
|
SELECT id, name, language, merge_products_by_gtin, COALESCE(content_languages, '{}'), owner_user_id
|
2026-08-09 22:47:43 +02:00
|
|
|
FROM companies WHERE id = $1`, cid).
|
2026-08-17 00:39:25 +02:00
|
|
|
Scan(&id, &name, &language, &merge, &contentLangs, &ownerUserID)
|
2026-08-09 22:47:43 +02:00
|
|
|
if err != nil {
|
|
|
|
|
Error(w, http.StatusNotFound, "company not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
parsed, _ := company.ParseContentLanguages(contentLangs, language)
|
2026-08-17 00:39:25 +02:00
|
|
|
out := map[string]any{
|
2026-08-09 22:47:43 +02:00
|
|
|
"id": id, "name": name, "language": language,
|
|
|
|
|
"content_languages": parsed, "merge_products_by_gtin": merge,
|
2026-08-17 00:39:25 +02:00
|
|
|
}
|
|
|
|
|
if ownerUserID != nil {
|
|
|
|
|
out["owner_user_id"] = *ownerUserID
|
|
|
|
|
}
|
|
|
|
|
JSON(w, http.StatusOK, out)
|
2026-08-09 22:47:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) handleUpdateCompany(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
|
|
|
|
role, _ := RoleFromContext(r.Context())
|
|
|
|
|
if role != "admin" {
|
|
|
|
|
Error(w, http.StatusForbidden, "admin required")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var body struct {
|
|
|
|
|
Name *string `json:"name"`
|
|
|
|
|
Language *string `json:"language"`
|
|
|
|
|
ContentLanguages []string `json:"content_languages"`
|
|
|
|
|
MergeProductsByGTIN *bool `json:"merge_products_by_gtin"`
|
|
|
|
|
}
|
|
|
|
|
if err := DecodeJSON(r, &body); err != nil {
|
|
|
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var languageArg any
|
|
|
|
|
primary := company.LoadLanguage(r.Context(), s.Pool, cid)
|
|
|
|
|
if body.Language != nil {
|
|
|
|
|
parsed, err := company.ParseLanguage(*body.Language, false)
|
|
|
|
|
if err != nil {
|
|
|
|
|
Error(w, http.StatusBadRequest, "unsupported language")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
languageArg = parsed
|
|
|
|
|
primary = parsed
|
|
|
|
|
}
|
|
|
|
|
var contentLangsArg any
|
|
|
|
|
if body.ContentLanguages != nil {
|
|
|
|
|
parsed, err := company.ParseContentLanguages(body.ContentLanguages, primary)
|
|
|
|
|
if err != nil {
|
|
|
|
|
Error(w, http.StatusBadRequest, "unsupported language")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
contentLangsArg = parsed
|
|
|
|
|
} else if body.Language != nil {
|
|
|
|
|
// Keep primary first when only language changes.
|
|
|
|
|
existing := company.LoadContentLanguages(r.Context(), s.Pool, cid)
|
|
|
|
|
parsed, err := company.ParseContentLanguages(existing, primary)
|
|
|
|
|
if err != nil {
|
|
|
|
|
parsed = []string{primary}
|
|
|
|
|
}
|
|
|
|
|
contentLangsArg = parsed
|
|
|
|
|
}
|
|
|
|
|
_, err := s.Pool.Exec(r.Context(), `
|
|
|
|
|
UPDATE companies SET
|
|
|
|
|
name = COALESCE($2, name),
|
|
|
|
|
language = COALESCE($3, language),
|
|
|
|
|
content_languages = COALESCE($5, content_languages),
|
|
|
|
|
merge_products_by_gtin = COALESCE($4, merge_products_by_gtin),
|
|
|
|
|
updated_at = now()
|
|
|
|
|
WHERE id = $1`, cid, body.Name, languageArg, body.MergeProductsByGTIN, contentLangsArg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
Error(w, http.StatusInternalServerError, "update failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
s.handleGetCompany(w, r)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) handleGetCompanySettings(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
|
|
|
|
var settings []byte
|
|
|
|
|
err := s.Pool.QueryRow(r.Context(), `
|
|
|
|
|
SELECT settings FROM company_settings WHERE company_id = $1`, cid).Scan(&settings)
|
|
|
|
|
if err != nil {
|
|
|
|
|
JSON(w, http.StatusOK, map[string]any{"settings": map[string]any{}})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
_, _ = w.Write([]byte(`{"settings":`))
|
|
|
|
|
_, _ = w.Write(settings)
|
|
|
|
|
_, _ = w.Write([]byte(`}`))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) handlePutCompanySettings(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
|
|
|
|
role, _ := RoleFromContext(r.Context())
|
|
|
|
|
if role != "admin" {
|
|
|
|
|
Error(w, http.StatusForbidden, "admin required")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var body struct {
|
|
|
|
|
Settings map[string]any `json:"settings"`
|
|
|
|
|
}
|
|
|
|
|
if err := DecodeJSON(r, &body); err != nil {
|
|
|
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := company.ValidateSettingsMap(body.Settings); err != nil {
|
|
|
|
|
Error(w, http.StatusBadRequest, err.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
b, err := json.Marshal(body.Settings)
|
|
|
|
|
if err != nil {
|
|
|
|
|
Error(w, http.StatusBadRequest, "invalid settings")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
_, err = s.Pool.Exec(r.Context(), `
|
|
|
|
|
INSERT INTO company_settings (company_id, settings, updated_at)
|
|
|
|
|
VALUES ($1, $2, now())
|
|
|
|
|
ON CONFLICT (company_id) DO UPDATE SET settings = EXCLUDED.settings, updated_at = now()`,
|
|
|
|
|
cid, b)
|
|
|
|
|
if err != nil {
|
|
|
|
|
Error(w, http.StatusInternalServerError, "save failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
JSON(w, http.StatusOK, map[string]any{"settings": body.Settings})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) handleListTeam(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
|
|
|
|
limit, offset := ParseLimitOffset(r)
|
2026-08-17 00:39:25 +02:00
|
|
|
var ownerUserID *uuid.UUID
|
|
|
|
|
_ = s.Pool.QueryRow(r.Context(), `SELECT owner_user_id FROM companies WHERE id = $1`, cid).Scan(&ownerUserID)
|
2026-08-09 22:47:43 +02:00
|
|
|
var total int64
|
|
|
|
|
if err := s.Pool.QueryRow(r.Context(), `
|
|
|
|
|
SELECT count(*) FROM memberships m WHERE m.company_id = $1 AND m.status = 'active'`, cid).Scan(&total); err != nil {
|
|
|
|
|
Error(w, http.StatusInternalServerError, "list failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
rows, err := s.Pool.Query(r.Context(), `
|
|
|
|
|
SELECT m.id, m.user_id, m.role, m.status, u.email, u.name
|
|
|
|
|
FROM memberships m JOIN users u ON u.id = m.user_id
|
|
|
|
|
WHERE m.company_id = $1 AND m.status = 'active' ORDER BY m.created_at LIMIT $2 OFFSET $3`, cid, limit, offset)
|
|
|
|
|
if err != nil {
|
|
|
|
|
Error(w, http.StatusInternalServerError, "list failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
type member struct {
|
2026-08-17 00:39:25 +02:00
|
|
|
ID uuid.UUID `json:"id"`
|
|
|
|
|
UserID uuid.UUID `json:"user_id"`
|
|
|
|
|
Role string `json:"role"`
|
|
|
|
|
Status string `json:"status"`
|
|
|
|
|
Email string `json:"email"`
|
|
|
|
|
Name *string `json:"name"`
|
|
|
|
|
IsOwner bool `json:"is_owner"`
|
2026-08-09 22:47:43 +02:00
|
|
|
}
|
|
|
|
|
out := make([]member, 0)
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var m member
|
|
|
|
|
if err := rows.Scan(&m.ID, &m.UserID, &m.Role, &m.Status, &m.Email, &m.Name); err != nil {
|
|
|
|
|
Error(w, http.StatusInternalServerError, "scan failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-08-17 00:39:25 +02:00
|
|
|
m.IsOwner = ownerUserID != nil && *ownerUserID == m.UserID
|
2026-08-09 22:47:43 +02:00
|
|
|
out = append(out, m)
|
|
|
|
|
}
|
2026-08-17 00:39:25 +02:00
|
|
|
resp := map[string]any{"members": out, "total": total, "limit": limit, "offset": offset}
|
|
|
|
|
if ownerUserID != nil {
|
|
|
|
|
resp["owner_user_id"] = *ownerUserID
|
|
|
|
|
}
|
|
|
|
|
JSON(w, http.StatusOK, resp)
|
2026-08-09 22:47:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) handleCreateInvite(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if !s.allowCompanyAdminOrPlatform(w, r) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
|
|
|
|
uid, _ := UserIDFromContext(r.Context())
|
|
|
|
|
var body struct {
|
|
|
|
|
Email string `json:"email"`
|
|
|
|
|
Role string `json:"role"`
|
|
|
|
|
}
|
|
|
|
|
if err := DecodeJSON(r, &body); err != nil {
|
|
|
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
inv, token, err := s.Auth.CreateInvite(r.Context(), cid, uid, body.Email, body.Role)
|
|
|
|
|
if err != nil {
|
|
|
|
|
ClientOrLog(w, http.StatusBadRequest, "could not create invite", err, auth.ClientError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
companyName, _ := s.Auth.CompanyName(r.Context(), cid)
|
|
|
|
|
smtpOn := s.Mail != nil && s.Mail.Enabled()
|
|
|
|
|
sendOK := false
|
|
|
|
|
if s.Mail != nil {
|
|
|
|
|
msg := mail.InviteMessage(s.Config.WebOrigin, inv.Email, token, companyName)
|
|
|
|
|
if err := s.Mail.Send(msg); err == nil {
|
|
|
|
|
sendOK = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// noop/disabled mailers return nil from Send; only count real SMTP as delivered.
|
|
|
|
|
mailSent, includeToken := inviteMailResult(smtpOn, sendOK)
|
|
|
|
|
resp := map[string]any{
|
|
|
|
|
"id": inv.ID, "email": inv.Email, "role": inv.Role,
|
|
|
|
|
"expires_at": inv.ExpiresAt, "mail_sent": mailSent, "smtp_enabled": smtpOn,
|
|
|
|
|
}
|
|
|
|
|
// Token returned when email was not delivered so operators can share the accept link.
|
|
|
|
|
if includeToken {
|
|
|
|
|
resp["token"] = token
|
2026-08-17 00:39:25 +02:00
|
|
|
resp["accept_url"] = mail.AcceptInviteURL(s.Config.WebOrigin, token)
|
2026-08-09 22:47:43 +02:00
|
|
|
}
|
|
|
|
|
JSON(w, http.StatusCreated, resp)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// inviteMailResult decides mail_sent and whether the accept token must be returned to the client.
|
|
|
|
|
func inviteMailResult(smtpEnabled, sendOK bool) (mailSent bool, includeToken bool) {
|
|
|
|
|
mailSent = smtpEnabled && sendOK
|
|
|
|
|
includeToken = !mailSent
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) handleRemoveMember(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if !s.allowCompanyAdminOrPlatform(w, r) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
|
|
|
|
userID, err := uuid.Parse(chi.URLParam(r, "userID"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
Error(w, http.StatusBadRequest, "invalid user id")
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-08-17 00:39:25 +02:00
|
|
|
if ownerID, hasOwner, oerr := s.Auth.CompanyOwnerID(r.Context(), cid); oerr != nil {
|
|
|
|
|
Error(w, http.StatusInternalServerError, "lookup failed")
|
|
|
|
|
return
|
|
|
|
|
} else if hasOwner && ownerID == userID {
|
|
|
|
|
Error(w, http.StatusConflict, auth.ErrCannotRemoveOwner.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-08-09 22:47:43 +02:00
|
|
|
var currentRole, status string
|
|
|
|
|
err = s.Pool.QueryRow(r.Context(), `
|
|
|
|
|
SELECT role, status FROM memberships
|
|
|
|
|
WHERE company_id = $1 AND user_id = $2`, cid, userID).Scan(¤tRole, &status)
|
|
|
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
|
|
|
Error(w, http.StatusNotFound, "member not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
Error(w, http.StatusInternalServerError, "lookup failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if status == "active" && auth.NormalizeMembershipRole(currentRole) == "admin" {
|
|
|
|
|
var activeAdmins int64
|
|
|
|
|
if err := s.Pool.QueryRow(r.Context(), `
|
|
|
|
|
SELECT count(*) FROM memberships
|
|
|
|
|
WHERE company_id = $1 AND role = 'admin' AND status = 'active'`, cid).Scan(&activeAdmins); err != nil {
|
|
|
|
|
Error(w, http.StatusInternalServerError, "lookup failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if blocksLastAdminRemove(activeAdmins) {
|
|
|
|
|
Error(w, http.StatusConflict, "cannot remove the last admin")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
tag, err := s.Pool.Exec(r.Context(), `
|
|
|
|
|
UPDATE memberships SET status = 'inactive', updated_at = now()
|
|
|
|
|
WHERE company_id = $1 AND user_id = $2 AND status = 'active'`, cid, userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
Error(w, http.StatusInternalServerError, "remove failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if tag.RowsAffected() == 0 {
|
|
|
|
|
Error(w, http.StatusNotFound, "member not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
JSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// blocksLastAdminDemote is true when demoting an admin would leave zero active admins.
|
|
|
|
|
func blocksLastAdminDemote(currentRole, newRole string, activeAdminCount int64) bool {
|
|
|
|
|
return currentRole == "admin" && newRole == "member" && activeAdminCount <= 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// blocksLastAdminRemove is true when removing an admin would leave zero active admins.
|
|
|
|
|
func blocksLastAdminRemove(activeAdminCount int64) bool {
|
|
|
|
|
return activeAdminCount <= 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) handleUpdateMemberRole(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if !s.allowCompanyAdminOrPlatform(w, r) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
|
|
|
|
userID, err := uuid.Parse(chi.URLParam(r, "userID"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
Error(w, http.StatusBadRequest, "invalid user id")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var body struct {
|
|
|
|
|
Role string `json:"role"`
|
|
|
|
|
}
|
|
|
|
|
if err := DecodeJSON(r, &body); err != nil {
|
|
|
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
newRole, err := auth.ParseMembershipRole(body.Role)
|
|
|
|
|
if err != nil {
|
|
|
|
|
Error(w, http.StatusBadRequest, "invalid role")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var currentRole, status string
|
|
|
|
|
err = s.Pool.QueryRow(r.Context(), `
|
|
|
|
|
SELECT role, status FROM memberships
|
|
|
|
|
WHERE company_id = $1 AND user_id = $2`, cid, userID).Scan(¤tRole, &status)
|
|
|
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
|
|
|
Error(w, http.StatusNotFound, "member not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
Error(w, http.StatusInternalServerError, "lookup failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if status != "active" {
|
|
|
|
|
Error(w, http.StatusBadRequest, "member is not active")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
currentRole = auth.NormalizeMembershipRole(currentRole)
|
|
|
|
|
if currentRole == newRole {
|
|
|
|
|
JSON(w, http.StatusOK, map[string]any{"status": "ok", "role": newRole, "user_id": userID})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if currentRole == "admin" && newRole == "member" {
|
|
|
|
|
var activeAdmins int64
|
|
|
|
|
if err := s.Pool.QueryRow(r.Context(), `
|
|
|
|
|
SELECT count(*) FROM memberships
|
|
|
|
|
WHERE company_id = $1 AND role = 'admin' AND status = 'active'`, cid).Scan(&activeAdmins); err != nil {
|
|
|
|
|
Error(w, http.StatusInternalServerError, "lookup failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if blocksLastAdminDemote(currentRole, newRole, activeAdmins) {
|
|
|
|
|
Error(w, http.StatusConflict, "cannot demote the last admin")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
tag, err := s.Pool.Exec(r.Context(), `
|
|
|
|
|
UPDATE memberships SET role = $3, updated_at = now()
|
|
|
|
|
WHERE company_id = $1 AND user_id = $2 AND status = 'active'`, cid, userID, newRole)
|
|
|
|
|
if err != nil {
|
|
|
|
|
Error(w, http.StatusInternalServerError, "update failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if tag.RowsAffected() == 0 {
|
|
|
|
|
Error(w, http.StatusNotFound, "member not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
JSON(w, http.StatusOK, map[string]any{"status": "ok", "role": newRole, "user_id": userID})
|
|
|
|
|
}
|
2026-08-17 00:39:25 +02:00
|
|
|
|
|
|
|
|
func (s *Server) handleTransferOwnership(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
cid, _ := CompanyIDFromContext(r.Context())
|
|
|
|
|
uid, ok := UserIDFromContext(r.Context())
|
|
|
|
|
if !ok {
|
|
|
|
|
Error(w, http.StatusUnauthorized, "unauthorized")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
platformAdmin, err := s.checkPlatformAdmin(r.Context(), uid)
|
|
|
|
|
if err != nil {
|
|
|
|
|
Error(w, http.StatusInternalServerError, "authorization check failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !platformAdmin {
|
|
|
|
|
isOwner, oerr := s.Auth.IsCompanyOwner(r.Context(), cid, uid)
|
|
|
|
|
if oerr != nil {
|
|
|
|
|
Error(w, http.StatusInternalServerError, "authorization check failed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if !isOwner {
|
|
|
|
|
Error(w, http.StatusForbidden, auth.ErrNotCompanyOwner.Error())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
var body struct {
|
|
|
|
|
UserID uuid.UUID `json:"user_id"`
|
|
|
|
|
}
|
|
|
|
|
if err := DecodeJSON(r, &body); err != nil || body.UserID == uuid.Nil {
|
|
|
|
|
Error(w, http.StatusBadRequest, "invalid json")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := s.Auth.TransferOwnership(r.Context(), cid, body.UserID); err != nil {
|
|
|
|
|
switch {
|
|
|
|
|
case errors.Is(err, auth.ErrTransferSelf):
|
|
|
|
|
JSON(w, http.StatusOK, map[string]any{"status": "ok", "owner_user_id": body.UserID})
|
|
|
|
|
case errors.Is(err, auth.ErrOwnerRequired), errors.Is(err, auth.ErrNotCompanyMember):
|
|
|
|
|
Error(w, http.StatusBadRequest, err.Error())
|
|
|
|
|
case errors.Is(err, auth.ErrCompanyNotFound):
|
|
|
|
|
Error(w, http.StatusNotFound, err.Error())
|
|
|
|
|
default:
|
|
|
|
|
ClientOrLog(w, http.StatusBadRequest, "could not transfer ownership", err, auth.ClientError)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
JSON(w, http.StatusOK, map[string]any{"status": "ok", "owner_user_id": body.UserID})
|
|
|
|
|
}
|