forked from baron/baron-sso
Merge branch 'dev/qr'
This commit is contained in:
10
.env.sample
10
.env.sample
@@ -3,7 +3,7 @@
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
# --- General System ---
|
# --- General System ---
|
||||||
APP_ENV=dev
|
APP_ENV=dev # 애플리케이션 실행 환경 (deve, production)
|
||||||
TZ=Asia/Seoul
|
TZ=Asia/Seoul
|
||||||
|
|
||||||
# --- Infrastructure Ports ---
|
# --- Infrastructure Ports ---
|
||||||
@@ -23,7 +23,6 @@ DB_NAME=baron_sso
|
|||||||
COOKIE_SECRET=super-secret-key-must-be-32-bytes!
|
COOKIE_SECRET=super-secret-key-must-be-32-bytes!
|
||||||
REDIS_ADDR=redis:6379
|
REDIS_ADDR=redis:6379
|
||||||
|
|
||||||
# --- Frontend Configuration ---
|
|
||||||
# Descope Project ID (Required for Auth)
|
# Descope Project ID (Required for Auth)
|
||||||
DESCOPE_PROJECT_ID=P2t...your_descope_project_id
|
DESCOPE_PROJECT_ID=P2t...your_descope_project_id
|
||||||
DESCOPE_MANAGEMENT_KEY=your_descope_management_key_here
|
DESCOPE_MANAGEMENT_KEY=your_descope_management_key_here
|
||||||
@@ -34,10 +33,15 @@ NAVER_CLOUD_SECRET_KEY=ncp_iam_...
|
|||||||
NAVER_CLOUD_SERVICE_ID=ncp:sms:kr:...:...
|
NAVER_CLOUD_SERVICE_ID=ncp:sms:kr:...:...
|
||||||
NAVER_SENDER_PHONE_NUMBER=...
|
NAVER_SENDER_PHONE_NUMBER=...
|
||||||
|
|
||||||
# --- AWS SES Configuration ---
|
# --- AWS SES (이메일 발송용) ---
|
||||||
AWS_REGION=ap-northeast-2
|
AWS_REGION=ap-northeast-2
|
||||||
AWS_ACCESS_KEY_ID=...
|
AWS_ACCESS_KEY_ID=...
|
||||||
AWS_SECRET_ACCESS_KEY=...
|
AWS_SECRET_ACCESS_KEY=...
|
||||||
AWS_SES_SENDER=no-reply@baron.co.kr
|
AWS_SES_SENDER=no-reply@baron.co.kr
|
||||||
|
|
||||||
|
# --- 관리자 page pw ---
|
||||||
ADMIN_PASSWORD=admin
|
ADMIN_PASSWORD=admin
|
||||||
|
|
||||||
|
# --- URLs for Proxy/Handoff ---
|
||||||
|
FRONTEND_URL=https://ssologin.hmac.kr # 프론트엔드 접속 주소 (이메일/SMS 링크 생성 시 사용)
|
||||||
|
BACKEND_URL=https://ssologin.hmac.kr # 프론트엔드에서 참조할 백엔드 API 주소
|
||||||
@@ -3,15 +3,25 @@ package main
|
|||||||
import (
|
import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strings"
|
||||||
"time"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bwmarrin/snowflake"
|
||||||
|
|
||||||
|
"baron-sso-backend/internal/handler"
|
||||||
|
|
||||||
|
"baron-sso-backend/internal/logger"
|
||||||
|
|
||||||
|
"baron-sso-backend/internal/repository"
|
||||||
|
|
||||||
|
"baron-sso-backend/internal/service"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
"github.com/bwmarrin/snowflake"
|
|
||||||
"baron-sso-backend/internal/handler"
|
|
||||||
"baron-sso-backend/internal/logger"
|
|
||||||
"baron-sso-backend/internal/repository"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
"github.com/gofiber/fiber/v2/middleware/encryptcookie"
|
"github.com/gofiber/fiber/v2/middleware/encryptcookie"
|
||||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||||
@@ -58,9 +68,14 @@ func main() {
|
|||||||
slog.Warn("Failed to connect to ClickHouse. Audit logs will fail.", "error", err)
|
slog.Warn("Failed to connect to ClickHouse. Audit logs will fail.", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
redisService, err := service.NewRedisService()
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("Failed to connect to Redis. Auth features may fail.", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Initialize Handlers
|
// 2. Initialize Handlers
|
||||||
auditHandler := handler.NewAuditHandler(auditRepo)
|
auditHandler := handler.NewAuditHandler(auditRepo)
|
||||||
authHandler := handler.NewAuthHandler()
|
authHandler := handler.NewAuthHandler(redisService)
|
||||||
adminHandler := handler.NewAdminHandler()
|
adminHandler := handler.NewAdminHandler()
|
||||||
|
|
||||||
// 3. Initialize Fiber
|
// 3. Initialize Fiber
|
||||||
@@ -85,6 +100,13 @@ func main() {
|
|||||||
|
|
||||||
// Log after request
|
// Log after request
|
||||||
latency := time.Since(start)
|
latency := time.Since(start)
|
||||||
|
status := c.Response().StatusCode()
|
||||||
|
path := c.Path()
|
||||||
|
|
||||||
|
// Skip logging for all successful requests (status < 400)
|
||||||
|
if status < 400 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
msg := "http_request"
|
msg := "http_request"
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -92,9 +114,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
slog.Info(msg,
|
slog.Info(msg,
|
||||||
"status", c.Response().StatusCode(),
|
"status", status,
|
||||||
"method", c.Method(),
|
"method", c.Method(),
|
||||||
"path", c.Path(),
|
"path", path,
|
||||||
"latency", latency.String(),
|
"latency", latency.String(),
|
||||||
"ip", c.IP(),
|
"ip", c.IP(),
|
||||||
"req_id", c.GetRespHeader(fiber.HeaderXRequestID),
|
"req_id", c.GetRespHeader(fiber.HeaderXRequestID),
|
||||||
@@ -118,7 +140,46 @@ func main() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
app.Get("/health", func(c *fiber.Ctx) error {
|
app.Get("/health", func(c *fiber.Ctx) error {
|
||||||
return c.JSON(fiber.Map{"status": "ok"})
|
status := "ok"
|
||||||
|
checks := make(map[string]string)
|
||||||
|
|
||||||
|
// Check ClickHouse
|
||||||
|
if auditRepo != nil {
|
||||||
|
if err := auditRepo.Ping(c.Context()); err != nil {
|
||||||
|
checks["clickhouse"] = "error: " + err.Error()
|
||||||
|
status = "error"
|
||||||
|
} else {
|
||||||
|
checks["clickhouse"] = "ok"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
checks["clickhouse"] = "not_initialized"
|
||||||
|
status = "degraded"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Redis
|
||||||
|
if redisService != nil {
|
||||||
|
if err := redisService.Ping(c.Context()); err != nil {
|
||||||
|
checks["redis"] = "error: " + err.Error()
|
||||||
|
status = "error"
|
||||||
|
} else {
|
||||||
|
checks["redis"] = "ok"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
checks["redis"] = "not_initialized"
|
||||||
|
status = "degraded"
|
||||||
|
}
|
||||||
|
|
||||||
|
if status == "error" {
|
||||||
|
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
|
||||||
|
"status": status,
|
||||||
|
"checks": checks,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"status": status,
|
||||||
|
"checks": checks,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// API Group
|
// API Group
|
||||||
@@ -188,6 +249,16 @@ func main() {
|
|||||||
level = slog.LevelInfo
|
level = slog.LevelInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter out noisy client navigation logs
|
||||||
|
if level == slog.LevelInfo {
|
||||||
|
msg := strings.ToLower(req.Message)
|
||||||
|
if strings.Contains(msg, "navigating to") ||
|
||||||
|
strings.Contains(msg, "going to") ||
|
||||||
|
strings.Contains(msg, "redirecting to") {
|
||||||
|
return c.SendStatus(fiber.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
slog.Log(c.Context(), level, req.Message, attrs...)
|
slog.Log(c.Context(), level, req.Message, attrs...)
|
||||||
return c.SendStatus(fiber.StatusOK)
|
return c.SendStatus(fiber.StatusOK)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -49,16 +49,12 @@ func GenerateSecureToken(length int) string {
|
|||||||
return hex.EncodeToString(b)
|
return hex.EncodeToString(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthHandler() *AuthHandler {
|
func NewAuthHandler(redisService *service.RedisService) *AuthHandler {
|
||||||
redisService, err := service.NewRedisService()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to connect to Redis: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
projectID := os.Getenv("DESCOPE_PROJECT_ID")
|
projectID := os.Getenv("DESCOPE_PROJECT_ID")
|
||||||
managementKey := os.Getenv("DESCOPE_MANAGEMENT_KEY")
|
managementKey := os.Getenv("DESCOPE_MANAGEMENT_KEY")
|
||||||
|
|
||||||
var descopeClient *client.DescopeClient
|
var descopeClient *client.DescopeClient
|
||||||
|
var err error
|
||||||
if projectID != "" {
|
if projectID != "" {
|
||||||
descopeClient, err = client.NewWithConfig(&client.Config{
|
descopeClient, err = client.NewWithConfig(&client.Config{
|
||||||
ProjectID: projectID,
|
ProjectID: projectID,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func NewClickHouseRepository(host string, port int, user, password, db string) (
|
|||||||
Username: user,
|
Username: user,
|
||||||
Password: password,
|
Password: password,
|
||||||
},
|
},
|
||||||
Debug: true,
|
Debug: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -79,3 +79,10 @@ func (r *ClickHouseRepository) Create(log *domain.AuditLog) error {
|
|||||||
log.Details,
|
log.Details,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ClickHouseRepository) Ping(ctx context.Context) error {
|
||||||
|
if r.conn == nil {
|
||||||
|
return fmt.Errorf("clickhouse connection is nil")
|
||||||
|
}
|
||||||
|
return r.conn.Ping(ctx)
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ func NewRedisService() (*RedisService, error) {
|
|||||||
return &RedisService{Client: rdb}, nil
|
return &RedisService{Client: rdb}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *RedisService) Ping(ctx context.Context) error {
|
||||||
|
if s.Client == nil {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
return s.Client.Ping(ctx).Err()
|
||||||
|
}
|
||||||
|
|
||||||
// StoreVerificationCode saves the SMS verification code with a 3-minute expiration
|
// StoreVerificationCode saves the SMS verification code with a 3-minute expiration
|
||||||
func (s *RedisService) StoreVerificationCode(phone, code string) error {
|
func (s *RedisService) StoreVerificationCode(phone, code string) error {
|
||||||
// Key format: "sms_verify:01012345678"
|
// Key format: "sms_verify:01012345678"
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
@@ -35,6 +33,13 @@ services:
|
|||||||
- ./backend:/app
|
- ./backend:/app
|
||||||
command: ["go", "run", "./cmd/server/main.go"]
|
command: ["go", "run", "./cmd/server/main.go"]
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
@@ -52,7 +57,8 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- baron_net
|
- baron_net
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
backend:
|
||||||
|
condition: service_healthy
|
||||||
command: >
|
command: >
|
||||||
/bin/sh -c "mkdir -p /usr/share/nginx/html/assets &&
|
/bin/sh -c "mkdir -p /usr/share/nginx/html/assets &&
|
||||||
echo \"DESCOPE_PROJECT_ID=$${DESCOPE_PROJECT_ID}\" > /usr/share/nginx/html/assets/.env &&
|
echo \"DESCOPE_PROJECT_ID=$${DESCOPE_PROJECT_ID}\" > /usr/share/nginx/html/assets/.env &&
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ services:
|
|||||||
- "${BACKEND_PORT:-3000}:3000"
|
- "${BACKEND_PORT:-3000}:3000"
|
||||||
depends_on:
|
depends_on:
|
||||||
- infra_check
|
- infra_check
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
networks:
|
networks:
|
||||||
- baron_net
|
- baron_net
|
||||||
|
|
||||||
@@ -30,7 +36,8 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "${FRONTEND_PORT:-80}:80"
|
- "${FRONTEND_PORT:-80}:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
backend:
|
||||||
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
- baron_net
|
- baron_net
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class _CreateUserScreenState extends State<CreateUserScreen> {
|
|||||||
|
|
||||||
// If cancelled or empty
|
// If cancelled or empty
|
||||||
if (inputPassword == null || inputPassword.isEmpty) {
|
if (inputPassword == null || inputPassword.isEmpty) {
|
||||||
if (mounted) context.go('/dashboard'); // Kick out
|
if (mounted) context.go('/'); // Kick out
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ class _CreateUserScreenState extends State<CreateUserScreen> {
|
|||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Invalid Password. Access Denied.'), backgroundColor: Colors.red),
|
const SnackBar(content: Text('Invalid Password. Access Denied.'), backgroundColor: Colors.red),
|
||||||
);
|
);
|
||||||
context.go('/dashboard'); // Kick out
|
context.go('/'); // Kick out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,7 +152,7 @@ class _CreateUserScreenState extends State<CreateUserScreen> {
|
|||||||
title: const Text('Create User'),
|
title: const Text('Create User'),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: () => context.go('/dashboard'),
|
onPressed: () => context.go('/'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Center(
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class _UserManagementScreenState extends State<UserManagementScreen> with Single
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (inputPassword == null || inputPassword.isEmpty) {
|
if (inputPassword == null || inputPassword.isEmpty) {
|
||||||
if (mounted) context.go('/dashboard');
|
if (mounted) context.go('/');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ class _UserManagementScreenState extends State<UserManagementScreen> with Single
|
|||||||
} else {
|
} else {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Invalid Password'), backgroundColor: Colors.red));
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Invalid Password'), backgroundColor: Colors.red));
|
||||||
context.go('/dashboard');
|
context.go('/');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -277,7 +277,7 @@ class _UserManagementScreenState extends State<UserManagementScreen> with Single
|
|||||||
title: const Text('User Management'),
|
title: const Text('User Management'),
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
onPressed: () => context.go('/dashboard'),
|
onPressed: () => context.go('/'),
|
||||||
),
|
),
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class _ApproveQrScreenState extends State<ApproveQrScreen> {
|
|||||||
final session = Descope.sessionManager.session;
|
final session = Descope.sessionManager.session;
|
||||||
if (session == null || session.refreshToken.isExpired) {
|
if (session == null || session.refreshToken.isExpired) {
|
||||||
setState(() => _message = "Please log in on your phone first.");
|
setState(() => _message = "Please log in on your phone first.");
|
||||||
context.go('/'); // Redirect to login
|
context.go('/login'); // Redirect to login
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ class _ApproveQrScreenState extends State<ApproveQrScreen> {
|
|||||||
|
|
||||||
// Automatically go to dashboard after a short delay
|
// Automatically go to dashboard after a short delay
|
||||||
Future.delayed(const Duration(seconds: 1), () {
|
Future.delayed(const Duration(seconds: 1), () {
|
||||||
if (mounted) context.go('/dashboard');
|
if (mounted) context.go('/');
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState(() => _message = "Error: $e");
|
setState(() => _message = "Error: $e");
|
||||||
@@ -103,14 +103,14 @@ class _ApproveQrScreenState extends State<ApproveQrScreen> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 16),
|
padding: const EdgeInsets.only(top: 16),
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () => context.go('/'),
|
onPressed: () => context.go('/login'),
|
||||||
child: const Text("Login on this device first"),
|
child: const Text("Login on this device first"),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
if (_success)
|
if (_success)
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () => context.go('/dashboard'),
|
onPressed: () => context.go('/'),
|
||||||
child: const Text("Go to My Dashboard"),
|
child: const Text("Go to My Dashboard"),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -449,25 +449,30 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
details: "User logged in via Baron SSO",
|
details: "User logged in via Baron SSO",
|
||||||
);
|
);
|
||||||
|
|
||||||
// 1. Handle Redirect Flow (Redirect to another app)
|
// 1. Handle Popup Flow (Highest Priority for child windows)
|
||||||
if (_redirectUrl != null && _redirectUrl!.isNotEmpty) {
|
// If opened as a popup (has opener), we notify and try to close.
|
||||||
final target = "$_redirectUrl?token=$token";
|
|
||||||
launchUrlString(target, webOnlyWindowName: '_self');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Handle Popup Flow (Send message to opener)
|
|
||||||
if (WebAuthIntegration.isPopup()) {
|
if (WebAuthIntegration.isPopup()) {
|
||||||
|
debugPrint("[Auth] Popup detected. Notifying opener and attempting to close.");
|
||||||
WebAuthIntegration.sendLoginSuccess(token);
|
WebAuthIntegration.sendLoginSuccess(token);
|
||||||
// If this window was truly a popup for another app, it should close now.
|
|
||||||
// If it's still here, we allow it to fall through to the dashboard.
|
// We don't 'return' here to allow a fallback if window.close() is blocked,
|
||||||
|
// but in most cases WebAuthIntegration.sendLoginSuccess will close the window.
|
||||||
|
} else {
|
||||||
|
// 2. Handle Redirect Flow (Only if NOT a popup)
|
||||||
|
if (_redirectUrl != null && _redirectUrl!.isNotEmpty) {
|
||||||
|
debugPrint("[Auth] Redirecting standalone window to: $_redirectUrl");
|
||||||
|
final target = "$_redirectUrl?token=$token";
|
||||||
|
launchUrlString(target, webOnlyWindowName: '_self');
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Standalone mode: Go to dashboard
|
// 3. Standalone mode / Fallback
|
||||||
// We call notify() to update the router's state, and go() to ensure navigation.
|
// If it's a standard login, or if a popup's window.close() was blocked by the browser.
|
||||||
|
debugPrint("[Auth] Login success. Navigating to root.");
|
||||||
AuthNotifier.instance.notify();
|
AuthNotifier.instance.notify();
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
context.go('/dashboard');
|
context.go('/');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class LoginSuccessScreen extends StatelessWidget {
|
|||||||
// 이 버튼이 QR 카메라를 켜는 버튼입니다.
|
// 이 버튼이 QR 카메라를 켜는 버튼입니다.
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.push('/qr-scan');
|
context.push('/scan');
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.camera_alt, size: 28),
|
icon: const Icon(Icons.camera_alt, size: 28),
|
||||||
label: const Text("QR 인증 (카메라 켜기)"),
|
label: const Text("QR 인증 (카메라 켜기)"),
|
||||||
@@ -50,7 +50,7 @@ class LoginSuccessScreen extends StatelessWidget {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.go('/dashboard');
|
context.go('/');
|
||||||
},
|
},
|
||||||
child: const Text("나중에 하기 (대시보드로 이동)", style: TextStyle(color: Colors.grey)),
|
child: const Text("나중에 하기 (대시보드로 이동)", style: TextStyle(color: Colors.grey)),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class DashboardScreen extends StatelessWidget {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.grey[50],
|
backgroundColor: Colors.grey[50],
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('Baron Launcher', style: GoogleFonts.outfit(fontWeight: FontWeight.bold)),
|
title: Text('Baron SSO', style: GoogleFonts.outfit(fontWeight: FontWeight.bold)),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
|
|||||||
@@ -72,7 +72,14 @@ final _router = GoRouter(
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/',
|
path: '/',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
_routerLogger.info("Navigating to root (LoginScreen)");
|
_routerLogger.info("Navigating to root (DashboardScreen)");
|
||||||
|
return const DashboardScreen();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/login',
|
||||||
|
builder: (context, state) {
|
||||||
|
_routerLogger.info("Navigating to /login");
|
||||||
return const LoginScreen();
|
return const LoginScreen();
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -92,13 +99,6 @@ final _router = GoRouter(
|
|||||||
return ApproveQrScreen(pendingRef: ref);
|
return ApproveQrScreen(pendingRef: ref);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
path: '/dashboard',
|
|
||||||
builder: (context, state) {
|
|
||||||
_routerLogger.info("Navigating to /dashboard");
|
|
||||||
return const DashboardScreen();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/scan',
|
path: '/scan',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
@@ -118,17 +118,29 @@ final _router = GoRouter(
|
|||||||
final isLoggedIn =
|
final isLoggedIn =
|
||||||
Descope.sessionManager.session?.refreshToken?.isExpired == false;
|
Descope.sessionManager.session?.refreshToken?.isExpired == false;
|
||||||
final path = state.uri.path;
|
final path = state.uri.path;
|
||||||
final isLoggingIn = path == '/' || path.startsWith('/verify/') || path == '/approve';
|
|
||||||
|
// Public paths that don't require login
|
||||||
|
final isPublicPath = path == '/login' ||
|
||||||
|
path.startsWith('/verify/') ||
|
||||||
|
path == '/approve';
|
||||||
|
|
||||||
_routerLogger.fine("Redirect check - Path: $path, IsLoggedIn: $isLoggedIn");
|
_routerLogger.fine("Redirect check - Path: $path, IsLoggedIn: $isLoggedIn");
|
||||||
|
|
||||||
if (!isLoggedIn && !isLoggingIn) {
|
// 0. ALWAYS allow /verify/ to proceed so it can signal the backend
|
||||||
_routerLogger.info("Not logged in, redirecting to /");
|
if (path.startsWith('/verify/')) {
|
||||||
return '/';
|
return null;
|
||||||
}
|
}
|
||||||
if (isLoggedIn && path == '/') {
|
|
||||||
_routerLogger.info("Logged in, redirecting to /dashboard");
|
// If not logged in and trying to access a protected page, redirect to /login
|
||||||
return '/dashboard';
|
if (!isLoggedIn && !isPublicPath) {
|
||||||
|
_routerLogger.info("Not logged in, redirecting to /login");
|
||||||
|
return '/login';
|
||||||
|
}
|
||||||
|
|
||||||
|
// If logged in and trying to access login page, redirect to root (dashboard)
|
||||||
|
if (isLoggedIn && path == '/login') {
|
||||||
|
_routerLogger.info("Logged in, redirecting to /");
|
||||||
|
return '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user