1
0
forked from baron/baron-sso

backend swagger/redoc 추가

This commit is contained in:
Lectom C Han
2026-01-28 13:06:09 +09:00
parent 1883753414
commit b7a0397ef9
15 changed files with 3890 additions and 20 deletions

View File

@@ -34,6 +34,7 @@ func migrateSchemas(db *gorm.DB) error {
// Add all domain models here
return db.AutoMigrate(
&domain.User{},
&domain.Tenant{},
// &domain.RelyingParty{}, // TODO: Uncomment when model is ready
// &domain.UserConsent{}, // TODO: Uncomment when model is ready
)

View File

@@ -0,0 +1,28 @@
package domain
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// Tenant represents a tenant model stored in PostgreSQL.
type Tenant struct {
ID string `gorm:"primaryKey;type:uuid;default:gen_random_uuid()" json:"id"`
Name string `gorm:"not null" json:"name"`
Slug string `gorm:"uniqueIndex;not null" json:"slug"`
Description string `json:"description"`
Status string `gorm:"default:'active'" json:"status"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// BeforeCreate hook to generate UUID if not present.
func (t *Tenant) BeforeCreate(tx *gorm.DB) (err error) {
if t.ID == "" {
t.ID = uuid.NewString()
}
return
}

View File

@@ -0,0 +1,20 @@
package handler
import (
"os"
"github.com/gofiber/fiber/v2"
)
func requireAdmin(c *fiber.Ctx) error {
adminPass := os.Getenv("ADMIN_PASSWORD")
if adminPass == "" {
adminPass = "admin"
}
reqPass := c.Get("X-Admin-Password")
if reqPass != adminPass {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid Admin Password"})
}
return nil
}

View File

@@ -51,7 +51,7 @@ func (h *AdminHandler) checkAuth(c *fiber.Ctx) error {
}
func (h *AdminHandler) CheckAuth(c *fiber.Ctx) error {
if err := h.checkAuth(c); err != nil {
if err := requireAdmin(c); err != nil {
return err
}
return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "ok"})

View File

@@ -7,6 +7,7 @@ import (
"time"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
type DevHandler struct {
@@ -37,8 +38,8 @@ type clientListResponse struct {
}
type clientDetailResponse struct {
Client clientSummary `json:"client"`
Endpoints clientEndpoints `json:"endpoints"`
Client clientSummary `json:"client"`
Endpoints clientEndpoints `json:"endpoints"`
}
type clientEndpoints struct {
@@ -61,6 +62,19 @@ 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)
@@ -163,6 +177,189 @@ 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"})
}
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"
}
}
client := 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(), client)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
summary := 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 := 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()})
}
return c.SendStatus(fiber.StatusNoContent)
}
func (h *DevHandler) ListConsents(c *fiber.Ctx) error {
subject := strings.TrimSpace(c.Query("subject"))
if subject == "" {
@@ -239,3 +436,61 @@ func mapClientSummary(client service.HydraClient) clientSummary {
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
}

View File

@@ -0,0 +1,278 @@
package handler
import (
"baron-sso-backend/internal/domain"
"errors"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)
type TenantHandler struct {
DB *gorm.DB
}
func NewTenantHandler(db *gorm.DB) *TenantHandler {
return &TenantHandler{DB: db}
}
type tenantSummary struct {
ID string `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"`
Status string `json:"status"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
type tenantListResponse struct {
Items []tenantSummary `json:"items"`
Limit int `json:"limit"`
Offset int `json:"offset"`
Total int64 `json:"total"`
}
func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
if err := requireAdmin(c); err != nil {
return err
}
if h.DB == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"})
}
limit := c.QueryInt("limit", 50)
offset := c.QueryInt("offset", 0)
if limit <= 0 {
limit = 50
}
if offset < 0 {
offset = 0
}
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()})
}
var tenants []domain.Tenant
if err := h.DB.Order("created_at desc").Limit(limit).Offset(offset).Find(&tenants).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
items := make([]tenantSummary, 0, len(tenants))
for _, t := range tenants {
items = append(items, mapTenantSummary(t))
}
return c.JSON(tenantListResponse{Items: items, Limit: limit, Offset: offset, Total: total})
}
func (h *TenantHandler) GetTenant(c *fiber.Ctx) error {
if err := requireAdmin(c); err != nil {
return err
}
if h.DB == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"})
}
tenantID := strings.TrimSpace(c.Params("id"))
if tenantID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "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 c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(mapTenantSummary(tenant))
}
func (h *TenantHandler) CreateTenant(c *fiber.Ctx) error {
if err := requireAdmin(c); err != nil {
return err
}
if h.DB == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"})
}
var req struct {
Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"`
Status string `json:"status"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
}
name := strings.TrimSpace(req.Name)
if name == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name is required"})
}
slug := normalizeTenantSlug(req.Slug)
if slug == "" {
slug = normalizeTenantSlug(name)
}
if slug == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "slug is required"})
}
status := normalizeTenantStatus(req.Status)
if status == "" {
status = "active"
}
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"})
} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
tenant := domain.Tenant{
Name: name,
Slug: slug,
Description: strings.TrimSpace(req.Description),
Status: status,
}
if err := h.DB.Create(&tenant).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(fiber.StatusCreated).JSON(mapTenantSummary(tenant))
}
func (h *TenantHandler) UpdateTenant(c *fiber.Ctx) error {
if err := requireAdmin(c); err != nil {
return err
}
if h.DB == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"})
}
tenantID := strings.TrimSpace(c.Params("id"))
if tenantID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "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 c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
var req struct {
Name *string `json:"name"`
Slug *string `json:"slug"`
Description *string `json:"description"`
Status *string `json:"status"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "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"})
}
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"})
}
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"})
} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
tenant.Slug = slug
}
}
if req.Description != nil {
tenant.Description = strings.TrimSpace(*req.Description)
}
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"})
}
tenant.Status = status
}
if err := h.DB.Save(&tenant).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(mapTenantSummary(tenant))
}
func (h *TenantHandler) DeleteTenant(c *fiber.Ctx) error {
if err := requireAdmin(c); err != nil {
return err
}
if h.DB == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "database not available"})
}
tenantID := strings.TrimSpace(c.Params("id"))
if tenantID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant id is required"})
}
if err := h.DB.Delete(&domain.Tenant{}, "id = ?", tenantID).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.SendStatus(fiber.StatusNoContent)
}
func mapTenantSummary(t domain.Tenant) tenantSummary {
return tenantSummary{
ID: t.ID,
Name: t.Name,
Slug: t.Slug,
Description: t.Description,
Status: t.Status,
CreatedAt: t.CreatedAt.Format(time.RFC3339),
UpdatedAt: t.UpdatedAt.Format(time.RFC3339),
}
}
func normalizeTenantSlug(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
value = strings.ReplaceAll(value, " ", "-")
var b strings.Builder
for _, r := range value {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
b.WriteRune(r)
}
}
return strings.Trim(b.String(), "-")
}
func normalizeTenantStatus(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
if value == "" {
return ""
}
if value != "active" && value != "inactive" {
return ""
}
return value
}

