diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index 0688d1ab..6632881b 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -33,9 +33,9 @@ const ( ) type AuthHandler struct { - ProjectID string - SmsService domain.SmsService - RedisService *service.RedisService + ProjectID string + SmsService domain.SmsService + RedisService *service.RedisService DescopeClient *client.DescopeClient } @@ -56,7 +56,7 @@ func NewAuthHandler() *AuthHandler { projectID := os.Getenv("DESCOPE_PROJECT_ID") managementKey := os.Getenv("DESCOPE_MANAGEMENT_KEY") - + var descopeClient *client.DescopeClient if projectID != "" { descopeClient, err = client.NewWithConfig(&client.Config{ @@ -69,9 +69,9 @@ func NewAuthHandler() *AuthHandler { } return &AuthHandler{ - ProjectID: projectID, - SmsService: service.NewSmsService(), - RedisService: redisService, + ProjectID: projectID, + SmsService: service.NewSmsService(), + RedisService: redisService, DescopeClient: descopeClient, } } @@ -85,7 +85,7 @@ func (h *AuthHandler) SendSms(c *fiber.Ctx) error { log.Printf("[SMS] Sending code to: %s", req.PhoneNumber) sanitizedPhone := strings.ReplaceAll(req.PhoneNumber, "-", "") - + rand.Seed(time.Now().UnixNano()) code := fmt.Sprintf("%06d", rand.Intn(1000000)) content := fmt.Sprintf("[Baron SSO] 인증번호: %s", code) @@ -107,18 +107,18 @@ func (h *AuthHandler) VerifySms(c *fiber.Ctx) error { sanitizedPhone := strings.ReplaceAll(req.PhoneNumber, "-", "") storedCode, _ := h.RedisService.GetVerificationCode(sanitizedPhone) - + if storedCode == "" || storedCode != req.Code { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid or expired code"}) } h.RedisService.DeleteVerificationCode(sanitizedPhone) - + // Note: In a real scenario, you might want to generate a Descope JWT here too // using the same logic as VerifyMagicLink, but for now returning a placeholder // or you can call the Descope logic if needed. - token := "sms-verified-placeholder-token" - + token := "sms-verified-placeholder-token" + return c.JSON(fiber.Map{"token": token}) } @@ -131,10 +131,10 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error { loginID := strings.ReplaceAll(req.LoginID, "-", "") loginID = strings.ReplaceAll(loginID, " ", "") - + // Generate secure tokens - token := generateSecureToken(4) - pendingRef := generateSecureToken(4) + token := generateSecureToken(3) + pendingRef := generateSecureToken(3) // Store in Redis h.RedisService.Set(prefixSession+pendingRef, fmt.Sprintf(`{"status":"%s"}`, statusPending), defaultExpiration) @@ -143,10 +143,10 @@ func (h *AuthHandler) InitEnchantedLink(c *fiber.Ctx) error { // Send SMS // Frontend URL should be dynamic or env based, but restoring hardcoded/env logic // The frontend uses ssologin.hmac.kr - frontendURL := "http://ssologin.hmac.kr" - link := fmt.Sprintf("%s/?t=%s", frontendURL, token) + frontendURL := "http://ssologin.hmac.kr" + link := fmt.Sprintf("%s/verify/%s", frontendURL, token) content := fmt.Sprintf("[Baron SSO] 로그인 링크: %s", link) - + log.Printf("[Enchanted] Sending link to %s", loginID) if err := h.SmsService.SendSms(loginID, content); err != nil { @@ -216,11 +216,11 @@ func (h *AuthHandler) VerifyMagicLink(c *fiber.Ctx) error { // If user does not exist, create it and retry if strings.Contains(err.Error(), "User not found") || strings.Contains(err.Error(), "E062108") { log.Printf("User %s not found. Creating new user...", loginID) - + // Format LoginID for Descope (E.164 for phones) descopeLoginID := loginID userObj := &descope.UserRequest{} - + if strings.Contains(loginID, "@") { userObj.Email = loginID } else { @@ -237,7 +237,7 @@ func (h *AuthHandler) VerifyMagicLink(c *fiber.Ctx) error { log.Printf("Failed to create user: %v", errCreate) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create new user"}) } - + // Retry generating embedded token with the Descope LoginID embeddedToken, err = h.DescopeClient.Management.User().GenerateEmbeddedLink(context.Background(), descopeLoginID, nil, 0) if err != nil { @@ -264,7 +264,7 @@ func (h *AuthHandler) VerifyMagicLink(c *fiber.Ctx) error { "jwt": sessionToken, }) h.RedisService.Set(prefixSession+pendingRef, string(sessionData), defaultExpiration) - + return c.JSON(fiber.Map{ "token": sessionToken, "message": "Login successful", @@ -282,7 +282,7 @@ func (h *AuthHandler) HandleDescopeSmsRelay(c *fiber.Ctx) error { Recipient string `json:"recipient"` Body string `json:"body"` } - + if err := c.BodyParser(&req); err != nil { log.Printf("[Webhook] Body parsing failed: %v", err) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) @@ -319,7 +319,7 @@ func (h *AuthHandler) HandleDescopeEmailRelay(c *fiber.Ctx) error { Subject string `json:"subject"` Text string `json:"text"` // Body containing the link } - + if err := c.BodyParser(&req); err != nil { log.Printf("[Email Webhook] Body parsing failed: %v", err) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"}) @@ -330,18 +330,18 @@ func (h *AuthHandler) HandleDescopeEmailRelay(c *fiber.Ctx) error { // 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 { log.Printf("[Email Webhook] Failed to forward Email-as-SMS: %v", err) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send SMS"}) } - + log.Printf("[Email Webhook] Successfully converted Email to SMS for %s", phone) return c.JSON(fiber.Map{"status": "ok"}) } @@ -350,4 +350,4 @@ func (h *AuthHandler) HandleDescopeEmailRelay(c *fiber.Ctx) error { // You would need an SMTP service here if you route ALL emails through this relay. log.Printf("[Email Webhook] Real email skipped (Not implemented): %s", req.To) return c.Status(501).JSON(fiber.Map{"error": "Real email sending not implemented"}) -} \ No newline at end of file +} diff --git a/frontend/lib/features/auth/presentation/login_screen.dart b/frontend/lib/features/auth/presentation/login_screen.dart index fdc7ac50..bf5a7e3c 100644 --- a/frontend/lib/features/auth/presentation/login_screen.dart +++ b/frontend/lib/features/auth/presentation/login_screen.dart @@ -9,7 +9,8 @@ import '../../../core/services/web_auth_integration.dart'; import '../../../core/services/auth_proxy_service.dart'; class LoginScreen extends ConsumerStatefulWidget { - const LoginScreen({super.key}); + final String? verificationToken; + const LoginScreen({super.key, this.verificationToken}); @override ConsumerState createState() => _LoginScreenState(); @@ -30,12 +31,18 @@ class _LoginScreenState extends ConsumerState super.initState(); _tabController = TabController(length: 2, vsync: this); - // Check for 't' token in URL (Magic Link / Enchanted Link verification) + // Check for tokens (Path Parameter or Legacy Query Parameter) WidgetsBinding.instance.addPostFrameCallback((_) { - final uri = Uri.base; - if (uri.queryParameters.containsKey('t')) { - _verifyToken(uri.queryParameters['t']!); + if (widget.verificationToken != null) { + _verifyToken(widget.verificationToken!); + } else { + final uri = Uri.base; + if (uri.queryParameters.containsKey('t')) { + _verifyToken(uri.queryParameters['t']!); + } } + + final uri = Uri.base; if (uri.queryParameters.containsKey('redirect_url')) { _redirectUrl = uri.queryParameters['redirect_url']; } diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 652fb26b..9f9b080b 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -4,11 +4,13 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:descope/descope.dart'; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:flutter_web_plugins/url_strategy.dart'; import 'features/auth/presentation/login_screen.dart'; import 'features/dashboard/presentation/dashboard_screen.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + usePathUrlStrategy(); // Load Env (Handling error if missing for now) try { @@ -36,6 +38,13 @@ final _router = GoRouter( initialLocation: '/', routes: [ GoRoute(path: '/', builder: (context, state) => const LoginScreen()), + GoRoute( + path: '/verify/:token', + builder: (context, state) { + final token = state.pathParameters['token']; + return LoginScreen(verificationToken: token); + }, + ), GoRoute( path: '/dashboard', builder: (context, state) => const DashboardScreen(), @@ -44,10 +53,11 @@ final _router = GoRouter( redirect: (context, state) { final isLoggedIn = Descope.sessionManager.session?.refreshToken.isExpired == false; - final isLoggingIn = state.uri.toString() == '/'; + final path = state.uri.path; + final isLoggingIn = path == '/' || path.startsWith('/verify/'); if (!isLoggedIn && !isLoggingIn) return '/'; - if (isLoggedIn && isLoggingIn) return '/dashboard'; + if (isLoggedIn && path == '/') return '/dashboard'; return null; },