forked from baron/baron-sso
backend swagger/redoc 추가
This commit is contained in:
@@ -162,6 +162,7 @@ func main() {
|
|||||||
authHandler := handler.NewAuthHandler(redisService, idpProvider)
|
authHandler := handler.NewAuthHandler(redisService, idpProvider)
|
||||||
adminHandler := handler.NewAdminHandler()
|
adminHandler := handler.NewAdminHandler()
|
||||||
devHandler := handler.NewDevHandler()
|
devHandler := handler.NewDevHandler()
|
||||||
|
tenantHandler := handler.NewTenantHandler(db)
|
||||||
|
|
||||||
// 3. Initialize Fiber
|
// 3. Initialize Fiber
|
||||||
appEnv := getEnv("APP_ENV", "dev")
|
appEnv := getEnv("APP_ENV", "dev")
|
||||||
@@ -265,6 +266,25 @@ func main() {
|
|||||||
Key: cookieSecret,
|
Key: cookieSecret,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
app.Get("/docs", func(c *fiber.Ctx) error {
|
||||||
|
return c.SendFile("./docs/swagger-ui/index.html")
|
||||||
|
})
|
||||||
|
app.Get("/docs/", func(c *fiber.Ctx) error {
|
||||||
|
return c.SendFile("./docs/swagger-ui/index.html")
|
||||||
|
})
|
||||||
|
app.Static("/docs", "./docs/swagger-ui")
|
||||||
|
app.Get("/redoc", func(c *fiber.Ctx) error {
|
||||||
|
return c.SendFile("./docs/redoc/index.html")
|
||||||
|
})
|
||||||
|
app.Get("/redoc/", func(c *fiber.Ctx) error {
|
||||||
|
return c.SendFile("./docs/redoc/index.html")
|
||||||
|
})
|
||||||
|
app.Static("/redoc", "./docs/redoc")
|
||||||
|
app.Get("/openapi.yaml", func(c *fiber.Ctx) error {
|
||||||
|
c.Type("yaml")
|
||||||
|
return c.SendFile("./docs/openapi.yaml")
|
||||||
|
})
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
app.Get("/", func(c *fiber.Ctx) error {
|
app.Get("/", func(c *fiber.Ctx) error {
|
||||||
return c.SendString("Baron SSO Audit Backend Online")
|
return c.SendString("Baron SSO Audit Backend Online")
|
||||||
@@ -355,12 +375,20 @@ func main() {
|
|||||||
// Admin Routes
|
// Admin Routes
|
||||||
admin := api.Group("/admin")
|
admin := api.Group("/admin")
|
||||||
admin.Get("/check", adminHandler.CheckAuth)
|
admin.Get("/check", adminHandler.CheckAuth)
|
||||||
|
admin.Get("/tenants", tenantHandler.ListTenants)
|
||||||
|
admin.Post("/tenants", tenantHandler.CreateTenant)
|
||||||
|
admin.Get("/tenants/:id", tenantHandler.GetTenant)
|
||||||
|
admin.Put("/tenants/:id", tenantHandler.UpdateTenant)
|
||||||
|
admin.Delete("/tenants/:id", tenantHandler.DeleteTenant)
|
||||||
|
|
||||||
// 개발자 포털 라우트 (RP/Consent 관리)
|
// 개발자 포털 라우트 (RP/Consent 관리)
|
||||||
dev := api.Group("/dev")
|
dev := api.Group("/dev")
|
||||||
dev.Get("/clients", devHandler.ListClients)
|
dev.Get("/clients", devHandler.ListClients)
|
||||||
|
dev.Post("/clients", devHandler.CreateClient)
|
||||||
dev.Get("/clients/:id", devHandler.GetClient)
|
dev.Get("/clients/:id", devHandler.GetClient)
|
||||||
|
dev.Put("/clients/:id", devHandler.UpdateClient)
|
||||||
dev.Patch("/clients/:id/status", devHandler.UpdateClientStatus)
|
dev.Patch("/clients/:id/status", devHandler.UpdateClientStatus)
|
||||||
|
dev.Delete("/clients/:id", devHandler.DeleteClient)
|
||||||
dev.Get("/consents", devHandler.ListConsents)
|
dev.Get("/consents", devHandler.ListConsents)
|
||||||
dev.Delete("/consents", devHandler.RevokeConsents)
|
dev.Delete("/consents", devHandler.RevokeConsents)
|
||||||
|
|
||||||
|
|||||||
1282
backend/docs/openapi.yaml
Normal file
1282
backend/docs/openapi.yaml
Normal file
File diff suppressed because it is too large
Load Diff
18
backend/docs/redoc/index.html
Normal file
18
backend/docs/redoc/index.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Baron SSO ReDoc</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<redoc spec-url="/openapi.yaml"></redoc>
|
||||||
|
<script src="/redoc/redoc.standalone.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1832
backend/docs/redoc/redoc.standalone.js
Normal file
1832
backend/docs/redoc/redoc.standalone.js
Normal file
File diff suppressed because one or more lines are too long
38
backend/docs/swagger-ui/index.html
Normal file
38
backend/docs/swagger-ui/index.html
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Baron SSO Swagger UI</title>
|
||||||
|
<link rel="stylesheet" href="/docs/swagger-ui.css" />
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: #f7f7f8;
|
||||||
|
}
|
||||||
|
.topbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="swagger-ui"></div>
|
||||||
|
<script src="/docs/swagger-ui-bundle.js"></script>
|
||||||
|
<script src="/docs/swagger-ui-standalone-preset.js"></script>
|
||||||
|
<script>
|
||||||
|
window.onload = () => {
|
||||||
|
SwaggerUIBundle({
|
||||||
|
url: "/openapi.yaml",
|
||||||
|
dom_id: "#swagger-ui",
|
||||||
|
deepLinking: true,
|
||||||
|
persistAuthorization: true,
|
||||||
|
presets: [
|
||||||
|
SwaggerUIBundle.presets.apis,
|
||||||
|
SwaggerUIStandalonePreset,
|
||||||
|
],
|
||||||
|
layout: "StandaloneLayout",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2
backend/docs/swagger-ui/swagger-ui-bundle.js
Normal file
2
backend/docs/swagger-ui/swagger-ui-bundle.js
Normal file
File diff suppressed because one or more lines are too long
2
backend/docs/swagger-ui/swagger-ui-standalone-preset.js
Normal file
2
backend/docs/swagger-ui/swagger-ui-standalone-preset.js
Normal file
File diff suppressed because one or more lines are too long
3
backend/docs/swagger-ui/swagger-ui.css
Normal file
3
backend/docs/swagger-ui/swagger-ui.css
Normal file
File diff suppressed because one or more lines are too long
@@ -34,6 +34,7 @@ func migrateSchemas(db *gorm.DB) error {
|
|||||||
// Add all domain models here
|
// Add all domain models here
|
||||||
return db.AutoMigrate(
|
return db.AutoMigrate(
|
||||||
&domain.User{},
|
&domain.User{},
|
||||||
|
&domain.Tenant{},
|
||||||
// &domain.RelyingParty{}, // TODO: Uncomment when model is ready
|
// &domain.RelyingParty{}, // TODO: Uncomment when model is ready
|
||||||
// &domain.UserConsent{}, // TODO: Uncomment when model is ready
|
// &domain.UserConsent{}, // TODO: Uncomment when model is ready
|
||||||
)
|
)
|
||||||
|
|||||||
28
backend/internal/domain/tenant.go
Normal file
28
backend/internal/domain/tenant.go
Normal 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
|
||||||
|
}
|
||||||
20
backend/internal/handler/admin_auth.go
Normal file
20
backend/internal/handler/admin_auth.go
Normal 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
|
||||||
|
}
|
||||||
@@ -51,7 +51,7 @@ func (h *AdminHandler) checkAuth(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 err
|
||||||
}
|
}
|
||||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "ok"})
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "ok"})
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DevHandler struct {
|
type DevHandler struct {
|
||||||
@@ -37,8 +38,8 @@ type clientListResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type clientDetailResponse struct {
|
type clientDetailResponse struct {
|
||||||
Client clientSummary `json:"client"`
|
Client clientSummary `json:"client"`
|
||||||
Endpoints clientEndpoints `json:"endpoints"`
|
Endpoints clientEndpoints `json:"endpoints"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type clientEndpoints struct {
|
type clientEndpoints struct {
|
||||||
@@ -61,6 +62,19 @@ type consentListResponse struct {
|
|||||||
Items []consentSummary `json:"items"`
|
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 {
|
func (h *DevHandler) ListClients(c *fiber.Ctx) error {
|
||||||
limit := c.QueryInt("limit", 50)
|
limit := c.QueryInt("limit", 50)
|
||||||
offset := c.QueryInt("offset", 0)
|
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 {
|
func (h *DevHandler) ListConsents(c *fiber.Ctx) error {
|
||||||
subject := strings.TrimSpace(c.Query("subject"))
|
subject := strings.TrimSpace(c.Query("subject"))
|
||||||
if subject == "" {
|
if subject == "" {
|
||||||
@@ -239,3 +436,61 @@ func mapClientSummary(client service.HydraClient) clientSummary {
|
|||||||
Metadata: client.Metadata,
|
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
|
||||||
|
}
|
||||||
|
|||||||
278
backend/internal/handler/tenant_handler.go
Normal file
278
backend/internal/handler/tenant_handler.go
Normal 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
|
||||||
|
}
|
||||||
@@ -19,30 +19,30 @@ var ErrHydraNotFound = errors.New("hydra admin: resource not found")
|
|||||||
|
|
||||||
// HydraAdminService는 Hydra Admin API 호출을 래핑합니다.
|
// HydraAdminService는 Hydra Admin API 호출을 래핑합니다.
|
||||||
type HydraAdminService struct {
|
type HydraAdminService struct {
|
||||||
AdminURL string
|
AdminURL string
|
||||||
PublicURL string
|
PublicURL string
|
||||||
HTTPClient *http.Client
|
HTTPClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
type HydraClient struct {
|
type HydraClient struct {
|
||||||
ClientID string `json:"client_id"`
|
ClientID string `json:"client_id"`
|
||||||
ClientName string `json:"client_name,omitempty"`
|
ClientName string `json:"client_name,omitempty"`
|
||||||
RedirectURIs []string `json:"redirect_uris,omitempty"`
|
RedirectURIs []string `json:"redirect_uris,omitempty"`
|
||||||
GrantTypes []string `json:"grant_types,omitempty"`
|
GrantTypes []string `json:"grant_types,omitempty"`
|
||||||
ResponseTypes []string `json:"response_types,omitempty"`
|
ResponseTypes []string `json:"response_types,omitempty"`
|
||||||
Scope string `json:"scope,omitempty"`
|
Scope string `json:"scope,omitempty"`
|
||||||
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
|
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"`
|
||||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HydraConsentSession struct {
|
type HydraConsentSession struct {
|
||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
GrantedScope []string `json:"granted_scope"`
|
GrantedScope []string `json:"granted_scope"`
|
||||||
GrantedAudience []string `json:"granted_audience,omitempty"`
|
GrantedAudience []string `json:"granted_audience,omitempty"`
|
||||||
Remember bool `json:"remember"`
|
Remember bool `json:"remember"`
|
||||||
AuthenticatedAt *time.Time `json:"authenticated_at,omitempty"`
|
AuthenticatedAt *time.Time `json:"authenticated_at,omitempty"`
|
||||||
RequestedAt *time.Time `json:"requested_at,omitempty"`
|
RequestedAt *time.Time `json:"requested_at,omitempty"`
|
||||||
Client HydraClient `json:"client"`
|
Client HydraClient `json:"client"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHydraAdminService() *HydraAdminService {
|
func NewHydraAdminService() *HydraAdminService {
|
||||||
@@ -151,6 +151,89 @@ func (s *HydraAdminService) PatchClientStatus(ctx context.Context, clientID, sta
|
|||||||
return &updated, nil
|
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) {
|
func (s *HydraAdminService) ListConsentSessions(ctx context.Context, subject, clientID string) ([]HydraConsentSession, error) {
|
||||||
params := map[string]string{
|
params := map[string]string{
|
||||||
"subject": subject,
|
"subject": subject,
|
||||||
|
|||||||
Reference in New Issue
Block a user