1
0
forked from baron/baron-sso

merge feat/304-userfront-wasm-e2e into dev

This commit is contained in:
Lectom C Han
2026-02-24 15:40:51 +09:00
55 changed files with 3425 additions and 431 deletions

View File

@@ -35,7 +35,7 @@ type apiKeyListResponse struct {
func (h *ApiKeyHandler) ListApiKeys(c *fiber.Ctx) error {
if h.DB == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"})
return errorJSON(c, fiber.StatusServiceUnavailable, "database not available")
}
limit := c.QueryInt("limit", 50)
@@ -43,12 +43,12 @@ func (h *ApiKeyHandler) ListApiKeys(c *fiber.Ctx) error {
var total int64
if err := h.DB.Model(&domain.ApiKey{}).Count(&total).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
var keys []domain.ApiKey
if err := h.DB.Order("created_at desc").Limit(limit).Offset(offset).Find(&keys).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
items := make([]apiKeySummary, 0, len(keys))
@@ -73,7 +73,7 @@ func (h *ApiKeyHandler) ListApiKeys(c *fiber.Ctx) error {
func (h *ApiKeyHandler) CreateApiKey(c *fiber.Ctx) error {
if h.DB == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"})
return errorJSON(c, fiber.StatusServiceUnavailable, "database not available")
}
var req struct {
@@ -81,11 +81,11 @@ func (h *ApiKeyHandler) CreateApiKey(c *fiber.Ctx) error {
Scopes []string `json:"scopes"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
}
if strings.TrimSpace(req.Name) == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name is required"})
return errorJSON(c, fiber.StatusBadRequest, "name is required")
}
// Generate Client ID (16 chars hex)
@@ -96,7 +96,7 @@ func (h *ApiKeyHandler) CreateApiKey(c *fiber.Ctx) error {
hashedSecret, err := bcrypt.GenerateFromPassword([]byte(plainSecret), bcrypt.DefaultCost)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to hash secret"})
return errorJSON(c, fiber.StatusInternalServerError, "failed to hash secret")
}
apiKey := domain.ApiKey{
@@ -108,7 +108,7 @@ func (h *ApiKeyHandler) CreateApiKey(c *fiber.Ctx) error {
}
if err := h.DB.Create(&apiKey).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
// Return summary + PLAIN SECRET (only this time)
@@ -129,16 +129,16 @@ func (h *ApiKeyHandler) CreateApiKey(c *fiber.Ctx) error {
func (h *ApiKeyHandler) DeleteApiKey(c *fiber.Ctx) error {
if h.DB == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"})
return errorJSON(c, fiber.StatusServiceUnavailable, "database not available")
}
id := c.Params("id")
if id == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id is required"})
return errorJSON(c, fiber.StatusBadRequest, "id is required")
}
if err := h.DB.Delete(&domain.ApiKey{}, "id = ?", id).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
return c.SendStatus(fiber.StatusNoContent)

View File

@@ -23,9 +23,7 @@ func NewAuditHandler(repo domain.AuditRepository) *AuditHandler {
func (h *AuditHandler) CreateLog(c *fiber.Ctx) error {
var req domain.AuditLog
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Cannot parse JSON",
})
return errorJSON(c, fiber.StatusBadRequest, "Cannot parse JSON")
}
// Auto-fill metadata if missing
@@ -43,16 +41,12 @@ func (h *AuditHandler) CreateLog(c *fiber.Ctx) error {
}
if h.repo == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
"error": "Audit service unavailable",
})
return errorJSON(c, fiber.StatusServiceUnavailable, "Audit service unavailable")
}
if err := h.repo.Create(&req); err != nil {
// Log internal error but don't expose details
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Failed to save audit log",
})
return errorJSON(c, fiber.StatusInternalServerError, "Failed to save audit log")
}
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
@@ -66,22 +60,16 @@ func (h *AuditHandler) ListLogs(c *fiber.Ctx) error {
cursorRaw := c.Query("cursor")
cursor, err := parseAuditCursor(cursorRaw)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid cursor",
})
return errorJSON(c, fiber.StatusBadRequest, "Invalid cursor")
}
if h.repo == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
"error": "Audit service unavailable",
})
return errorJSON(c, fiber.StatusServiceUnavailable, "Audit service unavailable")
}
logs, err := h.repo.FindPage(c.Context(), limit+1, cursor)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "Failed to retrieve audit logs",
})
return errorJSON(c, fiber.StatusInternalServerError, "Failed to retrieve audit logs")
}
nextCursor := ""

File diff suppressed because it is too large Load Diff

View File

@@ -121,3 +121,26 @@ func TestEnchantedLinkFlow_Sms_Success(t *testing.T) {
json.NewDecoder(resp.Body).Decode(&initResp)
assert.NotEmpty(t, initResp["userCode"])
}
func TestPollEnchantedLink_ExpiredToken_ReturnsCode(t *testing.T) {
redis := &mockRedisRepo{data: make(map[string]string)}
h := &AuthHandler{
RedisService: redis,
}
app := fiber.New()
app.Post("/api/v1/auth/enchanted-link/poll", h.PollEnchantedLink)
body, _ := json.Marshal(map[string]string{
"pendingRef": "missing-ref",
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/enchanted-link/poll", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := app.Test(req, -1)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var got map[string]interface{}
json.NewDecoder(resp.Body).Decode(&got)
assert.Equal(t, "expired_token", got["error"])
assert.Equal(t, "expired_token", got["code"])
}

View File

@@ -0,0 +1,206 @@
package handler
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v2"
)
func newVerifyLoginCodeTestApp(h *AuthHandler) *fiber.App {
app := fiber.New()
app.Post("/api/v1/auth/login/code/verify", h.VerifyLoginCode)
app.Post("/api/v1/auth/login/code/verify-short", h.VerifyLoginShortCode)
return app
}
func decodeJSONBody(t *testing.T, resp *http.Response) map[string]any {
t.Helper()
var got map[string]any
if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
t.Fatalf("failed to decode response body: %v", err)
}
return got
}
func TestVerifyLoginCode_InvalidBody_ReturnsExplicitCode(t *testing.T) {
h := &AuthHandler{}
app := newVerifyLoginCodeTestApp(h)
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login/code/verify", bytes.NewBufferString("{"))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
got := decodeJSONBody(t, resp)
if got["code"] != "bad_request" {
t.Fatalf("expected code=bad_request, got %v", got["code"])
}
}
func TestVerifyLoginCode_IdpUnavailable_ReturnsExplicitCode(t *testing.T) {
h := &AuthHandler{}
app := newVerifyLoginCodeTestApp(h)
body, _ := json.Marshal(map[string]any{
"loginId": "user@example.com",
"code": "AA-111111",
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login/code/verify", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusServiceUnavailable {
t.Fatalf("expected 503, got %d", resp.StatusCode)
}
got := decodeJSONBody(t, resp)
if got["code"] != "service_unavailable" {
t.Fatalf("expected code=service_unavailable, got %v", got["code"])
}
}
func TestVerifyLoginCode_VerifyOnlyInvalidCode_ReturnsExplicitCode(t *testing.T) {
redis := &mockRedisRepo{data: make(map[string]string)}
redis.data[prefixLoginCode+"user@example.com"] = "flow-1"
redis.data[prefixLoginCodePending+"user@example.com"] = "pending-1"
redis.data[prefixLoginCodeValue+"pending-1"] = "AB-123"
h := &AuthHandler{
RedisService: redis,
IdpProvider: &mockIdpProvider{},
}
app := newVerifyLoginCodeTestApp(h)
body, _ := json.Marshal(map[string]any{
"loginId": "user@example.com",
"code": "ZZ-999",
"verifyOnly": true,
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login/code/verify", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", resp.StatusCode)
}
got := decodeJSONBody(t, resp)
if got["code"] != "invalid_code" {
t.Fatalf("expected code=invalid_code, got %v", got["code"])
}
}
func TestVerifyLoginShortCode_MissingShortCode_ReturnsExplicitCode(t *testing.T) {
h := &AuthHandler{
RedisService: &mockRedisRepo{data: make(map[string]string)},
}
app := newVerifyLoginCodeTestApp(h)
body, _ := json.Marshal(map[string]any{
"shortCode": "",
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login/code/verify-short", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
got := decodeJSONBody(t, resp)
if got["code"] != "bad_request" {
t.Fatalf("expected code=bad_request, got %v", got["code"])
}
}
func TestVerifyLoginShortCode_InvalidOrExpired_ReturnsExplicitCode(t *testing.T) {
h := &AuthHandler{
RedisService: &mockRedisRepo{data: make(map[string]string)},
}
app := newVerifyLoginCodeTestApp(h)
body, _ := json.Marshal(map[string]any{
"shortCode": "AB-123456",
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login/code/verify-short", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", resp.StatusCode)
}
got := decodeJSONBody(t, resp)
if got["code"] != "invalid_or_expired_code" {
t.Fatalf("expected code=invalid_or_expired_code, got %v", got["code"])
}
}
func TestVerifyLoginShortCode_VerifyOnlyMissingPendingRef_ReturnsExplicitCode(t *testing.T) {
redis := &mockRedisRepo{data: make(map[string]string)}
payload, _ := json.Marshal(shortLoginCodePayload{
LoginID: "user@example.com",
Code: "AB-123",
})
redis.data[prefixLoginCodeShort+"AB-123456"] = string(payload)
h := &AuthHandler{
RedisService: redis,
}
app := newVerifyLoginCodeTestApp(h)
body, _ := json.Marshal(map[string]any{
"shortCode": "AB-123456",
"verifyOnly": true,
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login/code/verify-short", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
got := decodeJSONBody(t, resp)
if got["code"] != "invalid_session_reference" {
t.Fatalf("expected code=invalid_session_reference, got %v", got["code"])
}
}

View File

@@ -0,0 +1,109 @@
package handler
import (
"baron-sso-backend/internal/domain"
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/require"
)
func TestUpdateMe_InvalidatesProfileCacheForTokenSession(t *testing.T) {
token := "token-abc"
identityID := "user-1"
traits := map[string]interface{}{
"email": "qa@example.com",
"name": "QA User",
"phone_number": "+821012345678",
"department": "Old Dept",
"affiliationType": "employee",
"companyCode": "",
"role": domain.RoleUser,
}
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
switch {
case r.URL.Host == "kratos.test" &&
r.URL.Path == "/sessions/whoami" &&
r.Method == http.MethodGet:
if r.Header.Get("X-Session-Token") != token {
return httpResponse(r, http.StatusUnauthorized, `{"error":"invalid token"}`), nil
}
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
"identity": map[string]interface{}{
"id": identityID,
"traits": traits,
},
}), nil
case r.URL.Host == "kratos.test" &&
r.URL.Path == "/admin/identities/"+identityID &&
r.Method == http.MethodPut:
var payload struct {
Traits map[string]interface{} `json:"traits"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
return httpResponse(r, http.StatusBadRequest, `{"error":"invalid body"}`), nil
}
for k, v := range payload.Traits {
traits[k] = v
}
return httpResponse(r, http.StatusOK, `{"ok":true}`), nil
}
return httpResponse(r, http.StatusNotFound, "not found"), nil
})
setDefaultHTTPClientForTest(t, transport)
t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test")
t.Setenv("KRATOS_ADMIN_URL", "http://kratos.test")
redis := &mockRedisRepo{data: make(map[string]string)}
h := &AuthHandler{
RedisService: redis,
}
app := fiber.New()
app.Get("/api/v1/user/me", h.GetMe)
app.Put("/api/v1/user/me", h.UpdateMe)
// 1) 첫 조회로 Old Dept가 캐시에 저장됨
getReq1 := httptest.NewRequest(http.MethodGet, "/api/v1/user/me", nil)
getReq1.Header.Set("Authorization", "Bearer "+token)
getResp1, err := app.Test(getReq1, -1)
require.NoError(t, err)
require.Equal(t, http.StatusOK, getResp1.StatusCode)
var profile1 map[string]interface{}
require.NoError(t, json.NewDecoder(getResp1.Body).Decode(&profile1))
require.Equal(t, "Old Dept", profile1["department"])
// 2) 소속을 New Dept로 변경
updateBody, _ := json.Marshal(map[string]string{
"name": "QA User",
"phone": "01012345678",
"department": "New Dept",
})
updateReq := httptest.NewRequest(
http.MethodPut,
"/api/v1/user/me",
bytes.NewReader(updateBody),
)
updateReq.Header.Set("Content-Type", "application/json")
updateReq.Header.Set("Authorization", "Bearer "+token)
updateResp, err := app.Test(updateReq, -1)
require.NoError(t, err)
require.Equal(t, http.StatusOK, updateResp.StatusCode)
require.Equal(t, "New Dept", traits["department"])
// 3) 새로고침 재조회 시 New Dept가 보여야 함(캐시 무효화 회귀 방지)
getReq2 := httptest.NewRequest(http.MethodGet, "/api/v1/user/me", nil)
getReq2.Header.Set("Authorization", "Bearer "+token)
getResp2, err := app.Test(getReq2, -1)
require.NoError(t, err)
require.Equal(t, http.StatusOK, getResp2.StatusCode)
var profile2 map[string]interface{}
require.NoError(t, json.NewDecoder(getResp2.Body).Decode(&profile2))
require.Equal(t, "New Dept", profile2["department"])
}

