1
0
forked from baron/baron-sso

마이페이지 구현

This commit is contained in:
2026-01-27 13:39:49 +09:00
parent 4c608c6c3c
commit 60035aad53
11 changed files with 844 additions and 1 deletions

View File

@@ -385,6 +385,18 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
// --- Helpers ---
func (h *AuthHandler) getBearerToken(c *fiber.Ctx) string {
authHeader := c.Get("Authorization")
if authHeader == "" {
return ""
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
return ""
}
return parts[1]
}
func (h *AuthHandler) getSignupState(key string) (*signupState, error) {
val, err := h.RedisService.Get(key)
if err != nil || val == "" {
@@ -829,3 +841,213 @@ func (h *AuthHandler) HandleDescopeEmailRelay(c *fiber.Ctx) error {
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 {
if strings.HasPrefix(phone, "+8210") {
return "010" + phone[5:]
}
return phone
}
func (h *AuthHandler) formatPhoneForStorage(phone string) string {
phone = strings.ReplaceAll(phone, "-", "")
if strings.HasPrefix(phone, "010") && len(phone) == 11 {
return "+8210" + phone[3:]
}
return phone
}
// GetMe - Returns current user's profile with 010 phone format
func (h *AuthHandler) GetMe(c *fiber.Ctx) error {
token := h.getBearerToken(c)
if token == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Missing authorization token"})
}
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
if err != nil || !authorized {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"})
}
userResponse, 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 user profile"})
}
dept, _ := userResponse.CustomAttributes["department"].(string)
affType, _ := userResponse.CustomAttributes["affiliationType"].(string)
compCode, _ := userResponse.CustomAttributes["companyCode"].(string)
resp := domain.UserProfileResponse{
ID: userResponse.UserID,
Email: userResponse.Email,
Name: userResponse.Name,
Phone: h.formatPhoneForDisplay(userResponse.Phone),
Department: dept,
AffiliationType: affType,
CompanyCode: compCode,
}
return c.JSON(resp)
}
// UpdateMe - Updates current user's profile with phone verification check
func (h *AuthHandler) UpdateMe(c *fiber.Ctx) error {
token := h.getBearerToken(c)
if token == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Missing authorization token"})
}
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
if err != nil || !authorized {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"})
}
var req domain.UpdateUserRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
}
// 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"})
}
newPhoneStorage := h.formatPhoneForStorage(req.Phone)
oldPhoneStorage := currentUser.Phone
slog.Info("[UpdateMe] Checking changes", "userID", userToken.ID, "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:" + userToken.ID + ":" + 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", userToken.ID, "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", userToken.ID, "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", userToken.ID, "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", userToken.ID)
return c.JSON(fiber.Map{
"status": "success",
"updatedAt": time.Now().Format(time.RFC3339),
})
}
// SendUpdateCode - Sends OTP for phone number change
func (h *AuthHandler) SendUpdateCode(c *fiber.Ctx) error {
token := h.getBearerToken(c)
if token == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Unauthorized"})
}
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
if err != nil || !authorized {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"})
}
var req struct {
Phone string `json:"phone"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid phone"})
}
phone := h.formatPhoneForStorage(req.Phone)
code := fmt.Sprintf("%06d", rand.Intn(1000000))
// Store code in Redis
key := "otp_update_phone:" + userToken.ID + ":" + phone
h.RedisService.Set(key, code, 5*time.Minute)
// Send SMS
content := fmt.Sprintf("[Baron SSO] 정보 수정 인증번호: [%s]", code)
go h.SmsService.SendSms(phone, content)
return c.JSON(fiber.Map{"message": "인증번호가 전송되었습니다."})
}
// VerifyUpdateCode - Verifies OTP for phone number change
func (h *AuthHandler) VerifyUpdateCode(c *fiber.Ctx) error {
token := h.getBearerToken(c)
authorized, userToken, err := h.DescopeClient.Auth.ValidateSessionWithToken(c.Context(), token)
if err != nil || !authorized {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"})
}
var req struct {
Phone string `json:"phone"`
Code string `json:"code"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request"})
}
phone := h.formatPhoneForStorage(req.Phone)
key := "otp_update_phone:" + userToken.ID + ":" + phone
storedCode, _ := h.RedisService.Get(key)
if storedCode == "" || storedCode != req.Code {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "인증번호가 일치하지 않거나 만료되었습니다."})
}
// Mark as verified for 10 minutes
verifyKey := "verify_update_phone:" + userToken.ID + ":" + phone
h.RedisService.Set(verifyKey, "verified", 10*time.Minute)
h.RedisService.Delete(key)
return c.JSON(fiber.Map{"success": true})
}