This commit is contained in:
2026-08-17 00:39:25 +02:00
parent 92f046b542
commit 93dc70123c
54 changed files with 25528 additions and 20666 deletions
+78 -11
View File
@@ -20,20 +20,25 @@ func (s *Server) handleGetCompany(w http.ResponseWriter, r *http.Request) {
name, language string
merge bool
contentLangs []string
ownerUserID *uuid.UUID
)
err := s.Pool.QueryRow(r.Context(), `
SELECT id, name, language, merge_products_by_gtin, COALESCE(content_languages, '{}')
SELECT id, name, language, merge_products_by_gtin, COALESCE(content_languages, '{}'), owner_user_id
FROM companies WHERE id = $1`, cid).
Scan(&id, &name, &language, &merge, &contentLangs)
Scan(&id, &name, &language, &merge, &contentLangs, &ownerUserID)
if err != nil {
Error(w, http.StatusNotFound, "company not found")
return
}
parsed, _ := company.ParseContentLanguages(contentLangs, language)
JSON(w, http.StatusOK, map[string]any{
out := map[string]any{
"id": id, "name": name, "language": language,
"content_languages": parsed, "merge_products_by_gtin": merge,
})
}
if ownerUserID != nil {
out["owner_user_id"] = *ownerUserID
}
JSON(w, http.StatusOK, out)
}
func (s *Server) handleUpdateCompany(w http.ResponseWriter, r *http.Request) {
@@ -150,6 +155,8 @@ func (s *Server) handlePutCompanySettings(w http.ResponseWriter, r *http.Request
func (s *Server) handleListTeam(w http.ResponseWriter, r *http.Request) {
cid, _ := CompanyIDFromContext(r.Context())
limit, offset := ParseLimitOffset(r)
var ownerUserID *uuid.UUID
_ = s.Pool.QueryRow(r.Context(), `SELECT owner_user_id FROM companies WHERE id = $1`, cid).Scan(&ownerUserID)
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 {
@@ -166,12 +173,13 @@ func (s *Server) handleListTeam(w http.ResponseWriter, r *http.Request) {
}
defer rows.Close()
type member struct {
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"`
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"`
}
out := make([]member, 0)
for rows.Next() {
@@ -180,9 +188,14 @@ func (s *Server) handleListTeam(w http.ResponseWriter, r *http.Request) {
Error(w, http.StatusInternalServerError, "scan failed")
return
}
m.IsOwner = ownerUserID != nil && *ownerUserID == m.UserID
out = append(out, m)
}
JSON(w, http.StatusOK, map[string]any{"members": out, "total": total, "limit": limit, "offset": offset})
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)
}
func (s *Server) handleCreateInvite(w http.ResponseWriter, r *http.Request) {
@@ -222,6 +235,7 @@ func (s *Server) handleCreateInvite(w http.ResponseWriter, r *http.Request) {
// Token returned when email was not delivered so operators can share the accept link.
if includeToken {
resp["token"] = token
resp["accept_url"] = mail.AcceptInviteURL(s.Config.WebOrigin, token)
}
JSON(w, http.StatusCreated, resp)
}
@@ -243,6 +257,13 @@ func (s *Server) handleRemoveMember(w http.ResponseWriter, r *http.Request) {
Error(w, http.StatusBadRequest, "invalid user id")
return
}
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
}
var currentRole, status string
err = s.Pool.QueryRow(r.Context(), `
SELECT role, status FROM memberships
@@ -361,3 +382,49 @@ func (s *Server) handleUpdateMemberRole(w http.ResponseWriter, r *http.Request)
}
JSON(w, http.StatusOK, map[string]any{"status": "ok", "role": newRole, "user_id": userID})
}
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})
}