View File

@@ -83,6 +83,7 @@ func TestQRLoginFlow_Success(t *testing.T) {
var pollResp map[string]interface{}
json.NewDecoder(resp.Body).Decode(&pollResp)
assert.Equal(t, "authorization_pending", pollResp["error"])
assert.Equal(t, "authorization_pending", pollResp["code"])
// 3. Mock Approval
sessionData, _ := json.Marshal(map[string]string{

View File

@@ -1,6 +1,7 @@
package handler
import (
"baron-sso-backend/internal/middleware"
"bytes"
"encoding/json"
"fmt"
@@ -26,6 +27,13 @@ func newResetFlowTestApp(h *AuthHandler) *fiber.App {
return app
}
func newResetInitAppWithErrorCodeEnricher(h *AuthHandler) *fiber.App {
app := fiber.New()
app.Use(middleware.ErrorCodeEnricher())
app.Post("/api/v1/auth/password/reset/init", h.InitiatePasswordReset)
return app
}
type testRedisRepo struct {
values map[string]string
}
@@ -286,3 +294,35 @@ func TestProcessPasswordResetToken_EncodesLoginIDInRedirect(t *testing.T) {
t.Fatalf("expected encoded loginId round-trip=%s, got %s (location=%s)", loginID, gotLoginID, location)
}
}
func TestPasswordResetInit_LegacyErrorResponseHasCodeViaMiddleware(t *testing.T) {
h := &AuthHandler{}
app := newResetInitAppWithErrorCodeEnricher(h)
body, _ := json.Marshal(map[string]string{
"loginId": "",
})
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/password/reset/init", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", resp.StatusCode)
}
var got map[string]any
if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if got["error"] != "Login ID is required" {
t.Fatalf("unexpected error message: %v", got["error"])
}
if got["code"] != "bad_request" {
t.Fatalf("expected code=bad_request, got %v", got["code"])
}
}

View File

@@ -163,6 +163,14 @@ func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req)
}
func setDefaultHTTPClientForTest(t interface{ Cleanup(func()) }, transport http.RoundTripper) {
origDefault := http.DefaultClient
http.DefaultClient = &http.Client{Transport: transport}
t.Cleanup(func() {
http.DefaultClient = origDefault
})
}
func httpResponse(r *http.Request, code int, body string) *http.Response {
return &http.Response{
StatusCode: code,

View File

@@ -275,15 +275,13 @@ func (h *DevHandler) ListClients(c *fiber.Ctx) error {
clients, err := h.Hydra.ListClients(c.Context(), limit, offset)
if err != nil {
if errors.Is(err, service.ErrHydraNotFound) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "clients not found"})
return errorJSON(c, fiber.StatusNotFound, "clients not found")
}
errMsg := err.Error()
if strings.Contains(errMsg, "connection refused") || strings.Contains(errMsg, "dial tcp") {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
"error": "Hydra service is unavailable. Please check if Ory Hydra is running.",
})
return errorJSON(c, fiber.StatusServiceUnavailable, "Hydra service is unavailable. Please check if Ory Hydra is running.")
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": errMsg})
return errorJSON(c, fiber.StatusInternalServerError, errMsg)
}
items := make([]clientSummary, 0, len(clients))
@@ -306,15 +304,15 @@ func (h *DevHandler) ListClients(c *fiber.Ctx) error {
func (h *DevHandler) GetClient(c *fiber.Ctx) error {
clientID := c.Params("id")
if clientID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"})
return errorJSON(c, fiber.StatusBadRequest, "client id is required")
}
client, err := h.Hydra.GetClient(c.Context(), clientID)
if err != nil {
if errors.Is(err, service.ErrHydraNotFound) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"})
return errorJSON(c, fiber.StatusNotFound, "client not found")
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
summary := h.mapClientSummary(*client)
@@ -323,10 +321,10 @@ func (h *DevHandler) GetClient(c *fiber.Ctx) error {
if summary.Type == "private" {
isAppManager, err := h.checkAppManagerPermission(c)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "permission check error"})
return errorJSON(c, fiber.StatusInternalServerError, "permission check error")
}
if !isAppManager {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"})
return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions for private client")
}
}
@@ -345,19 +343,19 @@ func (h *DevHandler) GetClient(c *fiber.Ctx) error {
func (h *DevHandler) UpdateClientStatus(c *fiber.Ctx) error {
clientID := c.Params("id")
if clientID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"})
return errorJSON(c, fiber.StatusBadRequest, "client id is required")
}
var req struct {
Status string `json:"status"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
}
status := strings.ToLower(strings.TrimSpace(req.Status))
if status != "active" && status != "inactive" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "status must be active or inactive"})
return errorJSON(c, fiber.StatusBadRequest, "status must be active or inactive")
}
// [Security] Check permission before patching
@@ -367,7 +365,7 @@ func (h *DevHandler) UpdateClientStatus(c *fiber.Ctx) error {
if summary.Type == "private" {
isAppManager, _ := h.checkAppManagerPermission(c)
if !isAppManager {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"})
return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions for private client")
}
}
}
@@ -375,9 +373,9 @@ func (h *DevHandler) UpdateClientStatus(c *fiber.Ctx) error {
updated, err := h.Hydra.PatchClientStatus(c.Context(), clientID, status)
if err != nil {
if errors.Is(err, service.ErrHydraNotFound) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"})
return errorJSON(c, fiber.StatusNotFound, "client not found")
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
summary := h.mapClientSummary(*updated)
@@ -396,7 +394,7 @@ func (h *DevHandler) UpdateClientStatus(c *fiber.Ctx) error {
func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
var req clientUpsertRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
}
clientID := strings.TrimSpace(valueOr(req.ID, ""))
@@ -411,7 +409,7 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
redirectURIs := derefSlice(req.RedirectURIs, nil)
if len(redirectURIs) == 0 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "redirectUris is required"})
return errorJSON(c, fiber.StatusBadRequest, "redirectUris is required")
}
scopes := derefSlice(req.Scopes, defaultClientScopes())
@@ -420,23 +418,23 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
clientType := strings.ToLower(strings.TrimSpace(valueOr(req.Type, "private")))
if clientType != "pkce" && clientType != "private" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "type must be pkce or private"})
return errorJSON(c, fiber.StatusBadRequest, "type must be pkce or private")
}
// [Security] Check permission for private clients
if clientType == "private" {
isAppManager, err := h.checkAppManagerPermission(c)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "permission check error"})
return errorJSON(c, fiber.StatusInternalServerError, "permission check error")
}
if !isAppManager {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions to create private client"})
return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions to create private client")
}
}
status := strings.ToLower(strings.TrimSpace(valueOr(req.Status, "active")))
if status != "active" && status != "inactive" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "status must be active or inactive"})
return errorJSON(c, fiber.StatusBadRequest, "status must be active or inactive")
}
metadata := mergeMetadata(nil, req.Metadata)
@@ -468,7 +466,7 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
created, err := h.Hydra.CreateClient(c.Context(), clientReq)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
// Store secret in metadata for later retrieval
@@ -500,27 +498,27 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
clientID := strings.TrimSpace(c.Params("id"))
if clientID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"})
return errorJSON(c, fiber.StatusBadRequest, "client id is required")
}
var req clientUpsertRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
}
current, err := h.Hydra.GetClient(c.Context(), clientID)
if err != nil {
if errors.Is(err, service.ErrHydraNotFound) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"})
return errorJSON(c, fiber.StatusNotFound, "client not found")
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
clientType := ""
if req.Type != nil {
clientType = strings.ToLower(strings.TrimSpace(*req.Type))
if clientType != "pkce" && clientType != "private" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "type must be pkce or private"})
return errorJSON(c, fiber.StatusBadRequest, "type must be pkce or private")
}
}
@@ -529,10 +527,10 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
if currentSummary.Type == "private" || clientType == "private" {
isAppManager, err := h.checkAppManagerPermission(c)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "permission check error"})
return errorJSON(c, fiber.StatusInternalServerError, "permission check error")
}
if !isAppManager {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"})
return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions for private client")
}
}
@@ -540,7 +538,7 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
if req.Status != nil {
status = strings.ToLower(strings.TrimSpace(*req.Status))
if status != "active" && status != "inactive" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "status must be active or inactive"})
return errorJSON(c, fiber.StatusBadRequest, "status must be active or inactive")
}
}
@@ -554,7 +552,7 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
}
if req.RedirectURIs != nil && len(*req.RedirectURIs) == 0 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "redirectUris cannot be empty"})
return errorJSON(c, fiber.StatusBadRequest, "redirectUris cannot be empty")
}
metadata := mergeMetadata(current.Metadata, req.Metadata)
@@ -579,9 +577,9 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
updatedClient, err := h.Hydra.UpdateClient(c.Context(), clientID, updated)
if err != nil {
if errors.Is(err, service.ErrHydraNotFound) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"})
return errorJSON(c, fiber.StatusNotFound, "client not found")
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
summary := h.mapClientSummary(*updatedClient)
@@ -600,7 +598,7 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
func (h *DevHandler) DeleteClient(c *fiber.Ctx) error {
clientID := strings.TrimSpace(c.Params("id"))
if clientID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"})
return errorJSON(c, fiber.StatusBadRequest, "client id is required")
}
// [Security] Check permission for private clients
@@ -610,16 +608,16 @@ func (h *DevHandler) DeleteClient(c *fiber.Ctx) error {
if summary.Type == "private" {
isAppManager, _ := h.checkAppManagerPermission(c)
if !isAppManager {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"})
return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions for private client")
}
}
}
if err := h.Hydra.DeleteClient(c.Context(), clientID); err != nil {
if errors.Is(err, service.ErrHydraNotFound) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"})
return errorJSON(c, fiber.StatusNotFound, "client not found")
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
// 1. Clean up PostgreSQL
@@ -638,7 +636,7 @@ func (h *DevHandler) DeleteClient(c *fiber.Ctx) error {
func (h *DevHandler) ListConsents(c *fiber.Ctx) error {
clientID := strings.TrimSpace(c.Query("client_id"))
if clientID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client_id is required"})
return errorJSON(c, fiber.StatusBadRequest, "client_id is required")
}
subject := strings.TrimSpace(c.Query("subject"))
@@ -678,7 +676,7 @@ func (h *DevHandler) ListConsents(c *fiber.Ctx) error {
}
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
items := make([]consentSummary, 0, len(consents))
@@ -719,7 +717,7 @@ func (h *DevHandler) ListConsents(c *fiber.Ctx) error {
func (h *DevHandler) RevokeConsents(c *fiber.Ctx) error {
subject := strings.TrimSpace(c.Query("subject"))
if subject == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "subject is required"})
return errorJSON(c, fiber.StatusBadRequest, "subject is required")
}
clientID := strings.TrimSpace(c.Query("client_id"))
@@ -733,7 +731,7 @@ func (h *DevHandler) RevokeConsents(c *fiber.Ctx) error {
// 1. Revoke in Hydra
if err := h.Hydra.RevokeConsentSessions(c.Context(), subject, clientID); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
// 2. Sync to Local DB (Delete)
@@ -747,7 +745,7 @@ func (h *DevHandler) RevokeConsents(c *fiber.Ctx) error {
func (h *DevHandler) RotateClientSecret(c *fiber.Ctx) error {
clientID := strings.TrimSpace(c.Params("id"))
if clientID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"})
return errorJSON(c, fiber.StatusBadRequest, "client id is required")
}
// [Security] Check permission for private clients
@@ -757,7 +755,7 @@ func (h *DevHandler) RotateClientSecret(c *fiber.Ctx) error {
if summary.Type == "private" {
isAppManager, _ := h.checkAppManagerPermission(c)
if !isAppManager {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"})
return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions for private client")
}
}
}
@@ -765,22 +763,22 @@ func (h *DevHandler) RotateClientSecret(c *fiber.Ctx) error {
// 1. Generate new secret
newSecret, err := generateRandomSecret(20)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to generate secret"})
return errorJSON(c, fiber.StatusInternalServerError, "failed to generate secret")
}
// 2. Get current client to preserve other fields (already fetched above)
if err != nil {
if errors.Is(err, service.ErrHydraNotFound) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"})
return errorJSON(c, fiber.StatusNotFound, "client not found")
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
// 3. Update Hydra
current.ClientSecret = newSecret
updated, err := h.Hydra.UpdateClient(c.Context(), clientID, *current)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
// 4. Update Persistence (DB & Redis)

View File

@@ -0,0 +1,17 @@
package handler
import (
"baron-sso-backend/internal/response"
"github.com/gofiber/fiber/v2"
)
// errorJSON은 기존 error 필드를 유지하면서 기계 판독용 code를 명시적으로 추가합니다.
func errorJSON(c *fiber.Ctx, status int, message string) error {
return response.Error(c, status, response.StatusCode(status), message)
}
// errorJSONCode는 상태코드 기반 매핑만으로 부족한 경우 명시 코드를 강제할 때 사용합니다.
func errorJSONCode(c *fiber.Ctx, status int, code, message string) error {
return response.Error(c, status, code, message)
}

View File

@@ -33,13 +33,13 @@ func (h *FederationHandler) InitiateOIDCLogin(c *fiber.Ctx) error {
loginChallenge := c.Query("login_challenge")
if providerID == "" || loginChallenge == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "provider_id and login_challenge are required"})
return errorJSON(c, fiber.StatusBadRequest, "provider_id and login_challenge are required")
}
redirectURL, err := h.fedSvc.InitiateOIDCLogin(c.Context(), providerID, loginChallenge)
if err != nil {
// Log the error properly in a real application
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to initiate OIDC login"})
return errorJSON(c, fiber.StatusInternalServerError, "failed to initiate OIDC login")
}
return c.Redirect(redirectURL, fiber.StatusFound)
@@ -51,12 +51,12 @@ func (h *FederationHandler) HandleOIDCCallback(c *fiber.Ctx) error {
state := c.Query("state")
if code == "" || state == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "code and state are required"})
return errorJSON(c, fiber.StatusBadRequest, "code and state are required")
}
redirectURL, err := h.fedSvc.HandleOIDCCallback(c.Context(), code, state)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to handle OIDC callback"})
return errorJSON(c, fiber.StatusInternalServerError, "failed to handle OIDC callback")
}
return c.Redirect(redirectURL, fiber.StatusFound)
@@ -68,12 +68,12 @@ func (h *FederationHandler) HandleOIDCCallback(c *fiber.Ctx) error {
func (h *FederationHandler) ListIdpConfigsForClient(c *fiber.Ctx) error {
clientID := c.Params("clientId")
if clientID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "clientId is required"})
return errorJSON(c, fiber.StatusBadRequest, "clientId is required")
}
var configs []domain.IdentityProviderConfig
if err := h.db.Where("client_id = ?", clientID).Find(&configs).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
return c.JSON(configs)
@@ -83,12 +83,12 @@ func (h *FederationHandler) ListIdpConfigsForClient(c *fiber.Ctx) error {
func (h *FederationHandler) CreateIdpConfigForClient(c *fiber.Ctx) error {
clientID := c.Params("clientId")
if clientID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "clientId is required in path"})
return errorJSON(c, fiber.StatusBadRequest, "clientId is required in path")
}
var req domain.IdentityProviderConfig
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
}
// Assign clientID from path parameter
@@ -96,14 +96,14 @@ func (h *FederationHandler) CreateIdpConfigForClient(c *fiber.Ctx) error {
// Basic validation
if req.DisplayName == "" || req.ProviderType == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "display_name and provider_type are required"})
return errorJSON(c, fiber.StatusBadRequest, "display_name and provider_type are required")
}
// TODO: Optionally, validate if the clientID exists in Hydra
// Create in DB
if err := h.db.Create(&req).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
return c.Status(fiber.StatusCreated).JSON(req)
@@ -115,7 +115,7 @@ func (h *FederationHandler) CreateIdpConfigForClient(c *fiber.Ctx) error {
func (h *FederationHandler) ListIdpConfigsForTenant(c *fiber.Ctx) error {
tenantID := c.Params("tenantId")
if tenantID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenantId is required"})
return errorJSON(c, fiber.StatusBadRequest, "tenantId is required")
}
// This is a temporary solution. We should create a proper method in the repository.
@@ -123,7 +123,7 @@ func (h *FederationHandler) ListIdpConfigsForTenant(c *fiber.Ctx) error {
// Note: This now queries client_id, which is incorrect for tenants.
// This method is deprecated.
if err := h.db.Where("tenant_id = ?", tenantID).Find(&configs).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
return c.JSON(configs)
@@ -133,26 +133,26 @@ func (h *FederationHandler) ListIdpConfigsForTenant(c *fiber.Ctx) error {
func (h *FederationHandler) CreateIdpConfig(c *fiber.Ctx) error {
var req domain.IdentityProviderConfig
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
}
// Basic validation - This is the old validation logic
if req.ClientID == "" || req.DisplayName == "" || req.ProviderType == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client_id, display_name, and provider_type are required"})
return errorJSON(c, fiber.StatusBadRequest, "client_id, display_name, and provider_type are required")
}
// This check is now incorrect and deprecated.
var tenant domain.Tenant
if err := h.db.First(&tenant, "id = ?", req.ClientID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant not found"})
return errorJSON(c, fiber.StatusBadRequest, "tenant not found")
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
// Create in DB
if err := h.db.Create(&req).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
return c.Status(fiber.StatusCreated).JSON(req)

View File

@@ -20,17 +20,17 @@ func NewRelyingPartyHandler(s service.RelyingPartyService, kratos service.Kratos
func (h *RelyingPartyHandler) Create(c *fiber.Ctx) error {
tenantID := c.Params("tenantId")
if tenantID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenantId is required"})
return errorJSON(c, fiber.StatusBadRequest, "tenantId is required")
}
var req domain.HydraClient
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
}
rp, err := h.Service.Create(c.Context(), tenantID, req)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
return c.Status(fiber.StatusCreated).JSON(rp)
@@ -39,7 +39,7 @@ func (h *RelyingPartyHandler) Create(c *fiber.Ctx) error {
func (h *RelyingPartyHandler) ListAll(c *fiber.Ctx) error {
profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse)
if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "unauthorized: user profile not found in context"})
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: user profile not found in context")
}
var rps []domain.RelyingParty
@@ -51,11 +51,11 @@ func (h *RelyingPartyHandler) ListAll(c *fiber.Ctx) error {
rps, err = h.Service.List(c.Context(), *profile.TenantID)
} else {
slog.Warn("Forbidden access to all applications", "userID", profile.ID, "role", profile.Role)
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient role to list all applications"})
return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient role to list all applications")
}
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
return c.JSON(rps)
@@ -64,12 +64,12 @@ func (h *RelyingPartyHandler) ListAll(c *fiber.Ctx) error {
func (h *RelyingPartyHandler) List(c *fiber.Ctx) error {
tenantID := c.Params("tenantId")
if tenantID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenantId is required"})
return errorJSON(c, fiber.StatusBadRequest, "tenantId is required")
}
rps, err := h.Service.List(c.Context(), tenantID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
return c.JSON(rps)
@@ -79,7 +79,7 @@ func (h *RelyingPartyHandler) Get(c *fiber.Ctx) error {
id := c.Params("id")
rp, hydraClient, err := h.Service.Get(c.Context(), id)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "relying party not found"})
return errorJSON(c, fiber.StatusNotFound, "relying party not found")
}
return c.JSON(fiber.Map{
@@ -92,12 +92,12 @@ func (h *RelyingPartyHandler) Update(c *fiber.Ctx) error {
id := c.Params("id")
var req domain.HydraClient
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
}
rp, err := h.Service.Update(c.Context(), id, req)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
return c.JSON(rp)
@@ -106,7 +106,7 @@ func (h *RelyingPartyHandler) Update(c *fiber.Ctx) error {
func (h *RelyingPartyHandler) Delete(c *fiber.Ctx) error {
id := c.Params("id")
if err := h.Service.Delete(c.Context(), id); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
return c.SendStatus(fiber.StatusNoContent)

View File

@@ -58,17 +58,17 @@ func (h *TenantHandler) RegisterTenantPublic(c *fiber.Ctx) error {
AdminEmail string `json:"adminEmail"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
}
// Basic validation
if req.Name == "" || req.Domain == "" || req.AdminEmail == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name, domain, and adminEmail are required"})
return errorJSON(c, fiber.StatusBadRequest, "name, domain, and adminEmail are required")
}
tenant, err := h.Service.RequestRegistration(c.Context(), req.Name, req.Slug, req.Description, req.Domain, req.AdminEmail)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusBadRequest, err.Error())
}
return c.Status(fiber.StatusAccepted).JSON(fiber.Map{
@@ -80,11 +80,11 @@ func (h *TenantHandler) RegisterTenantPublic(c *fiber.Ctx) error {
func (h *TenantHandler) ApproveTenant(c *fiber.Ctx) error {
tenantID := c.Params("id")
if tenantID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant id is required"})
return errorJSON(c, fiber.StatusBadRequest, "tenant id is required")
}
if err := h.Service.ApproveTenant(c.Context(), tenantID); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
return c.JSON(fiber.Map{"message": "Tenant approved successfully"})
@@ -92,7 +92,7 @@ func (h *TenantHandler) ApproveTenant(c *fiber.Ctx) error {
func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
if h.DB == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"})
return errorJSON(c, fiber.StatusServiceUnavailable, "database not available")
}
limit := c.QueryInt("limit", 50)
@@ -106,12 +106,12 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
var total int64
if err := h.DB.Model(&domain.Tenant{}).Count(&total).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
var tenants []domain.Tenant
if err := h.DB.Order("created_at desc").Limit(limit).Offset(offset).Preload("Domains").Find(&tenants).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
items := make([]tenantSummary, 0, len(tenants))
@@ -124,20 +124,20 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
func (h *TenantHandler) GetTenant(c *fiber.Ctx) error {
if h.DB == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"})
return errorJSON(c, fiber.StatusServiceUnavailable, "database not available")
}
tenantID := strings.TrimSpace(c.Params("id"))
if tenantID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant id is required"})
return errorJSON(c, fiber.StatusBadRequest, "tenant id is required")
}
var tenant domain.Tenant
if err := h.DB.Preload("Domains").First(&tenant, "id = ?", tenantID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "tenant not found"})
return errorJSON(c, fiber.StatusNotFound, "tenant not found")
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
return c.JSON(mapTenantSummary(tenant))
@@ -145,7 +145,7 @@ func (h *TenantHandler) GetTenant(c *fiber.Ctx) error {
func (h *TenantHandler) CreateTenant(c *fiber.Ctx) error {
if h.DB == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"})
return errorJSON(c, fiber.StatusServiceUnavailable, "database not available")
}
var req struct {
@@ -158,12 +158,12 @@ func (h *TenantHandler) CreateTenant(c *fiber.Ctx) error {
Config map[string]any `json:"config"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
}
name := strings.TrimSpace(req.Name)
if name == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name is required"})
return errorJSON(c, fiber.StatusBadRequest, "name is required")
}
slug := normalizeTenantSlug(req.Slug)
@@ -171,7 +171,7 @@ func (h *TenantHandler) CreateTenant(c *fiber.Ctx) error {
slug = normalizeTenantSlug(name)
}
if slug == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "slug is required"})
return errorJSON(c, fiber.StatusBadRequest, "slug is required")
}
status := normalizeTenantStatus(req.Status)
@@ -189,9 +189,9 @@ func (h *TenantHandler) CreateTenant(c *fiber.Ctx) error {
tenant, err := h.Service.RegisterTenant(c.Context(), name, slug, req.Description, req.Domains, parentID)
if err != nil {
if strings.Contains(err.Error(), "already exists") {
return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusConflict, err.Error())
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
if req.Config != nil {
@@ -204,20 +204,20 @@ func (h *TenantHandler) CreateTenant(c *fiber.Ctx) error {
func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error {
if h.DB == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"})
return errorJSON(c, fiber.StatusServiceUnavailable, "database not available")
}
tenantID := strings.TrimSpace(c.Params("id"))
if tenantID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant id is required"})
return errorJSON(c, fiber.StatusBadRequest, "tenant id is required")
}
var tenant domain.Tenant
if err := h.DB.First(&tenant, "id = ?", tenantID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "tenant not found"})
return errorJSON(c, fiber.StatusNotFound, "tenant not found")
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
var req struct {
@@ -229,27 +229,27 @@ func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error {
Config map[string]any `json:"config"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
}
if req.Name != nil {
name := strings.TrimSpace(*req.Name)
if name == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name cannot be empty"})
return errorJSON(c, fiber.StatusBadRequest, "name cannot be empty")
}
tenant.Name = name
}
if req.Slug != nil {
slug := normalizeTenantSlug(*req.Slug)
if slug == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "slug cannot be empty"})
return errorJSON(c, fiber.StatusBadRequest, "slug cannot be empty")
}
if slug != tenant.Slug {
var exists domain.Tenant
if err := h.DB.Unscoped().Where("slug = ?", slug).First(&exists).Error; err == nil {
return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": "slug already exists"})
return errorJSON(c, fiber.StatusConflict, "slug already exists")
} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
tenant.Slug = slug
}
@@ -260,7 +260,7 @@ func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error {
if req.Status != nil {
status := normalizeTenantStatus(*req.Status)
if status == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "status must be active or inactive"})
return errorJSON(c, fiber.StatusBadRequest, "status must be active or inactive")
}
tenant.Status = status
}
@@ -269,14 +269,14 @@ func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error {
}
if err := h.DB.Save(&tenant).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
// Update domains if provided
if req.Domains != nil {
// Simple approach: Delete existing and recreate
if err := h.DB.Delete(&domain.TenantDomain{}, "tenant_id = ?", tenant.ID).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to clear old domains"})
return errorJSON(c, fiber.StatusInternalServerError, "failed to clear old domains")
}
for _, d := range req.Domains {
if strings.TrimSpace(d) == "" {
@@ -284,7 +284,7 @@ func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error {
}
// Use repository for consistency
if err := repository.NewTenantRepository(h.DB).AddDomain(c.Context(), tenant.ID, strings.TrimSpace(d), true); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to add domain: " + d})
return errorJSON(c, fiber.StatusInternalServerError, "failed to add domain: "+d)
}
}
}
@@ -297,30 +297,30 @@ func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error {
func (h *TenantHandler) DeleteTenant(c *fiber.Ctx) error {
if h.DB == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"})
return errorJSON(c, fiber.StatusServiceUnavailable, "database not available")
}
tenantID := strings.TrimSpace(c.Params("id"))
if tenantID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant id is required"})
return errorJSON(c, fiber.StatusBadRequest, "tenant id is required")
}
var tenant domain.Tenant
if err := h.DB.First(&tenant, "id = ?", tenantID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "tenant not found"})
return errorJSON(c, fiber.StatusNotFound, "tenant not found")
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
// Rename slug to release it for reuse before soft delete
deletedSlug := tenant.Slug + "-deleted-" + time.Now().Format("20060102150405")
if err := h.DB.Model(&tenant).Update("slug", deletedSlug).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to release slug"})
return errorJSON(c, fiber.StatusInternalServerError, "failed to release slug")
}
if err := h.DB.Delete(&tenant).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
return c.SendStatus(fiber.StatusNoContent)
@@ -329,13 +329,13 @@ func (h *TenantHandler) DeleteTenant(c *fiber.Ctx) error {
func (h *TenantHandler) ListAdmins(c *fiber.Ctx) error {
tenantID := c.Params("id")
if tenantID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant id is required"})
return errorJSON(c, fiber.StatusBadRequest, "tenant id is required")
}
// Fetch admins from Keto
relations, err := h.Keto.ListRelations(c.Context(), "Tenant", tenantID, "admins", "")
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
type adminInfo struct {
@@ -381,7 +381,7 @@ func (h *TenantHandler) AddAdmin(c *fiber.Ctx) error {
tenantID := c.Params("id")
userID := c.Params("userId")
if tenantID == "" || userID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenantId and userId are required"})
return errorJSON(c, fiber.StatusBadRequest, "tenantId and userId are required")
}
if h.KetoOutbox != nil {
@@ -401,7 +401,7 @@ func (h *TenantHandler) RemoveAdmin(c *fiber.Ctx) error {
tenantID := c.Params("id")
userID := c.Params("userId")
if tenantID == "" || userID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenantId and userId are required"})
return errorJSON(c, fiber.StatusBadRequest, "tenantId and userId are required")
}
if h.KetoOutbox != nil {

View File

@@ -19,7 +19,7 @@ func (h *UserGroupHandler) List(c *fiber.Ctx) error {
tenantID := c.Params("tenantId")
groups, err := h.Service.List(c.Context(), tenantID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
return c.JSON(groups)
}
@@ -42,7 +42,7 @@ func (h *UserGroupHandler) Get(c *fiber.Ctx) error {
id := c.Params("id")
group, err := h.Service.Get(c.Context(), id)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to get group: " + err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, "failed to get group: "+err.Error())
}
return c.JSON(group)
}
@@ -77,11 +77,11 @@ func (h *UserGroupHandler) AddMember(c *fiber.Ctx) error {
UserID string `json:"userId"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "userId is required"})
return errorJSON(c, fiber.StatusBadRequest, "userId is required")
}
if err := h.Service.AddMember(c.Context(), groupID, req.UserID); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
return c.SendStatus(fiber.StatusOK)
}
@@ -91,7 +91,7 @@ func (h *UserGroupHandler) RemoveMember(c *fiber.Ctx) error {
userID := c.Params("userId")
if err := h.Service.RemoveMember(c.Context(), groupID, userID); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
return c.SendStatus(fiber.StatusNoContent)
}
@@ -103,11 +103,11 @@ func (h *UserGroupHandler) AssignRole(c *fiber.Ctx) error {
Relation string `json:"relation"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid body"})
return errorJSON(c, fiber.StatusBadRequest, "invalid body")
}
if err := h.Service.AssignRoleToTenant(c.Context(), groupID, req.TenantID, req.Relation); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
return c.SendStatus(fiber.StatusOK)
}
@@ -116,7 +116,7 @@ func (h *UserGroupHandler) ListRoles(c *fiber.Ctx) error {
groupID := c.Params("id")
roles, err := h.Service.ListRoles(c.Context(), groupID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
return c.JSON(roles)
}
@@ -127,7 +127,7 @@ func (h *UserGroupHandler) RemoveRole(c *fiber.Ctx) error {
relation := c.Params("relation")
if err := h.Service.RemoveRoleFromTenant(c.Context(), groupID, tenantID, relation); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
return c.SendStatus(fiber.StatusNoContent)
}

View File

@@ -127,7 +127,7 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
// Fetch from UserRepo
users, total, err := h.UserRepo.List(c.Context(), offset, limit, search)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to fetch users from both kratos and local db"})
return errorJSON(c, fiber.StatusInternalServerError, "failed to fetch users from both kratos and local db")
}
items := make([]userSummary, 0, len(users))
@@ -156,20 +156,20 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
func (h *UserHandler) GetUser(c *fiber.Ctx) error {
if h.KratosAdmin == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "identity provider not available"})
return errorJSON(c, fiber.StatusServiceUnavailable, "identity provider not available")
}
userID := strings.TrimSpace(c.Params("id"))
if userID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "user id is required"})
return errorJSON(c, fiber.StatusBadRequest, "user id is required")
}
identity, err := h.KratosAdmin.GetIdentity(c.Context(), userID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
if identity == nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
return errorJSON(c, fiber.StatusNotFound, "user not found")
}
// [New] Check access scope
@@ -177,7 +177,7 @@ func (h *UserHandler) GetUser(c *fiber.Ctx) error {
if requester != nil && requester.Role == domain.RoleTenantAdmin {
compCode := extractTraitString(identity.Traits, "companyCode")
if requester.CompanyCode == "" || compCode != requester.CompanyCode {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: access to user in another tenant denied"})
return errorJSON(c, fiber.StatusForbidden, "forbidden: access to user in another tenant denied")
}
}
@@ -186,7 +186,7 @@ func (h *UserHandler) GetUser(c *fiber.Ctx) error {
func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
if h.OryProvider == nil || h.KratosAdmin == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "identity provider not available"})
return errorJSON(c, fiber.StatusServiceUnavailable, "identity provider not available")
}
var req struct {
@@ -200,19 +200,19 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
Metadata map[string]any `json:"metadata"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
}
email := strings.TrimSpace(req.Email)
if email == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "email is required"})
return errorJSON(c, fiber.StatusBadRequest, "email is required")
}
if !strings.Contains(email, "@") || !strings.Contains(email, ".") {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid email format"})
return errorJSON(c, fiber.StatusBadRequest, "invalid email format")
}
name := strings.TrimSpace(req.Name)
if name == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name is required"})
return errorJSON(c, fiber.StatusBadRequest, "name is required")
}
password := strings.TrimSpace(req.Password)
@@ -232,13 +232,13 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
if password == "" {
generated, genErr := utils.GeneratePasswordWithPolicy(policy)
if genErr != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to generate password"})
return errorJSON(c, fiber.StatusInternalServerError, "failed to generate password")
}
password = generated
generatedPassword = generated
} else {
if err := utils.ValidatePasswordWithPolicy(policy, password); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusBadRequest, err.Error())
}
}
@@ -284,9 +284,9 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
identityID, err := h.OryProvider.CreateUser(brokerUser, password)
if err != nil {
if strings.Contains(err.Error(), "already exists") {
return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": "email already exists"})
return errorJSON(c, fiber.StatusConflict, "email already exists")
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
// [New] Local DB Sync
@@ -351,7 +351,7 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
identity, err := h.KratosAdmin.GetIdentity(c.Context(), identityID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
if identity == nil {
return c.Status(fiber.StatusCreated).JSON(fiber.Map{"id": identityID, "initialPassword": generatedPassword})
@@ -366,20 +366,20 @@ func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
if h.KratosAdmin == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "identity provider not available"})
return errorJSON(c, fiber.StatusServiceUnavailable, "identity provider not available")
}
userID := strings.TrimSpace(c.Params("id"))
if userID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "user id is required"})
return errorJSON(c, fiber.StatusBadRequest, "user id is required")
}
identity, err := h.KratosAdmin.GetIdentity(c.Context(), userID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
if identity == nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "user not found"})
return errorJSON(c, fiber.StatusNotFound, "user not found")
}
// [New] Check access scope
@@ -387,7 +387,7 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
if requester != nil && requester.Role == domain.RoleTenantAdmin {
compCode := extractTraitString(identity.Traits, "companyCode")
if requester.CompanyCode == "" || compCode != requester.CompanyCode {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: cannot update user in another tenant"})
return errorJSON(c, fiber.StatusForbidden, "forbidden: cannot update user in another tenant")
}
}
@@ -402,13 +402,13 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
Metadata map[string]any `json:"metadata"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
}
// [New] Tenant Admin restriction: Cannot change companyCode
if requester != nil && requester.Role == domain.RoleTenantAdmin {
if req.CompanyCode != nil && *req.CompanyCode != requester.CompanyCode {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: tenant admins cannot change user's tenant"})
return errorJSON(c, fiber.StatusForbidden, "forbidden: tenant admins cannot change user's tenant")
}
}
@@ -468,7 +468,7 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
state := normalizeKratosState(req.Status)
updated, err := h.KratosAdmin.UpdateIdentity(c.Context(), userID, traits, state)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
// [New] Local DB Sync
@@ -561,7 +561,7 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
if req.Password != nil && *req.Password != "" {
if err := h.KratosAdmin.UpdateIdentityPassword(c.Context(), userID, *req.Password); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
}
@@ -570,12 +570,12 @@ func (h *UserHandler) UpdateUser(c *fiber.Ctx) error {
func (h *UserHandler) DeleteUser(c *fiber.Ctx) error {
if h.KratosAdmin == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "identity provider not available"})
return errorJSON(c, fiber.StatusServiceUnavailable, "identity provider not available")
}
userID := strings.TrimSpace(c.Params("id"))
if userID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "user id is required"})
return errorJSON(c, fiber.StatusBadRequest, "user id is required")
}
// [New] Check access scope before deletion
@@ -585,13 +585,13 @@ func (h *UserHandler) DeleteUser(c *fiber.Ctx) error {
if err == nil && identity != nil {
compCode := extractTraitString(identity.Traits, "companyCode")
if requester.CompanyCode == "" || compCode != requester.CompanyCode {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: cannot delete user in another tenant"})
return errorJSON(c, fiber.StatusForbidden, "forbidden: cannot delete user in another tenant")
}
}
}
if err := h.KratosAdmin.DeleteIdentity(c.Context(), userID); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
return errorJSON(c, fiber.StatusInternalServerError, err.Error())
}
// [Keto] Cleanup relations via Outbox