forked from baron/baron-sso
내정보 페이지 사용성개선, adminFront user 정보 연동.
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"baron-sso-backend/internal/logger"
|
||||
"baron-sso-backend/internal/service"
|
||||
"baron-sso-backend/internal/utils"
|
||||
"bytes"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
@@ -18,7 +19,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -75,6 +76,7 @@ type AuthHandler struct {
|
||||
DescopeClient *client.DescopeClient
|
||||
IdpProvider domain.IdentityProvider
|
||||
AuditRepo domain.AuditRepository
|
||||
Hydra *service.HydraAdminService
|
||||
}
|
||||
|
||||
type signupState struct {
|
||||
@@ -156,6 +158,7 @@ func NewAuthHandler(redisService *service.RedisService, idpProvider domain.Ident
|
||||
DescopeClient: descopeClient,
|
||||
IdpProvider: idpProvider,
|
||||
AuditRepo: auditRepo,
|
||||
Hydra: service.NewHydraAdminService(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,49 +500,7 @@ func (h *AuthHandler) resolvePasswordPolicy() *domain.PasswordPolicy {
|
||||
|
||||
// validatePasswordWithPolicy는 정책 기준으로 비밀번호를 검증합니다.
|
||||
func validatePasswordWithPolicy(policy *domain.PasswordPolicy, password string) error {
|
||||
if policy == nil {
|
||||
return nil
|
||||
}
|
||||
if policy.MinLength > 0 && len(password) < policy.MinLength {
|
||||
return fmt.Errorf("비밀번호는 최소 %d자 이상이어야 합니다", policy.MinLength)
|
||||
}
|
||||
|
||||
types := 0
|
||||
hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
|
||||
hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
|
||||
hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password)
|
||||
hasSymbol := regexp.MustCompile(`[\W_]`).MatchString(password)
|
||||
if hasLower {
|
||||
types++
|
||||
}
|
||||
if hasUpper {
|
||||
types++
|
||||
}
|
||||
if hasNumber {
|
||||
types++
|
||||
}
|
||||
if hasSymbol {
|
||||
types++
|
||||
}
|
||||
|
||||
if policy.MinCharacterTypes > 0 && types < policy.MinCharacterTypes {
|
||||
return fmt.Errorf("비밀번호는 영문 대/소문자/숫자/특수문자 중 %d가지 이상을 포함해야 합니다", policy.MinCharacterTypes)
|
||||
}
|
||||
|
||||
if policy.Lowercase && !hasLower {
|
||||
return fmt.Errorf("비밀번호에 소문자가 포함되어야 합니다")
|
||||
}
|
||||
if policy.Uppercase && !hasUpper {
|
||||
return fmt.Errorf("비밀번호에 대문자가 포함되어야 합니다")
|
||||
}
|
||||
if policy.Number && !hasNumber {
|
||||
return fmt.Errorf("비밀번호에 숫자가 포함되어야 합니다")
|
||||
}
|
||||
if policy.NonAlphanumeric && !hasSymbol {
|
||||
return fmt.Errorf("비밀번호에 특수문자가 포함되어야 합니다")
|
||||
}
|
||||
|
||||
return nil
|
||||
return utils.ValidatePasswordWithPolicy(policy, password)
|
||||
}
|
||||
|
||||
// GetPasswordPolicy는 IDP 기준 비밀번호 정책을 제공합니다.
|
||||
@@ -2345,6 +2306,109 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
type linkedRpSummary struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Logo string `json:"logo,omitempty"`
|
||||
LastAuthenticatedAt string `json:"lastAuthenticatedAt,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
}
|
||||
|
||||
type linkedRpListResponse struct {
|
||||
Items []linkedRpSummary `json:"items"`
|
||||
}
|
||||
|
||||
type linkedRpRecord struct {
|
||||
linkedRpSummary
|
||||
lastAuth time.Time
|
||||
}
|
||||
|
||||
func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error {
|
||||
if h.Hydra == nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "hydra admin unavailable"})
|
||||
}
|
||||
|
||||
subject, err := h.resolveConsentSubject(c)
|
||||
if err != nil || subject == "" {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"})
|
||||
}
|
||||
|
||||
sessions, err := h.Hydra.ListConsentSessions(c.Context(), subject, "")
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
records := make(map[string]*linkedRpRecord)
|
||||
for _, session := range sessions {
|
||||
clientID := strings.TrimSpace(session.Client.ClientID)
|
||||
if clientID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(session.Client.ClientName)
|
||||
if name == "" {
|
||||
name = clientID
|
||||
}
|
||||
|
||||
lastAuth := time.Time{}
|
||||
if session.AuthenticatedAt != nil {
|
||||
lastAuth = *session.AuthenticatedAt
|
||||
} else if session.RequestedAt != nil {
|
||||
lastAuth = *session.RequestedAt
|
||||
}
|
||||
|
||||
scopes := session.GrantedScope
|
||||
if len(scopes) == 0 && strings.TrimSpace(session.Client.Scope) != "" {
|
||||
scopes = strings.Fields(session.Client.Scope)
|
||||
}
|
||||
|
||||
existing := records[clientID]
|
||||
if existing == nil {
|
||||
records[clientID] = &linkedRpRecord{
|
||||
linkedRpSummary: linkedRpSummary{
|
||||
ID: clientID,
|
||||
Name: name,
|
||||
Logo: extractHydraClientLogo(session.Client.Metadata),
|
||||
Status: hydraClientStatus(session.Client.Metadata),
|
||||
Scopes: scopes,
|
||||
},
|
||||
lastAuth: lastAuth,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if existing.Name == "" {
|
||||
existing.Name = name
|
||||
}
|
||||
if existing.Logo == "" {
|
||||
existing.Logo = extractHydraClientLogo(session.Client.Metadata)
|
||||
}
|
||||
existing.Scopes = mergeScopes(existing.Scopes, scopes)
|
||||
if lastAuth.After(existing.lastAuth) {
|
||||
existing.lastAuth = lastAuth
|
||||
}
|
||||
}
|
||||
|
||||
ordered := make([]*linkedRpRecord, 0, len(records))
|
||||
for _, record := range records {
|
||||
ordered = append(ordered, record)
|
||||
}
|
||||
sort.Slice(ordered, func(i, j int) bool {
|
||||
return ordered[i].lastAuth.After(ordered[j].lastAuth)
|
||||
})
|
||||
|
||||
items := make([]linkedRpSummary, 0, len(ordered))
|
||||
for _, record := range ordered {
|
||||
if !record.lastAuth.IsZero() {
|
||||
record.LastAuthenticatedAt = record.lastAuth.Format(time.RFC3339)
|
||||
}
|
||||
items = append(items, record.linkedRpSummary)
|
||||
}
|
||||
|
||||
return c.JSON(linkedRpListResponse{Items: items})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error) {
|
||||
token := h.getBearerToken(c)
|
||||
if token != "" {
|
||||
@@ -2384,6 +2448,19 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe
|
||||
return h.getKratosProfileWithCookie(cookie)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) resolveConsentSubject(c *fiber.Ctx) (string, error) {
|
||||
token := h.getBearerToken(c)
|
||||
if token != "" {
|
||||
return h.resolveIdentityID(c, token)
|
||||
}
|
||||
cookie := c.Get("Cookie")
|
||||
if cookie == "" {
|
||||
return "", fmt.Errorf("missing authorization token")
|
||||
}
|
||||
identityID, _, err := h.getKratosIdentityWithCookie(cookie)
|
||||
return identityID, err
|
||||
}
|
||||
|
||||
func isAuthEventType(eventType string) bool {
|
||||
normalized := strings.ToLower(eventType)
|
||||
return strings.Contains(normalized, " /api/v1/auth/")
|
||||
@@ -3345,3 +3422,68 @@ func (h *AuthHandler) VerifyUpdateCode(c *fiber.Ctx) error {
|
||||
|
||||
return c.JSON(fiber.Map{"success": true})
|
||||
}
|
||||
|
||||
func hydraClientStatus(metadata map[string]interface{}) string {
|
||||
if metadata == nil {
|
||||
return "active"
|
||||
}
|
||||
if value, ok := metadata["status"].(string); ok {
|
||||
normalized := strings.ToLower(strings.TrimSpace(value))
|
||||
if normalized != "" {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
return "active"
|
||||
}
|
||||
|
||||
func extractHydraClientLogo(metadata map[string]interface{}) string {
|
||||
if metadata == nil {
|
||||
return ""
|
||||
}
|
||||
candidates := []string{
|
||||
"logo",
|
||||
"logo_url",
|
||||
"logoUrl",
|
||||
"logo_uri",
|
||||
"logoUri",
|
||||
"app_logo",
|
||||
"appLogo",
|
||||
}
|
||||
for _, key := range candidates {
|
||||
if value, ok := metadata[key]; ok {
|
||||
if logo, ok := value.(string); ok {
|
||||
logo = strings.TrimSpace(logo)
|
||||
if logo != "" {
|
||||
return logo
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func mergeScopes(current []string, next []string) []string {
|
||||
if len(next) == 0 {
|
||||
return current
|
||||
}
|
||||
seen := make(map[string]struct{}, len(current)+len(next))
|
||||
for _, scope := range current {
|
||||
scope = strings.TrimSpace(scope)
|
||||
if scope == "" {
|
||||
continue
|
||||
}
|
||||
seen[scope] = struct{}{}
|
||||
}
|
||||
for _, scope := range next {
|
||||
scope = strings.TrimSpace(scope)
|
||||
if scope == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[scope]; ok {
|
||||
continue
|
||||
}
|
||||
seen[scope] = struct{}{}
|
||||
current = append(current, scope)
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user