View File

@@ -19,30 +19,30 @@ var ErrHydraNotFound = errors.New("hydra admin: resource not found")
// HydraAdminService는 Hydra Admin API 호출을 래핑합니다.
type HydraAdminService struct {
AdminURL string
PublicURL string
AdminURL string
PublicURL string
HTTPClient *http.Client
}
type HydraClient struct {
ClientID string `json:"client_id"`
ClientName string `json:"client_name,omitempty"`
RedirectURIs []string `json:"redirect_uris,omitempty"`
GrantTypes []string `json:"grant_types,omitempty"`
ResponseTypes []string `json:"response_types,omitempty"`
Scope string `json:"scope,omitempty"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
ClientID string `json:"client_id"`
ClientName string `json:"client_name,omitempty"`
RedirectURIs []string `json:"redirect_uris,omitempty"`
GrantTypes []string `json:"grant_types,omitempty"`
ResponseTypes []string `json:"response_types,omitempty"`
Scope string `json:"scope,omitempty"`
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type HydraConsentSession struct {
Subject string `json:"subject"`
GrantedScope []string `json:"granted_scope"`
GrantedAudience []string `json:"granted_audience,omitempty"`
Remember bool `json:"remember"`
AuthenticatedAt *time.Time `json:"authenticated_at,omitempty"`
RequestedAt *time.Time `json:"requested_at,omitempty"`
Client HydraClient `json:"client"`
Subject string `json:"subject"`
GrantedScope []string `json:"granted_scope"`
GrantedAudience []string `json:"granted_audience,omitempty"`
Remember bool `json:"remember"`
AuthenticatedAt *time.Time `json:"authenticated_at,omitempty"`
RequestedAt *time.Time `json:"requested_at,omitempty"`
Client HydraClient `json:"client"`
}
func NewHydraAdminService() *HydraAdminService {
@@ -151,6 +151,89 @@ func (s *HydraAdminService) PatchClientStatus(ctx context.Context, clientID, sta
return &updated, nil
}
func (s *HydraAdminService) CreateClient(ctx context.Context, client HydraClient) (*HydraClient, error) {
body, _ := json.Marshal(client)
endpoint := fmt.Sprintf("%s/clients", strings.TrimRight(s.AdminURL, "/"))
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := s.httpClient().Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
return nil, fmt.Errorf("hydra admin: create client failed status=%d body=%s", resp.StatusCode, string(respBody))
}
var created HydraClient
if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
return nil, fmt.Errorf("hydra admin: decode created client failed: %w", err)
}
return &created, nil
}
func (s *HydraAdminService) UpdateClient(ctx context.Context, clientID string, client HydraClient) (*HydraClient, error) {
client.ClientID = clientID
body, _ := json.Marshal(client)
endpoint := fmt.Sprintf("%s/clients/%s", strings.TrimRight(s.AdminURL, "/"), url.PathEscape(clientID))
req, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := s.httpClient().Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, ErrHydraNotFound
}
if resp.StatusCode >= 300 {
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
return nil, fmt.Errorf("hydra admin: update client failed status=%d body=%s", resp.StatusCode, string(respBody))
}
var updated HydraClient
if err := json.NewDecoder(resp.Body).Decode(&updated); err != nil {
return nil, fmt.Errorf("hydra admin: decode updated client failed: %w", err)
}
return &updated, nil
}
func (s *HydraAdminService) DeleteClient(ctx context.Context, clientID string) error {
endpoint := fmt.Sprintf("%s/clients/%s", strings.TrimRight(s.AdminURL, "/"), url.PathEscape(clientID))
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil)
if err != nil {
return err
}
resp, err := s.httpClient().Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return ErrHydraNotFound
}
if resp.StatusCode >= 300 {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
return fmt.Errorf("hydra admin: delete client failed status=%d body=%s", resp.StatusCode, string(body))
}
return nil
}
func (s *HydraAdminService) ListConsentSessions(ctx context.Context, subject, clientID string) ([]HydraConsentSession, error) {
params := map[string]string{
"subject": subject,