package handler import ( "baron-sso-backend/internal/service" "errors" "strings" "time" "github.com/gofiber/fiber/v2" "github.com/google/uuid" ) type DevHandler struct { Hydra *service.HydraAdminService Redis *service.RedisService } func NewDevHandler(redis *service.RedisService) *DevHandler { return &DevHandler{ Hydra: service.NewHydraAdminService(), Redis: redis, } } type clientSummary struct { ID string `json:"id"` Name string `json:"name"` Type string `json:"type"` Status string `json:"status"` CreatedAt *time.Time `json:"createdAt,omitempty"` RedirectURIs []string `json:"redirectUris"` Scopes []string `json:"scopes"` ClientSecret string `json:"clientSecret,omitempty"` Metadata map[string]interface{} `json:"metadata,omitempty"` } type clientListResponse struct { Items []clientSummary `json:"items"` Limit int `json:"limit"` Offset int `json:"offset"` } type clientDetailResponse struct { Client clientSummary `json:"client"` Endpoints clientEndpoints `json:"endpoints"` } type clientEndpoints struct { Discovery string `json:"discovery"` Issuer string `json:"issuer"` Authorization string `json:"authorization"` Token string `json:"token"` UserInfo string `json:"userinfo"` } type consentSummary struct { Subject string `json:"subject"` ClientID string `json:"clientId"` ClientName string `json:"clientName,omitempty"` GrantedScopes []string `json:"grantedScopes"` AuthenticatedAt string `json:"authenticatedAt,omitempty"` } type consentListResponse struct { Items []consentSummary `json:"items"` } type clientUpsertRequest struct { ID *string `json:"id"` Name *string `json:"name"` Type *string `json:"type"` Status *string `json:"status"` RedirectURIs *[]string `json:"redirectUris"` Scopes *[]string `json:"scopes"` GrantTypes *[]string `json:"grantTypes"` ResponseTypes *[]string `json:"responseTypes"` TokenEndpointAuthMethod *string `json:"tokenEndpointAuthMethod"` Metadata *map[string]interface{} `json:"metadata"` } func (h *DevHandler) ListClients(c *fiber.Ctx) error { limit := c.QueryInt("limit", 50) offset := c.QueryInt("offset", 0) if limit <= 0 { limit = 50 } if offset < 0 { offset = 0 } 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"}) } 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 c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": errMsg}) } items := make([]clientSummary, 0, len(clients)) for _, client := range clients { items = append(items, h.mapClientSummary(client)) } return c.JSON(clientListResponse{ Items: items, Limit: limit, Offset: offset, }) } 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"}) } 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 c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } summary := h.mapClientSummary(*client) return c.JSON(clientDetailResponse{ Client: summary, Endpoints: clientEndpoints{ Discovery: strings.TrimRight(h.Hydra.PublicURL, "/") + "/.well-known/openid-configuration", Issuer: h.Hydra.PublicURL, Authorization: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/auth", Token: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/token", UserInfo: strings.TrimRight(h.Hydra.PublicURL, "/") + "/userinfo", }, }) } 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"}) } 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"}) } 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"}) } 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 c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } summary := h.mapClientSummary(*updated) return c.JSON(clientDetailResponse{ Client: summary, Endpoints: clientEndpoints{ Discovery: strings.TrimRight(h.Hydra.PublicURL, "/") + "/.well-known/openid-configuration", Issuer: h.Hydra.PublicURL, Authorization: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/auth", Token: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/token", UserInfo: strings.TrimRight(h.Hydra.PublicURL, "/") + "/userinfo", }, }) } 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"}) } clientID := strings.TrimSpace(valueOr(req.ID, "")) if clientID == "" { clientID = uuid.NewString() } name := strings.TrimSpace(valueOr(req.Name, "")) if name == "" { name = clientID } redirectURIs := derefSlice(req.RedirectURIs, nil) if len(redirectURIs) == 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "redirectUris is required"}) } scopes := derefSlice(req.Scopes, defaultClientScopes()) grantTypes := derefSlice(req.GrantTypes, defaultGrantTypes()) responseTypes := derefSlice(req.ResponseTypes, defaultResponseTypes()) clientType := strings.ToLower(strings.TrimSpace(valueOr(req.Type, "confidential"))) if clientType != "public" && clientType != "confidential" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "type must be public or confidential"}) } 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"}) } metadata := mergeMetadata(nil, req.Metadata) if metadata == nil { metadata = map[string]interface{}{} } metadata["status"] = status tokenAuthMethod := strings.TrimSpace(valueOr(req.TokenEndpointAuthMethod, "")) if tokenAuthMethod == "" { if clientType == "public" { tokenAuthMethod = "none" } else { tokenAuthMethod = "client_secret_basic" } } clientReq := service.HydraClient{ ClientID: clientID, ClientName: name, RedirectURIs: redirectURIs, GrantTypes: grantTypes, ResponseTypes: responseTypes, Scope: strings.Join(scopes, " "), TokenEndpointAuthMethod: tokenAuthMethod, Metadata: metadata, } created, err := h.Hydra.CreateClient(c.Context(), clientReq) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } // Store secret in metadata for later retrieval if created.ClientSecret != "" { if created.Metadata == nil { created.Metadata = map[string]interface{}{} } created.Metadata["client_secret"] = created.ClientSecret _, _ = h.Hydra.UpdateClient(c.Context(), created.ClientID, *created) // Also store in Redis if available if h.Redis != nil { _ = h.Redis.Set("client_secret:"+created.ClientID, created.ClientSecret, 0) } } summary := h.mapClientSummary(*created) return c.Status(fiber.StatusCreated).JSON(clientDetailResponse{ Client: summary, Endpoints: clientEndpoints{ Discovery: strings.TrimRight(h.Hydra.PublicURL, "/") + "/.well-known/openid-configuration", Issuer: h.Hydra.PublicURL, Authorization: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/auth", Token: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/token", UserInfo: strings.TrimRight(h.Hydra.PublicURL, "/") + "/userinfo", }, }) } 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"}) } var req clientUpsertRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "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 c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } clientType := "" if req.Type != nil { clientType = strings.ToLower(strings.TrimSpace(*req.Type)) if clientType != "public" && clientType != "confidential" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "type must be public or confidential"}) } } status := "" 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"}) } } tokenAuthMethod := strings.TrimSpace(valueOr(req.TokenEndpointAuthMethod, "")) if tokenAuthMethod == "" && clientType != "" { if clientType == "public" { tokenAuthMethod = "none" } else { tokenAuthMethod = "client_secret_basic" } } if req.RedirectURIs != nil && len(*req.RedirectURIs) == 0 { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "redirectUris cannot be empty"}) } metadata := mergeMetadata(current.Metadata, req.Metadata) if status != "" { if metadata == nil { metadata = map[string]interface{}{} } metadata["status"] = status } updated := service.HydraClient{ ClientID: current.ClientID, ClientName: valueOr(req.Name, current.ClientName), RedirectURIs: derefSlice(req.RedirectURIs, current.RedirectURIs), GrantTypes: derefSlice(req.GrantTypes, current.GrantTypes), ResponseTypes: derefSlice(req.ResponseTypes, current.ResponseTypes), Scope: buildScope(valueOrSlice(req.Scopes, strings.Fields(current.Scope))), TokenEndpointAuthMethod: resolveTokenAuthMethod(tokenAuthMethod, current.TokenEndpointAuthMethod), Metadata: metadata, } 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 c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } summary := h.mapClientSummary(*updatedClient) return c.JSON(clientDetailResponse{ Client: summary, Endpoints: clientEndpoints{ Discovery: strings.TrimRight(h.Hydra.PublicURL, "/") + "/.well-known/openid-configuration", Issuer: h.Hydra.PublicURL, Authorization: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/auth", Token: strings.TrimRight(h.Hydra.PublicURL, "/") + "/oauth2/token", UserInfo: strings.TrimRight(h.Hydra.PublicURL, "/") + "/userinfo", }, }) } 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"}) } 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 c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } // Clean up Redis if h.Redis != nil { _ = h.Redis.Delete("client_secret:" + clientID) } return c.SendStatus(fiber.StatusNoContent) } func (h *DevHandler) ListConsents(c *fiber.Ctx) error { subject := strings.TrimSpace(c.Query("subject")) if subject == "" { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "subject is required"}) } clientID := strings.TrimSpace(c.Query("client_id")) sessions, err := h.Hydra.ListConsentSessions(c.Context(), subject, clientID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } items := make([]consentSummary, 0, len(sessions)) for _, session := range sessions { client := session.Client if client.ClientID == "" && session.ConsentRequest != nil { client = session.ConsentRequest.Client } subject := session.Subject if subject == "" && session.ConsentRequest != nil { subject = session.ConsentRequest.Subject } authAt := "" if session.AuthenticatedAt != nil { authAt = session.AuthenticatedAt.Format(time.RFC3339) } else if session.RequestedAt != nil { authAt = session.RequestedAt.Format(time.RFC3339) } else if session.HandledAt != nil { authAt = session.HandledAt.Format(time.RFC3339) } items = append(items, consentSummary{ Subject: subject, ClientID: client.ClientID, ClientName: client.ClientName, GrantedScopes: session.GrantedScope, AuthenticatedAt: authAt, }) } return c.JSON(consentListResponse{Items: items}) } 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"}) } clientID := strings.TrimSpace(c.Query("client_id")) if err := h.Hydra.RevokeConsentSessions(c.Context(), subject, clientID); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.SendStatus(fiber.StatusNoContent) } func (h *DevHandler) mapClientSummary(client service.HydraClient) clientSummary { status := "active" if client.Metadata != nil { if value, ok := client.Metadata["status"].(string); ok && strings.ToLower(value) == "inactive" { status = "inactive" } } clientType := "confidential" if strings.EqualFold(client.TokenEndpointAuthMethod, "none") { clientType = "public" } name := strings.TrimSpace(client.ClientName) if name == "" { name = client.ClientID } scopes := strings.Fields(client.Scope) clientSecret := client.ClientSecret // 1. Check Metadata (Legacy/Fallback) if clientSecret == "" && client.Metadata != nil { if val, ok := client.Metadata["client_secret"].(string); ok { clientSecret = val } } // 2. Check Redis (New) if clientSecret == "" && h.Redis != nil { if val, err := h.Redis.Get("client_secret:" + client.ClientID); err == nil && val != "" { clientSecret = val } } return clientSummary{ ID: client.ClientID, Name: name, Type: clientType, Status: status, RedirectURIs: client.RedirectURIs, Scopes: scopes, ClientSecret: clientSecret, Metadata: client.Metadata, } } func defaultClientScopes() []string { return []string{"openid", "profile", "email"} } func defaultGrantTypes() []string { return []string{"authorization_code", "refresh_token"} } func defaultResponseTypes() []string { return []string{"code"} } func buildScope(scopes []string) string { return strings.Join(scopes, " ") } func valueOr(ptr *string, fallback string) string { if ptr == nil { return fallback } return *ptr } func valueOrSlice(ptr *[]string, fallback []string) []string { if ptr == nil { return fallback } return *ptr } func derefSlice(ptr *[]string, fallback []string) []string { if ptr == nil { return fallback } return *ptr } func mergeMetadata(current map[string]interface{}, incoming *map[string]interface{}) map[string]interface{} { if incoming == nil { return current } merged := map[string]interface{}{} for k, v := range current { merged[k] = v } for k, v := range *incoming { merged[k] = v } return merged } func resolveTokenAuthMethod(requested, fallback string) string { if strings.TrimSpace(requested) == "" { return fallback } return requested }