|
|
|
|
@@ -25,8 +25,6 @@ import (
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/descope/go-sdk/descope"
|
|
|
|
|
"github.com/descope/go-sdk/descope/client"
|
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@@ -80,18 +78,16 @@ const (
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type AuthHandler struct {
|
|
|
|
|
ProjectID string
|
|
|
|
|
SmsService domain.SmsService
|
|
|
|
|
EmailService domain.EmailService
|
|
|
|
|
RedisService *service.RedisService
|
|
|
|
|
DescopeClient *client.DescopeClient
|
|
|
|
|
KratosAdmin *service.KratosAdminService
|
|
|
|
|
IdpProvider domain.IdentityProvider
|
|
|
|
|
AuditRepo domain.AuditRepository
|
|
|
|
|
OathkeeperRepo domain.OathkeeperLogRepository
|
|
|
|
|
Hydra *service.HydraAdminService
|
|
|
|
|
TenantService service.TenantService
|
|
|
|
|
KetoService service.KetoService
|
|
|
|
|
KetoService service.KetoService
|
|
|
|
|
UserRepo repository.UserRepository
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -151,34 +147,17 @@ func checkPollInterval(redis *service.RedisService, key string, interval time.Du
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewAuthHandler(redisService *service.RedisService, idpProvider domain.IdentityProvider, auditRepo domain.AuditRepository, oathkeeperRepo domain.OathkeeperLogRepository, tenantService service.TenantService, ketoService service.KetoService, userRepo repository.UserRepository) *AuthHandler {
|
|
|
|
|
projectID := os.Getenv("DESCOPE_PROJECT_ID")
|
|
|
|
|
managementKey := os.Getenv("DESCOPE_MANAGEMENT_KEY")
|
|
|
|
|
|
|
|
|
|
var descopeClient *client.DescopeClient
|
|
|
|
|
var err error
|
|
|
|
|
if projectID != "" {
|
|
|
|
|
descopeClient, err = client.NewWithConfig(&client.Config{
|
|
|
|
|
ProjectID: projectID,
|
|
|
|
|
ManagementKey: managementKey,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
slog.Warn("Failed to initialize Descope Client", "error", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &AuthHandler{
|
|
|
|
|
ProjectID: projectID,
|
|
|
|
|
SmsService: service.NewSmsService(),
|
|
|
|
|
EmailService: service.NewEmailService(),
|
|
|
|
|
RedisService: redisService,
|
|
|
|
|
DescopeClient: descopeClient,
|
|
|
|
|
KratosAdmin: service.NewKratosAdminService(),
|
|
|
|
|
IdpProvider: idpProvider,
|
|
|
|
|
AuditRepo: auditRepo,
|
|
|
|
|
OathkeeperRepo: oathkeeperRepo,
|
|
|
|
|
Hydra: service.NewHydraAdminService(),
|
|
|
|
|
TenantService: tenantService,
|
|
|
|
|
KetoService: ketoService,
|
|
|
|
|
KetoService: ketoService,
|
|
|
|
|
UserRepo: userRepo,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -424,7 +403,7 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
|
|
|
|
|
} else {
|
|
|
|
|
slog.Warn("[Signup] Attempted to join non-active tenant", "email", req.Email, "tenant", tenant.Slug, "status", tenant.Status)
|
|
|
|
|
// Policy: If tenant exists but not active, reject signup or allow as general?
|
|
|
|
|
// For now, let's allow as general but log it.
|
|
|
|
|
// For now, let's allow as general but log it.
|
|
|
|
|
// Or return error if we want strict domain locking.
|
|
|
|
|
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "Your organization's tenant is currently not active."})
|
|
|
|
|
}
|
|
|
|
|
@@ -1528,7 +1507,7 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
|
|
|
|
|
if err := c.BodyParser(&req); err != nil {
|
|
|
|
|
ale.Status = fiber.StatusBadRequest
|
|
|
|
|
ale.LatencyMs = time.Since(startTime)
|
|
|
|
|
ale.DescopeError = err.Error()
|
|
|
|
|
ale.ProviderError = err.Error()
|
|
|
|
|
ale.Log(slog.LevelError, "Body parse error")
|
|
|
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
|
|
|
|
}
|
|
|
|
|
@@ -1543,7 +1522,7 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
|
|
|
|
|
if h.IdpProvider == nil {
|
|
|
|
|
ale.Status = fiber.StatusInternalServerError
|
|
|
|
|
ale.LatencyMs = time.Since(startTime)
|
|
|
|
|
ale.DescopeError = "IDP Provider is nil"
|
|
|
|
|
ale.ProviderError = "IDP Provider is nil"
|
|
|
|
|
ale.Log(slog.LevelError, "IDP Provider is nil")
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Authentication service not configured"})
|
|
|
|
|
}
|
|
|
|
|
@@ -1555,7 +1534,7 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
|
|
|
|
|
}
|
|
|
|
|
ale.Status = fiber.StatusUnauthorized
|
|
|
|
|
ale.LatencyMs = time.Since(startTime)
|
|
|
|
|
ale.DescopeError = err.Error()
|
|
|
|
|
ale.ProviderError = err.Error()
|
|
|
|
|
ale.Log(slog.LevelWarn, "IDP sign-in failed", slog.String("provider", h.IdpProvider.Name()))
|
|
|
|
|
if strings.Contains(err.Error(), "not found") || strings.Contains(err.Error(), "identity") {
|
|
|
|
|
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not registered"})
|
|
|
|
|
@@ -1605,7 +1584,7 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
|
|
|
|
|
return c.JSON(resp)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// InitiatePasswordReset - 사용자가 비밀번호 재설정을 시작하면, loginID 유형에 따라 Descope를 통해 이메일 또는 SMS를 보냅니다.
|
|
|
|
|
// InitiatePasswordReset - 사용자가 비밀번호 재설정을 시작하면, loginID 유형에 따라 이메일 또는 SMS를 보냅니다.
|
|
|
|
|
func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
|
|
|
|
startTime := time.Now()
|
|
|
|
|
ale := logger.NewAuditLogEntry(c, "initiate")
|
|
|
|
|
@@ -1614,7 +1593,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
|
|
|
|
if err := c.BodyParser(&req); err != nil {
|
|
|
|
|
ale.Status = fiber.StatusBadRequest
|
|
|
|
|
ale.LatencyMs = time.Since(startTime)
|
|
|
|
|
ale.DescopeError = err.Error()
|
|
|
|
|
ale.ProviderError = err.Error()
|
|
|
|
|
ale.Log(slog.LevelError, "Body parse error")
|
|
|
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
|
|
|
|
}
|
|
|
|
|
@@ -1626,7 +1605,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
|
|
|
|
if loginID == "" {
|
|
|
|
|
ale.Status = fiber.StatusBadRequest
|
|
|
|
|
ale.LatencyMs = time.Since(startTime)
|
|
|
|
|
ale.DescopeError = "Login ID is required"
|
|
|
|
|
ale.ProviderError = "Login ID is required"
|
|
|
|
|
ale.Log(slog.LevelWarn, "Login ID missing")
|
|
|
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Login ID is required"})
|
|
|
|
|
}
|
|
|
|
|
@@ -1634,7 +1613,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
|
|
|
|
if h.IdpProvider == nil {
|
|
|
|
|
ale.Status = fiber.StatusInternalServerError
|
|
|
|
|
ale.LatencyMs = time.Since(startTime)
|
|
|
|
|
ale.DescopeError = "IDP Provider is not initialized"
|
|
|
|
|
ale.ProviderError = "IDP Provider is not initialized"
|
|
|
|
|
ale.Log(slog.LevelError, "IDP Provider is not initialized")
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Authentication service not configured"})
|
|
|
|
|
}
|
|
|
|
|
@@ -1643,7 +1622,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
|
|
|
|
if userfrontURL == "" {
|
|
|
|
|
ale.Status = fiber.StatusInternalServerError
|
|
|
|
|
ale.LatencyMs = time.Since(startTime)
|
|
|
|
|
ale.DescopeError = "USERFRONT_URL is not set"
|
|
|
|
|
ale.ProviderError = "USERFRONT_URL is not set"
|
|
|
|
|
ale.Log(slog.LevelError, "USERFRONT_URL is not set")
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "USERFRONT_URL environment variable is not set"})
|
|
|
|
|
}
|
|
|
|
|
@@ -1656,7 +1635,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
|
|
|
|
if resetToken == "" {
|
|
|
|
|
ale.Status = fiber.StatusInternalServerError
|
|
|
|
|
ale.LatencyMs = time.Since(startTime)
|
|
|
|
|
ale.DescopeError = "Failed to generate reset token"
|
|
|
|
|
ale.ProviderError = "Failed to generate reset token"
|
|
|
|
|
ale.Log(slog.LevelError, "Failed to generate reset token")
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to generate reset token"})
|
|
|
|
|
}
|
|
|
|
|
@@ -1664,7 +1643,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
|
|
|
|
if err := h.RedisService.Set(prefixPwdResetToken+resetToken, loginID, pwdResetExpiration); err != nil {
|
|
|
|
|
ale.Status = fiber.StatusInternalServerError
|
|
|
|
|
ale.LatencyMs = time.Since(startTime)
|
|
|
|
|
ale.DescopeError = err.Error()
|
|
|
|
|
ale.ProviderError = err.Error()
|
|
|
|
|
ale.Log(slog.LevelError, "Failed to store reset token in Redis")
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to store reset token"})
|
|
|
|
|
}
|
|
|
|
|
@@ -1682,7 +1661,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
|
|
|
|
if !drySend && h.EmailService == nil {
|
|
|
|
|
ale.Status = fiber.StatusInternalServerError
|
|
|
|
|
ale.LatencyMs = time.Since(startTime)
|
|
|
|
|
ale.DescopeError = "Email service not configured"
|
|
|
|
|
ale.ProviderError = "Email service not configured"
|
|
|
|
|
ale.Log(slog.LevelError, "Email service not configured")
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Email service not configured"})
|
|
|
|
|
}
|
|
|
|
|
@@ -1703,7 +1682,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
|
|
|
|
if err := h.EmailService.SendEmail(loginID, subject, body); err != nil {
|
|
|
|
|
ale.Status = fiber.StatusInternalServerError
|
|
|
|
|
ale.LatencyMs = time.Since(startTime)
|
|
|
|
|
ale.DescopeError = err.Error()
|
|
|
|
|
ale.ProviderError = err.Error()
|
|
|
|
|
ale.Log(slog.LevelError, "Failed to send reset email", slog.String("loginId", loginID))
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send reset email"})
|
|
|
|
|
}
|
|
|
|
|
@@ -1716,7 +1695,7 @@ func (h *AuthHandler) InitiatePasswordReset(c *fiber.Ctx) error {
|
|
|
|
|
if err := h.SmsService.SendSms(loginID, resetSms); err != nil {
|
|
|
|
|
ale.Status = fiber.StatusInternalServerError
|
|
|
|
|
ale.LatencyMs = time.Since(startTime)
|
|
|
|
|
ale.DescopeError = err.Error()
|
|
|
|
|
ale.ProviderError = err.Error()
|
|
|
|
|
ale.Log(slog.LevelError, "Failed to send reset SMS", slog.String("loginId", loginID))
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send reset SMS"})
|
|
|
|
|
}
|
|
|
|
|
@@ -1793,7 +1772,7 @@ func (h *AuthHandler) ProcessPasswordResetToken(c *fiber.Ctx) error {
|
|
|
|
|
if token == "" {
|
|
|
|
|
ale.Status = fiber.StatusBadRequest
|
|
|
|
|
ale.LatencyMs = time.Since(startTime)
|
|
|
|
|
ale.DescopeError = "Missing token"
|
|
|
|
|
ale.ProviderError = "Missing token"
|
|
|
|
|
ale.Log(slog.LevelWarn, "Missing token in request")
|
|
|
|
|
return c.Status(fiber.StatusBadRequest).SendString("Missing token")
|
|
|
|
|
}
|
|
|
|
|
@@ -1802,7 +1781,7 @@ func (h *AuthHandler) ProcessPasswordResetToken(c *fiber.Ctx) error {
|
|
|
|
|
if err != nil || loginID == "" {
|
|
|
|
|
ale.Status = fiber.StatusUnauthorized
|
|
|
|
|
ale.LatencyMs = time.Since(startTime)
|
|
|
|
|
ale.DescopeError = "Invalid or expired reset token"
|
|
|
|
|
ale.ProviderError = "Invalid or expired reset token"
|
|
|
|
|
ale.Log(slog.LevelWarn, "Reset token invalid or expired")
|
|
|
|
|
return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired token")
|
|
|
|
|
}
|
|
|
|
|
@@ -1842,7 +1821,7 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error {
|
|
|
|
|
if err := c.BodyParser(&req); err != nil {
|
|
|
|
|
ale.Status = fiber.StatusBadRequest
|
|
|
|
|
ale.LatencyMs = time.Since(startTime)
|
|
|
|
|
ale.DescopeError = err.Error()
|
|
|
|
|
ale.ProviderError = err.Error()
|
|
|
|
|
ale.Log(slog.LevelError, "Body parse error")
|
|
|
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
|
|
|
|
}
|
|
|
|
|
@@ -1875,7 +1854,7 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error {
|
|
|
|
|
if loginID == "" || req.NewPassword == "" {
|
|
|
|
|
ale.Status = fiber.StatusBadRequest
|
|
|
|
|
ale.LatencyMs = time.Since(startTime)
|
|
|
|
|
ale.DescopeError = "Login ID and new password are required"
|
|
|
|
|
ale.ProviderError = "Login ID and new password are required"
|
|
|
|
|
ale.Log(slog.LevelWarn, "Login ID or new password missing")
|
|
|
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Login ID and new password are required"})
|
|
|
|
|
}
|
|
|
|
|
@@ -1887,7 +1866,7 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error {
|
|
|
|
|
if err := validatePasswordWithPolicy(policy, req.NewPassword); err != nil {
|
|
|
|
|
ale.Status = fiber.StatusBadRequest
|
|
|
|
|
ale.LatencyMs = time.Since(startTime)
|
|
|
|
|
ale.DescopeError = err.Error()
|
|
|
|
|
ale.ProviderError = err.Error()
|
|
|
|
|
ale.Log(slog.LevelWarn, "Validation failed: "+err.Error())
|
|
|
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
|
|
|
|
|
}
|
|
|
|
|
@@ -1897,7 +1876,7 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error {
|
|
|
|
|
if h.IdpProvider == nil {
|
|
|
|
|
ale.Status = fiber.StatusInternalServerError
|
|
|
|
|
ale.LatencyMs = time.Since(startTime)
|
|
|
|
|
ale.DescopeError = "IDP Provider is nil"
|
|
|
|
|
ale.ProviderError = "IDP Provider is nil"
|
|
|
|
|
ale.Log(slog.LevelError, "IDP Provider is nil")
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Authentication service not configured"})
|
|
|
|
|
}
|
|
|
|
|
@@ -1905,7 +1884,7 @@ func (h *AuthHandler) CompletePasswordReset(c *fiber.Ctx) error {
|
|
|
|
|
if err := h.IdpProvider.UpdateUserPassword(loginID, req.NewPassword, nil); err != nil {
|
|
|
|
|
ale.Status = fiber.StatusInternalServerError
|
|
|
|
|
ale.LatencyMs = time.Since(startTime)
|
|
|
|
|
ale.DescopeError = err.Error()
|
|
|
|
|
ale.ProviderError = err.Error()
|
|
|
|
|
ale.Log(slog.LevelError, "Failed to update password via IDP")
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to update password"})
|
|
|
|
|
}
|
|
|
|
|
@@ -2042,20 +2021,6 @@ func (h *AuthHandler) ScanQRLogin(c *fiber.Ctx) error {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. 모바일 토큰은 승인 검증용으로만 사용하고, 웹 전용 세션을 새로 발급
|
|
|
|
|
if sessionToken, loginID, approvedSessionID, err := h.tryIssueDescopeQrSession(c, req.Token); err != nil {
|
|
|
|
|
slog.Error("[QR] Issue web session failed", "error", err)
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to issue web session"})
|
|
|
|
|
} else if sessionToken != nil && sessionToken.JWT != "" {
|
|
|
|
|
h.storeQrApproverSessionID(pendingRef, approvedSessionID)
|
|
|
|
|
h.writeQrAuditLog(loginID, pendingRef, sessionToken, approvedSessionID)
|
|
|
|
|
sessionData, _ := json.Marshal(map[string]string{
|
|
|
|
|
"status": statusSuccess,
|
|
|
|
|
"jwt": sessionToken.JWT,
|
|
|
|
|
})
|
|
|
|
|
h.RedisService.Set(prefixSession+pendingRef, string(sessionData), 5*time.Minute)
|
|
|
|
|
return c.JSON(fiber.Map{"message": "QR Login Approved"})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
approvedSessionID := ""
|
|
|
|
|
if req.Token != "" {
|
|
|
|
|
if sessionID, err := h.getKratosSessionID(req.Token); err == nil {
|
|
|
|
|
@@ -2079,11 +2044,6 @@ func (h *AuthHandler) ScanQRLogin(c *fiber.Ctx) error {
|
|
|
|
|
return c.JSON(fiber.Map{"message": "QR Login Approved"})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ProxyToDescope (Placeholder)
|
|
|
|
|
func (h *AuthHandler) ProxyToDescope(c *fiber.Ctx, path string, payload interface{}) error {
|
|
|
|
|
return c.Status(501).SendString("Descope Proxy Disabled")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type kratosCourierRequest struct {
|
|
|
|
|
Recipient string `json:"recipient"`
|
|
|
|
|
TemplateType string `json:"template_type"`
|
|
|
|
|
@@ -2480,82 +2440,6 @@ func sanitizePhoneForSms(phone string) string {
|
|
|
|
|
return sanitized
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HandleDescopeSmsRelay
|
|
|
|
|
func (h *AuthHandler) HandleDescopeSmsRelay(c *fiber.Ctx) error {
|
|
|
|
|
var req struct {
|
|
|
|
|
Recipient string `json:"recipient"`
|
|
|
|
|
Body string `json:"body"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := c.BodyParser(&req); err != nil {
|
|
|
|
|
slog.Error("Webhook Body parsing failed", "error", err)
|
|
|
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if req.Recipient == "" || req.Body == "" {
|
|
|
|
|
slog.Warn("Webhook missing recipient or body")
|
|
|
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Missing recipient or body"})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
slog.Info("Received SMS request", "recipient", req.Recipient)
|
|
|
|
|
|
|
|
|
|
phone := req.Recipient
|
|
|
|
|
if strings.HasPrefix(phone, "+82") {
|
|
|
|
|
phone = "0" + phone[3:]
|
|
|
|
|
}
|
|
|
|
|
phone = strings.ReplaceAll(phone, "-", "")
|
|
|
|
|
phone = strings.ReplaceAll(phone, " ", "")
|
|
|
|
|
|
|
|
|
|
if err := h.SmsService.SendSms(phone, req.Body); err != nil {
|
|
|
|
|
slog.Error("Failed to forward SMS to Naver", "error", err)
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send SMS via Naver"})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
slog.Info("Successfully forwarded SMS", "phone", phone)
|
|
|
|
|
return c.JSON(fiber.Map{"status": "ok"})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HandleDescopeEmailRelay - Webhook for Descope Generic Email Gateway
|
|
|
|
|
// Used for "Fake Email Strategy" to support Polling with SMS.
|
|
|
|
|
func (h *AuthHandler) HandleDescopeEmailRelay(c *fiber.Ctx) error {
|
|
|
|
|
var req struct {
|
|
|
|
|
To string `json:"to"` // e.g., 01012345678@sms.baron
|
|
|
|
|
Subject string `json:"subject"`
|
|
|
|
|
Text string `json:"text"` // Body containing the link
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := c.BodyParser(&req); err != nil {
|
|
|
|
|
slog.Error("[Email Webhook] Body parsing failed", "error", err)
|
|
|
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
slog.Info("[Email Webhook] Received email request", "to", req.To)
|
|
|
|
|
|
|
|
|
|
// Check if it's a Fake Email for SMS
|
|
|
|
|
if strings.HasSuffix(req.To, "@sms.baron") {
|
|
|
|
|
phone := strings.Split(req.To, "@")[0]
|
|
|
|
|
|
|
|
|
|
// Sanitize Phone (Descope might sanitize or not, but let's be safe)
|
|
|
|
|
if strings.HasPrefix(phone, "+82") {
|
|
|
|
|
phone = "0" + phone[3:]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Send SMS with the text body (Descope template should be optimized for SMS)
|
|
|
|
|
if err := h.SmsService.SendSms(phone, req.Text); err != nil {
|
|
|
|
|
slog.Error("[Email Webhook] Failed to forward Email-as-SMS", "error", err)
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send SMS"})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
slog.Info("[Email Webhook] Successfully converted Email to SMS", "phone", phone)
|
|
|
|
|
return c.JSON(fiber.Map{"status": "ok"})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Real Email Handling (Not implemented in this Relay)
|
|
|
|
|
// You would need an SMTP service here if you route ALL emails through this relay.
|
|
|
|
|
slog.Warn("[Email Webhook] Real email skipped (Not implemented)", "to", req.To)
|
|
|
|
|
return c.Status(501).JSON(fiber.Map{"error": "Real email sending not implemented"})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- User Profile Handlers ---
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) formatPhoneForDisplay(phone string) string {
|
|
|
|
|
@@ -2581,6 +2465,7 @@ func (h *AuthHandler) GetMe(c *fiber.Ctx) error {
|
|
|
|
|
}
|
|
|
|
|
return c.JSON(profile)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetEnrichedProfile - Exported wrapper for resolveCurrentProfile used by middlewares
|
|
|
|
|
func (h *AuthHandler) GetEnrichedProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error) {
|
|
|
|
|
return h.resolveCurrentProfile(c)
|
|
|
|
|
@@ -2975,6 +2860,17 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
|
|
|
|
|
if err != nil {
|
|
|
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"})
|
|
|
|
|
}
|
|
|
|
|
currentSessionID := ""
|
|
|
|
|
if token := h.getBearerToken(c); token != "" {
|
|
|
|
|
currentSessionID = extractSessionIDFromJWT(token)
|
|
|
|
|
}
|
|
|
|
|
if currentSessionID == "" {
|
|
|
|
|
if cookie := c.Get("Cookie"); cookie != "" {
|
|
|
|
|
if sessionID, err := h.getKratosSessionIDWithCookie(cookie); err == nil {
|
|
|
|
|
currentSessionID = sessionID
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
subject := ""
|
|
|
|
|
if h.OathkeeperRepo != nil {
|
|
|
|
|
@@ -3056,7 +2952,7 @@ func (h *AuthHandler) GetAuthTimeline(c *fiber.Ctx) error {
|
|
|
|
|
if !isAuthEventType(log.EventType) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if !matchesAuthTimelineUser(log, profile, candidates) {
|
|
|
|
|
if !matchesAuthTimelineUser(log, profile, candidates, currentSessionID) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if shouldSkipAuthTimeline(log) {
|
|
|
|
|
@@ -3467,7 +3363,9 @@ func (h *AuthHandler) AcceptConsentRequest(c *fiber.Ctx) error {
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error {
|
|
|
|
|
var req struct {
|
|
|
|
|
LoginChallenge string `json:"login_challenge"`
|
|
|
|
|
LoginChallenge string `json:"login_challenge"`
|
|
|
|
|
ApprovedSessionID string `json:"approved_session_id,omitempty"`
|
|
|
|
|
SessionID string `json:"session_id,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
if err := c.BodyParser(&req); err != nil {
|
|
|
|
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
|
|
|
|
@@ -3481,13 +3379,31 @@ func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error {
|
|
|
|
|
return fiber.NewError(fiber.StatusUnauthorized, "Authentication required")
|
|
|
|
|
}
|
|
|
|
|
c.Locals("user_id", subject)
|
|
|
|
|
if sessionID, ok := c.Locals("session_id").(string); ok && sessionID != "" {
|
|
|
|
|
c.Locals("approved_session_id", sessionID)
|
|
|
|
|
} else if token := h.getBearerToken(c); token != "" {
|
|
|
|
|
if derivedID := extractSessionIDFromJWT(token); derivedID != "" {
|
|
|
|
|
c.Locals("approved_session_id", derivedID)
|
|
|
|
|
approvedSessionID := strings.TrimSpace(req.ApprovedSessionID)
|
|
|
|
|
if approvedSessionID == "" {
|
|
|
|
|
approvedSessionID = strings.TrimSpace(req.SessionID)
|
|
|
|
|
}
|
|
|
|
|
if approvedSessionID == "" {
|
|
|
|
|
if sessionID, ok := c.Locals("session_id").(string); ok && sessionID != "" {
|
|
|
|
|
approvedSessionID = sessionID
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if approvedSessionID == "" {
|
|
|
|
|
if token := h.getBearerToken(c); token != "" {
|
|
|
|
|
approvedSessionID = extractSessionIDFromJWT(token)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if approvedSessionID == "" {
|
|
|
|
|
if cookie := c.Get("Cookie"); cookie != "" {
|
|
|
|
|
if derivedID, err := h.getKratosSessionIDWithCookie(cookie); err == nil {
|
|
|
|
|
approvedSessionID = derivedID
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if approvedSessionID != "" {
|
|
|
|
|
c.Locals("session_id", approvedSessionID)
|
|
|
|
|
c.Locals("approved_session_id", approvedSessionID)
|
|
|
|
|
}
|
|
|
|
|
if h.KratosAdmin != nil {
|
|
|
|
|
if identity, err := h.KratosAdmin.GetIdentity(c.Context(), subject); err == nil && identity != nil {
|
|
|
|
|
if loginID := pickLoginIDFromTraits(identity.Traits); loginID != "" {
|
|
|
|
|
@@ -3506,65 +3422,12 @@ func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error) {
|
|
|
|
|
// [Development Mode Fallback]
|
|
|
|
|
if os.Getenv("APP_ENV") != "production" {
|
|
|
|
|
// 우선순위: 1. 헤더, 2. 쿠키, 3. 기본값(user)
|
|
|
|
|
testRole := c.Get("X-Test-Role")
|
|
|
|
|
if testRole == "" {
|
|
|
|
|
testRole = c.Cookies("X-Mock-Role")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if testRole == "" {
|
|
|
|
|
testRole = domain.RoleUser // 기본값을 user로 변경하여 차단 확인
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
slog.Info("Using MOCK profile", "role", testRole, "source", "dev_fallback")
|
|
|
|
|
return &domain.UserProfileResponse{
|
|
|
|
|
ID: "dev-admin-uuid",
|
|
|
|
|
Email: "dev-admin@baron.local",
|
|
|
|
|
Name: "Dev Admin (" + testRole + ")",
|
|
|
|
|
Role: testRole,
|
|
|
|
|
CompanyCode: "hanmac",
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var profile *domain.UserProfileResponse
|
|
|
|
|
var err error
|
|
|
|
|
|
|
|
|
|
token := h.getBearerToken(c)
|
|
|
|
|
if token != "" {
|
|
|
|
|
if looksLikeJWT(token) && h.DescopeClient != nil {
|
|
|
|
|
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
|
|
|
|
|
if err == nil && authorized {
|
|
|
|
|
userResponse, err := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID)
|
|
|
|
|
if err == nil {
|
|
|
|
|
identityID, resolveErr := h.resolveKratosIdentityID(
|
|
|
|
|
c.Context(),
|
|
|
|
|
userResponse.Email,
|
|
|
|
|
normalizePhoneForLoginID(userResponse.Phone),
|
|
|
|
|
)
|
|
|
|
|
if resolveErr == nil && identityID != "" {
|
|
|
|
|
dept, _ := userResponse.CustomAttributes["department"].(string)
|
|
|
|
|
affType, _ := userResponse.CustomAttributes["affiliationType"].(string)
|
|
|
|
|
compCode, _ := userResponse.CustomAttributes["companyCode"].(string)
|
|
|
|
|
profile = &domain.UserProfileResponse{
|
|
|
|
|
ID: identityID,
|
|
|
|
|
Email: userResponse.Email,
|
|
|
|
|
Name: userResponse.Name,
|
|
|
|
|
Phone: h.formatPhoneForDisplay(userResponse.Phone),
|
|
|
|
|
Department: dept,
|
|
|
|
|
AffiliationType: affType,
|
|
|
|
|
CompanyCode: compCode,
|
|
|
|
|
Metadata: userResponse.CustomAttributes,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if profile == nil {
|
|
|
|
|
profile, err = h.getKratosProfile(token)
|
|
|
|
|
}
|
|
|
|
|
profile, err = h.getKratosProfile(token)
|
|
|
|
|
} else {
|
|
|
|
|
cookie := c.Get("Cookie")
|
|
|
|
|
if cookie != "" {
|
|
|
|
|
@@ -3604,23 +3467,6 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe
|
|
|
|
|
func (h *AuthHandler) resolveConsentSubject(c *fiber.Ctx) (string, error) {
|
|
|
|
|
token := h.getBearerToken(c)
|
|
|
|
|
if token != "" {
|
|
|
|
|
if looksLikeJWT(token) && h.DescopeClient != nil {
|
|
|
|
|
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
|
|
|
|
|
if err == nil && authorized {
|
|
|
|
|
userResponse, loadErr := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID)
|
|
|
|
|
if loadErr == nil {
|
|
|
|
|
identityID, resolveErr := h.resolveKratosIdentityID(
|
|
|
|
|
c.Context(),
|
|
|
|
|
userResponse.Email,
|
|
|
|
|
normalizePhoneForLoginID(userResponse.Phone),
|
|
|
|
|
)
|
|
|
|
|
if resolveErr == nil {
|
|
|
|
|
return identityID, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return "", fmt.Errorf("failed to resolve kratos identity for consent subject")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
identityID, resolveErr := h.resolveIdentityID(c, token)
|
|
|
|
|
if resolveErr == nil && identityID != "" {
|
|
|
|
|
return identityID, nil
|
|
|
|
|
@@ -3643,26 +3489,6 @@ func (h *AuthHandler) resolveConsentSubject(c *fiber.Ctx) (string, error) {
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) resolveConsentSubjects(c *fiber.Ctx) ([]string, error) {
|
|
|
|
|
token := h.getBearerToken(c)
|
|
|
|
|
if token != "" && looksLikeJWT(token) && h.DescopeClient != nil {
|
|
|
|
|
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
|
|
|
|
|
if err == nil && authorized {
|
|
|
|
|
subjects := make([]string, 0, 2)
|
|
|
|
|
userResponse, loadErr := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID)
|
|
|
|
|
if loadErr == nil {
|
|
|
|
|
subjects = appendLoginIDsFromValues(subjects, userResponse.Email, userResponse.Phone)
|
|
|
|
|
identityID, resolveErr := h.resolveKratosIdentityID(
|
|
|
|
|
c.Context(),
|
|
|
|
|
userResponse.Email,
|
|
|
|
|
normalizePhoneForLoginID(userResponse.Phone),
|
|
|
|
|
)
|
|
|
|
|
if resolveErr == nil && identityID != "" {
|
|
|
|
|
subjects = append([]string{identityID}, subjects...)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return uniqueStrings(subjects), nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if token != "" {
|
|
|
|
|
identityID, traits, err := h.getKratosIdentity(token)
|
|
|
|
|
if err == nil && identityID != "" {
|
|
|
|
|
@@ -4042,7 +3868,7 @@ func normalizeLoginIdentifier(value string) string {
|
|
|
|
|
return normalizePhoneForLoginID(trimmed)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func matchesAuthTimelineUser(log domain.AuditLog, profile *domain.UserProfileResponse, candidates map[string]struct{}) bool {
|
|
|
|
|
func matchesAuthTimelineUser(log domain.AuditLog, profile *domain.UserProfileResponse, candidates map[string]struct{}, sessionID string) bool {
|
|
|
|
|
if profile == nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
@@ -4051,11 +3877,24 @@ func matchesAuthTimelineUser(log domain.AuditLog, profile *domain.UserProfileRes
|
|
|
|
|
}
|
|
|
|
|
loginID := extractLoginIDFromAuditDetails(log.Details)
|
|
|
|
|
normalized := normalizeLoginIdentifier(loginID)
|
|
|
|
|
if normalized == "" {
|
|
|
|
|
if normalized != "" {
|
|
|
|
|
if _, ok := candidates[normalized]; ok {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if sessionID == "" {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
_, ok := candidates[normalized]
|
|
|
|
|
return ok
|
|
|
|
|
if log.SessionID != "" && log.SessionID == sessionID {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
if extracted := extractSessionIDFromAuditDetails(log.Details); extracted != "" && extracted == sessionID {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
if approved := extractApprovedSessionIDFromAuditDetails(log.Details); approved != "" && approved == sessionID {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func extractLoginIDFromAuditDetails(details string) string {
|
|
|
|
|
@@ -4215,52 +4054,36 @@ func extractSessionIDFromAuditDetails(details string) string {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) resolveIdentityID(c *fiber.Ctx, token string) (string, error) {
|
|
|
|
|
if looksLikeJWT(token) && h.DescopeClient != nil {
|
|
|
|
|
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
|
|
|
|
|
if err == nil && authorized {
|
|
|
|
|
if h.KratosAdmin == nil {
|
|
|
|
|
return "", fmt.Errorf("kratos admin unavailable")
|
|
|
|
|
}
|
|
|
|
|
userResponse, loadErr := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID)
|
|
|
|
|
if loadErr != nil {
|
|
|
|
|
return "", loadErr
|
|
|
|
|
}
|
|
|
|
|
identityID, resolveErr := h.resolveKratosIdentityID(
|
|
|
|
|
c.Context(),
|
|
|
|
|
userResponse.Email,
|
|
|
|
|
normalizePhoneForLoginID(userResponse.Phone),
|
|
|
|
|
)
|
|
|
|
|
if resolveErr != nil || identityID == "" {
|
|
|
|
|
return "", fmt.Errorf("failed to resolve kratos identity for token")
|
|
|
|
|
}
|
|
|
|
|
return identityID, nil
|
|
|
|
|
func extractApprovedSessionIDFromAuditDetails(details string) string {
|
|
|
|
|
if details == "" {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
var payload map[string]any
|
|
|
|
|
if err := json.Unmarshal([]byte(details), &payload); err != nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
if raw, ok := payload["approved_session_id"]; ok {
|
|
|
|
|
switch value := raw.(type) {
|
|
|
|
|
case string:
|
|
|
|
|
return value
|
|
|
|
|
default:
|
|
|
|
|
return fmt.Sprint(value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
id, _, err := h.getKratosIdentity(token)
|
|
|
|
|
return id, err
|
|
|
|
|
if raw, ok := payload["approvedSessionId"]; ok {
|
|
|
|
|
switch value := raw.(type) {
|
|
|
|
|
case string:
|
|
|
|
|
return value
|
|
|
|
|
default:
|
|
|
|
|
return fmt.Sprint(value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) tryIssueDescopeQrSession(c *fiber.Ctx, token string) (*domain.Token, string, string, error) {
|
|
|
|
|
if !looksLikeJWT(token) || h.DescopeClient == nil {
|
|
|
|
|
return nil, "", "", nil
|
|
|
|
|
}
|
|
|
|
|
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
|
|
|
|
|
if err != nil || !authorized {
|
|
|
|
|
return nil, "", "", nil
|
|
|
|
|
}
|
|
|
|
|
loginID, err := h.resolveDescopeLoginID(c.Context(), userToken)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, "", "", err
|
|
|
|
|
}
|
|
|
|
|
authInfo, err := h.IdpProvider.IssueSession(loginID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, "", "", err
|
|
|
|
|
}
|
|
|
|
|
if authInfo == nil || authInfo.SessionToken == nil || authInfo.SessionToken.JWT == "" {
|
|
|
|
|
return nil, "", "", fmt.Errorf("descope issue session returned empty token")
|
|
|
|
|
}
|
|
|
|
|
return authInfo.SessionToken, loginID, "", nil
|
|
|
|
|
func (h *AuthHandler) resolveIdentityID(c *fiber.Ctx, token string) (string, error) {
|
|
|
|
|
id, _, err := h.getKratosIdentity(token)
|
|
|
|
|
return id, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) resolveKratosLoginID(token string) (string, error) {
|
|
|
|
|
@@ -4408,38 +4231,6 @@ func (h *AuthHandler) startQrCodeLoginForQr(loginID, pendingRef, rawRef string)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *AuthHandler) resolveDescopeLoginID(ctx context.Context, token *descope.Token) (string, error) {
|
|
|
|
|
if token == nil {
|
|
|
|
|
return "", fmt.Errorf("descope token is nil")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if loginID := extractLoginIDFromClaims(token.Claims); loginID != "" {
|
|
|
|
|
return loginID, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if h.DescopeClient == nil {
|
|
|
|
|
return "", fmt.Errorf("descope client is nil")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
user, err := h.DescopeClient.Management.User().Load(ctx, token.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
if user == nil {
|
|
|
|
|
return "", fmt.Errorf("descope user not found")
|
|
|
|
|
}
|
|
|
|
|
if loginID := pickPrimaryLoginID(user.LoginIDs); loginID != "" {
|
|
|
|
|
return loginID, nil
|
|
|
|
|
}
|
|
|
|
|
if user.Email != "" {
|
|
|
|
|
return user.Email, nil
|
|
|
|
|
}
|
|
|
|
|
if user.Phone != "" {
|
|
|
|
|
return user.Phone, nil
|
|
|
|
|
}
|
|
|
|
|
return "", fmt.Errorf("descope login id not found")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func pickPrimaryLoginID(loginIDs []string) string {
|
|
|
|
|
for _, id := range loginIDs {
|
|
|
|
|
if strings.Contains(id, "@") {
|
|
|
|
|
@@ -4768,93 +4559,6 @@ func (h *AuthHandler) UpdateMe(c *fiber.Ctx) error {
|
|
|
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if token != "" && looksLikeJWT(token) && h.DescopeClient != nil {
|
|
|
|
|
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
|
|
|
|
|
if err == nil && authorized {
|
|
|
|
|
// 1. Load current user to check changes
|
|
|
|
|
currentUser, err := h.DescopeClient.Management.User().Load(c.Context(), userToken.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to load current user"})
|
|
|
|
|
}
|
|
|
|
|
identityID, resolveErr := h.resolveKratosIdentityID(
|
|
|
|
|
c.Context(),
|
|
|
|
|
currentUser.Email,
|
|
|
|
|
normalizePhoneForLoginID(currentUser.Phone),
|
|
|
|
|
)
|
|
|
|
|
if resolveErr != nil || identityID == "" {
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to resolve user identity"})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
newPhoneStorage := h.formatPhoneForStorage(req.Phone)
|
|
|
|
|
oldPhoneStorage := currentUser.Phone
|
|
|
|
|
|
|
|
|
|
slog.Info("[UpdateMe] Checking changes", "userID", identityID, "oldPhone", oldPhoneStorage, "newPhone", newPhoneStorage, "newName", req.Name)
|
|
|
|
|
|
|
|
|
|
// 2. Handle Phone Number Change
|
|
|
|
|
if newPhoneStorage != "" && newPhoneStorage != oldPhoneStorage {
|
|
|
|
|
// Check verification status in Redis
|
|
|
|
|
verifyKey := "verify_update_phone:" + identityID + ":" + newPhoneStorage
|
|
|
|
|
val, _ := h.RedisService.Get(verifyKey)
|
|
|
|
|
if val != "verified" {
|
|
|
|
|
slog.Warn("[UpdateMe] Phone verification missing", "key", verifyKey)
|
|
|
|
|
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "휴대폰 번호 변경을 위해 SMS 인증이 필요합니다."})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update Phone in Descope and mark as verified
|
|
|
|
|
slog.Info("[UpdateMe] Updating phone number", "userID", identityID, "newPhone", newPhoneStorage)
|
|
|
|
|
_, err = h.DescopeClient.Management.User().UpdatePhone(c.Context(), userToken.ID, newPhoneStorage, true, false)
|
|
|
|
|
if err != nil {
|
|
|
|
|
slog.Error("Failed to update phone in Descope", "error", err)
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "전화번호 업데이트에 실패했습니다."})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the old phone was used as a LoginID, replace it with the new one
|
|
|
|
|
for _, loginID := range currentUser.LoginIDs {
|
|
|
|
|
// Normalize for comparison
|
|
|
|
|
normID := strings.ReplaceAll(loginID, "+82", "0")
|
|
|
|
|
normOld := strings.ReplaceAll(oldPhoneStorage, "+82", "0")
|
|
|
|
|
|
|
|
|
|
if loginID == oldPhoneStorage || (normOld != "" && normID == normOld) {
|
|
|
|
|
slog.Info("[UpdateMe] Updating LoginID", "old", loginID, "new", newPhoneStorage)
|
|
|
|
|
_, err = h.DescopeClient.Management.User().UpdateLoginID(c.Context(), loginID, newPhoneStorage)
|
|
|
|
|
if err != nil {
|
|
|
|
|
slog.Warn("Failed to update LoginID", "error", err)
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear verification after successful update
|
|
|
|
|
h.RedisService.Delete(verifyKey)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. Update Name if changed
|
|
|
|
|
if req.Name != "" && req.Name != currentUser.Name {
|
|
|
|
|
slog.Info("[UpdateMe] Updating display name", "userID", identityID, "newName", req.Name)
|
|
|
|
|
_, err = h.DescopeClient.Management.User().UpdateDisplayName(c.Context(), userToken.ID, req.Name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
slog.Error("Failed to update user name", "error", err)
|
|
|
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "이름 업데이트에 실패했습니다."})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. Update Custom Attributes (Department)
|
|
|
|
|
if req.Department != "" {
|
|
|
|
|
slog.Info("[UpdateMe] Updating department", "userID", identityID, "dept", req.Department)
|
|
|
|
|
if _, err := h.DescopeClient.Management.User().UpdateCustomAttribute(c.Context(), userToken.ID, "department", req.Department); err != nil {
|
|
|
|
|
slog.Error("Failed to update department", "error", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
slog.Info("[UpdateMe] Profile update completed successfully", "userID", identityID)
|
|
|
|
|
|
|
|
|
|
return c.JSON(fiber.Map{
|
|
|
|
|
"status": "success",
|
|
|
|
|
"updatedAt": time.Now().Format(time.RFC3339),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
identityID string
|
|
|
|
|
traits map[string]interface{}
|
|
|
|
|
@@ -4928,18 +4632,7 @@ func (h *AuthHandler) ChangeMyPassword(c *fiber.Ctx) error {
|
|
|
|
|
|
|
|
|
|
loginID := ""
|
|
|
|
|
token := h.getBearerToken(c)
|
|
|
|
|
if token != "" && looksLikeJWT(token) && h.DescopeClient != nil {
|
|
|
|
|
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
|
|
|
|
|
if err == nil && authorized {
|
|
|
|
|
resolved, err := h.resolveDescopeLoginID(c.Context(), userToken)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Failed to resolve login ID"})
|
|
|
|
|
}
|
|
|
|
|
loginID = resolved
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if loginID == "" && token != "" {
|
|
|
|
|
if token != "" {
|
|
|
|
|
if resolved, err := h.resolveKratosLoginID(token); err == nil {
|
|
|
|
|
loginID = resolved
|
|
|
|
|
}
|
|
|
|
|
|