forked from baron/baron-sso
마이페이지 구현
This commit is contained in:
@@ -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})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user