1
0
forked from baron/baron-sso

userfront gateway 분리.

This commit is contained in:
Lectom C Han
2026-01-30 16:14:20 +09:00
parent 35552943d7
commit 1db7ce8f10
8 changed files with 302 additions and 99 deletions

View File

@@ -469,6 +469,9 @@ class AuthProxyService {
} }
static Future<void> sendLog(String level, String message, {Map<String, dynamic>? data}) async { static Future<void> sendLog(String level, String message, {Map<String, dynamic>? data}) async {
if (!_canSendClientLog()) {
return;
}
final url = Uri.parse('$_baseUrl/api/v1/client-log'); final url = Uri.parse('$_baseUrl/api/v1/client-log');
try { try {
await http.post( await http.post(
@@ -480,7 +483,9 @@ class AuthProxyService {
if (data != null) 'data': data, if (data != null) 'data': data,
}), }),
); );
_recordClientLogSuccess();
} catch (_) { } catch (_) {
_recordClientLogFailure();
// Ignore logging errors to prevent loops // Ignore logging errors to prevent loops
} }
} }
@@ -493,6 +498,34 @@ class AuthProxyService {
await sendLog('ERROR', message, data: data); await sendLog('ERROR', message, data: data);
} }
static int _clientLogFailureCount = 0;
static DateTime? _clientLogLastFailureAt;
static DateTime? _clientLogOpenUntil;
static bool _canSendClientLog() {
final now = DateTime.now();
final openUntil = _clientLogOpenUntil;
if (openUntil != null && now.isBefore(openUntil)) {
return false;
}
return true;
}
static void _recordClientLogFailure() {
_clientLogFailureCount += 1;
_clientLogLastFailureAt = DateTime.now();
if (_clientLogFailureCount >= 3) {
_clientLogOpenUntil = DateTime.now().add(const Duration(minutes: 1));
_clientLogFailureCount = 0;
}
}
static void _recordClientLogSuccess() {
_clientLogFailureCount = 0;
_clientLogLastFailureAt = null;
_clientLogOpenUntil = null;
}
// --- Signup Methods --- // --- Signup Methods ---
static Future<bool> checkEmailAvailability(String email) async { static Future<bool> checkEmailAvailability(String email) async {

View File

@@ -25,7 +25,7 @@ class LoggerService {
); );
// 2. Configure Standard Logger (logging package) // 2. Configure Standard Logger (logging package)
std_log.Logger.root.level = kReleaseMode ? std_log.Level.INFO : std_log.Level.ALL; std_log.Logger.root.level = kReleaseMode ? std_log.Level.WARNING : std_log.Level.ALL;
std_log.Logger.root.onRecord.listen((record) { std_log.Logger.root.onRecord.listen((record) {
if (kReleaseMode) { if (kReleaseMode) {
@@ -71,7 +71,7 @@ class LoggerService {
debugPrint(jsonEncode(logData)); debugPrint(jsonEncode(logData));
// 2. Relay to Backend (Docker Terminal) // 2. Relay to Backend (Docker Terminal)
if (record.level >= std_log.Level.INFO) { if (record.level >= std_log.Level.WARNING) {
AuthProxyService.sendLog( AuthProxyService.sendLog(
record.level.name, record.level.name,
record.message, record.message,

View File

@@ -17,11 +17,12 @@ class _ApproveQrScreenState extends State<ApproveQrScreen> {
String? _message; String? _message;
bool _success = false; bool _success = false;
bool _isCheckingSession = false; bool _isCheckingSession = false;
bool _redirectingToLogin = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_bootstrapCookieSession(); _bootstrapCookieSession().then((_) => _redirectIfNotLoggedIn());
} }
Future<bool> _bootstrapCookieSession() async { Future<bool> _bootstrapCookieSession() async {
@@ -45,6 +46,21 @@ class _ApproveQrScreenState extends State<ApproveQrScreen> {
} }
} }
void _redirectIfNotLoggedIn() {
if (_redirectingToLogin || !mounted) return;
final hasStoredToken = AuthTokenStore.getToken() != null;
final hasDescopeSession = Descope.sessionManager.session?.refreshToken.isExpired == false;
final usesCookie = AuthTokenStore.usesCookie();
final isLoggedIn = hasStoredToken || hasDescopeSession || usesCookie;
if (!isLoggedIn) {
_redirectingToLogin = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
context.go('/signin?notice=qr_login_required');
});
}
}
Future<void> _handleApprove() async { Future<void> _handleApprove() async {
if (widget.pendingRef == null) return; if (widget.pendingRef == null) return;
@@ -56,8 +72,9 @@ class _ApproveQrScreenState extends State<ApproveQrScreen> {
hasCookie = await _bootstrapCookieSession(); hasCookie = await _bootstrapCookieSession();
} }
if (storedToken == null && (session == null || session.refreshToken.isExpired) && !hasCookie) { if (storedToken == null && (session == null || session.refreshToken.isExpired) && !hasCookie) {
setState(() => _message = "Please log in on your phone first."); if (mounted) {
context.go('/signin'); // Redirect to login context.go('/signin?notice=qr_login_required');
}
return; return;
} }
@@ -96,6 +113,10 @@ class _ApproveQrScreenState extends State<ApproveQrScreen> {
final usesCookie = AuthTokenStore.usesCookie(); final usesCookie = AuthTokenStore.usesCookie();
final isLoggedIn = hasStoredToken || hasDescopeSession || usesCookie || _isCheckingSession; final isLoggedIn = hasStoredToken || hasDescopeSession || usesCookie || _isCheckingSession;
if (!isLoggedIn && !_redirectingToLogin) {
_redirectIfNotLoggedIn();
}
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text("QR Login Approval")), appBar: AppBar(title: const Text("QR Login Approval")),
body: Center( body: Center(

View File

@@ -48,6 +48,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
bool _verificationOnly = false; bool _verificationOnly = false;
bool _verificationApproved = false; bool _verificationApproved = false;
String _verificationMessage = ''; String _verificationMessage = '';
String _verificationTitle = '승인 완료';
String _verificationPageTitle = '로그인 승인';
String _verificationActionLabel = '확인';
String _verificationActionPath = '/';
Timer? _verificationRedirectTimer;
bool _noticeHandled = false;
bool _drySendEnabled = false; bool _drySendEnabled = false;
@override @override
@@ -69,6 +75,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
final hasVerificationToken = widget.verificationToken != null || hasTokenParam; final hasVerificationToken = widget.verificationToken != null || hasTokenParam;
final hasLoginCode = loginIdParam != null && codeParam != null; final hasLoginCode = loginIdParam != null && codeParam != null;
_verificationOnly = hasVerificationToken || hasLoginCode || hasShortCodePath; _verificationOnly = hasVerificationToken || hasLoginCode || hasShortCodePath;
final notice = uri.queryParameters['notice'];
if (hasShortCodePath) { if (hasShortCodePath) {
final shortCode = uri.pathSegments[1]; final shortCode = uri.pathSegments[1];
@@ -80,6 +87,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_verifyToken(widget.verificationToken ?? uri.queryParameters['t']!); _verifyToken(widget.verificationToken ?? uri.queryParameters['t']!);
} }
if (!_noticeHandled && notice == 'qr_login_required') {
_noticeHandled = true;
_showInfo('로그인 한 상태여야 QR 스캔으로 로그인 할 수 있습니다');
}
if (!_verificationOnly) { if (!_verificationOnly) {
_tryCookieSession(); _tryCookieSession();
} }
@@ -360,12 +372,31 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
return AuthTokenStore.usesCookie(); return AuthTokenStore.usesCookie();
} }
void _markVerificationApproved(String message) { void _markVerificationApproved(
String message, {
String title = '승인 완료',
String pageTitle = '로그인 승인',
String actionLabel = '확인',
String actionPath = '/',
bool autoRedirect = false,
Duration redirectDelay = const Duration(seconds: 2),
}) {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_verificationApproved = true; _verificationApproved = true;
_verificationMessage = message; _verificationMessage = message;
_verificationTitle = title;
_verificationPageTitle = pageTitle;
_verificationActionLabel = actionLabel;
_verificationActionPath = actionPath;
}); });
_verificationRedirectTimer?.cancel();
if (autoRedirect) {
_verificationRedirectTimer = Timer(redirectDelay, () {
if (!mounted) return;
context.go(actionPath);
});
}
} }
Widget _buildVerificationResultView() { Widget _buildVerificationResultView() {
@@ -377,9 +408,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
children: [ children: [
const Icon(Icons.check_circle_outline, color: Colors.green, size: 72), const Icon(Icons.check_circle_outline, color: Colors.green, size: 72),
const SizedBox(height: 16), const SizedBox(height: 16),
const Text( Text(
'승인 완료', _verificationTitle,
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.green), style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.green),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
@@ -389,8 +420,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
FilledButton( FilledButton(
onPressed: () => context.go('/'), onPressed: () => context.go(_verificationActionPath),
child: const Text('확인'), child: Text(_verificationActionLabel),
), ),
], ],
), ),
@@ -410,10 +441,19 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
if (jwt is String && jwt.isNotEmpty) { if (jwt is String && jwt.isNotEmpty) {
if (hasLocalSession) { if (hasLocalSession) {
_markVerificationApproved("승인되었습니다. 이미 로그인된 브라우저입니다."); _markVerificationApproved(
"승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다",
);
return; return;
} }
_completeLoginFromToken(jwt, provider: provider); _markVerificationApproved(
"링크로 로그인 되었습니다. 잠시 후 로그인 화면으로 이동합니다.",
title: '링크 로그인 완료',
pageTitle: '링크 로그인',
actionLabel: '로그인 화면으로 이동',
actionPath: '/signin',
autoRedirect: true,
);
return; return;
} }
@@ -451,7 +491,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
if (jwt is String && jwt.isNotEmpty) { if (jwt is String && jwt.isNotEmpty) {
if (hasLocalSession) { if (hasLocalSession) {
_markVerificationApproved("승인되었습니다. 이미 로그인된 브라우저입니다."); _markVerificationApproved(
"승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다",
);
return; return;
} }
_completeLoginFromToken(jwt, provider: res['provider'] as String?); _completeLoginFromToken(jwt, provider: res['provider'] as String?);
@@ -489,7 +531,9 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
if (jwt is String && jwt.isNotEmpty) { if (jwt is String && jwt.isNotEmpty) {
if (hasLocalSession) { if (hasLocalSession) {
_markVerificationApproved("승인되었습니다. 이미 로그인된 브라우저입니다."); _markVerificationApproved(
"승인 되었습니다. 이 기기는 로그인되어 있는 상태입니다. 원격 창도 로그인이 될 예정입니다",
);
return; return;
} }
_completeLoginFromToken(jwt, provider: res['provider'] as String?); _completeLoginFromToken(jwt, provider: res['provider'] as String?);
@@ -510,6 +554,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
@override @override
void dispose() { void dispose() {
_stopQrPolling(); _stopQrPolling();
_verificationRedirectTimer?.cancel();
_tabController.dispose(); _tabController.dispose();
_linkIdController.dispose(); _linkIdController.dispose();
_passwordLoginIdController.dispose(); _passwordLoginIdController.dispose();
@@ -851,7 +896,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
if (_verificationOnly && _verificationApproved) { if (_verificationOnly && _verificationApproved) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('로그인 승인'), title: Text(_verificationPageTitle),
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/'), onPressed: () => context.go('/'),

View File

@@ -21,6 +21,7 @@ class _QRScanScreenState extends State<QRScanScreen> {
bool _isScanned = false; bool _isScanned = false;
bool _isCheckingSession = false; bool _isCheckingSession = false;
bool _isProcessing = false; bool _isProcessing = false;
bool _isRequestingCamera = false;
bool? _isSuccess; bool? _isSuccess;
String? _resultMessage; String? _resultMessage;
@@ -100,10 +101,7 @@ class _QRScanScreenState extends State<QRScanScreen> {
} }
if (sessionToken == null && !usesCookie) { if (sessionToken == null && !usesCookie) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( context.go('/signin?notice=qr_login_required');
const SnackBar(content: Text('로그인이 필요합니다.'), backgroundColor: Colors.red),
);
context.pop();
} }
return; return;
} }
@@ -148,6 +146,28 @@ class _QRScanScreenState extends State<QRScanScreen> {
controller.start(); controller.start();
} }
Future<void> _requestCameraPermission() async {
if (_isRequestingCamera) return;
setState(() => _isRequestingCamera = true);
try {
await controller.start();
} catch (e) {
_log.warning('Camera permission request failed: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('카메라 권한 요청에 실패했습니다. 브라우저/OS 설정을 확인해주세요.'),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() => _isRequestingCamera = false);
}
}
}
Widget _buildResultView() { Widget _buildResultView() {
final success = _isSuccess == true; final success = _isSuccess == true;
final icon = success ? Icons.check_circle_outline : Icons.error_outline; final icon = success ? Icons.check_circle_outline : Icons.error_outline;
@@ -207,13 +227,30 @@ class _QRScanScreenState extends State<QRScanScreen> {
controller: controller, controller: controller,
onDetect: _onDetect, onDetect: _onDetect,
errorBuilder: (context, error) { errorBuilder: (context, error) {
final isPermissionDenied = error.errorCode ==
MobileScannerErrorCode.permissionDenied;
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon(Icons.error, color: Colors.red, size: 50), const Icon(Icons.error, color: Colors.red, size: 50),
const SizedBox(height: 10), const SizedBox(height: 10),
Text('Camera Error: ${error.errorCode}'), Text(
isPermissionDenied
? '카메라 권한이 필요합니다.'
: '카메라 오류: ${error.errorCode}',
),
const SizedBox(height: 12),
FilledButton(
onPressed: _isRequestingCamera
? null
: _requestCameraPermission,
child: Text(
_isRequestingCamera
? '요청 중...'
: '카메라 권한 요청하기',
),
),
], ],
), ),
); );

View File

@@ -31,6 +31,11 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
final FocusNode _departmentFocus = FocusNode(); final FocusNode _departmentFocus = FocusNode();
final FocusNode _phoneFocus = FocusNode(); final FocusNode _phoneFocus = FocusNode();
final FocusNode _phoneCodeFocus = FocusNode(); final FocusNode _phoneCodeFocus = FocusNode();
bool _nameTouched = false;
bool _departmentTouched = false;
bool _phoneTouched = false;
bool _phoneCodeTouched = false;
bool _isSavingField = false;
String _initialPhone = ''; String _initialPhone = '';
bool _isPhoneChanged = false; bool _isPhoneChanged = false;
@@ -102,6 +107,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_isCodeSent = false; _isCodeSent = false;
_isVerifying = false; _isVerifying = false;
_codeController?.clear(); _codeController?.clear();
_phoneTouched = false;
_phoneCodeTouched = false;
} }
void _startEditing(String field, UserProfile profile) { void _startEditing(String field, UserProfile profile) {
@@ -117,6 +124,16 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_resetPhoneState(); _resetPhoneState();
} }
}); });
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
if (field == 'name') {
FocusScope.of(context).requestFocus(_nameFocus);
} else if (field == 'department') {
FocusScope.of(context).requestFocus(_departmentFocus);
} else if (field == 'phone') {
FocusScope.of(context).requestFocus(_phoneFocus);
}
});
} }
void _cancelEditing(UserProfile profile) { void _cancelEditing(UserProfile profile) {
@@ -131,6 +148,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_resetPhoneState(); _resetPhoneState();
} }
_editingField = null; _editingField = null;
_nameTouched = false;
_departmentTouched = false;
}); });
} }
@@ -193,18 +212,55 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
void _autoSaveIfEditing(UserProfile profile, String field) { void _autoSaveIfEditing(UserProfile profile, String field) {
if (_editingField != field) return; if (_editingField != field) return;
if (_isVerifying) return; if (_isVerifying) return;
if (_isSavingField) return;
if (!_hasFieldChanged(profile, field)) {
setState(() {
if (field == 'phone') {
_resetPhoneState();
}
_editingField = null;
if (field == 'name') {
_nameTouched = false;
} else if (field == 'department') {
_departmentTouched = false;
}
});
return;
}
_saveField(profile); _saveField(profile);
} }
void _handlePhoneFocusChange(UserProfile profile) { void _handlePhoneFocusChange(UserProfile profile) {
if (_editingField != 'phone') return; if (_editingField != 'phone') return;
if (_isVerifying) return; if (_isVerifying) return;
if (_isSavingField) return;
if (_phoneFocus.hasFocus || _phoneCodeFocus.hasFocus) return; if (_phoneFocus.hasFocus || _phoneCodeFocus.hasFocus) return;
if (!_hasFieldChanged(profile, 'phone')) {
setState(() {
_resetPhoneState();
_editingField = null;
});
return;
}
_saveField(profile); _saveField(profile);
} }
bool _hasFieldChanged(UserProfile profile, String field) {
if (field == 'name') {
return (_nameController?.text.trim() ?? '') != profile.name;
}
if (field == 'department') {
return (_departmentController?.text.trim() ?? '') != profile.department;
}
if (field == 'phone') {
return (_phoneController?.text.trim() ?? '') != profile.phone;
}
return false;
}
Future<void> _saveField(UserProfile profile) async { Future<void> _saveField(UserProfile profile) async {
if (_editingField == null) return; if (_editingField == null) return;
if (_isSavingField) return;
final nextName = _editingField == 'name' final nextName = _editingField == 'name'
? _nameController!.text.trim() ? _nameController!.text.trim()
@@ -243,6 +299,20 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
} }
} }
if (!_hasFieldChanged(profile, _editingField!)) {
setState(() {
if (_editingField == 'phone') {
_resetPhoneState();
}
_editingField = null;
_nameTouched = false;
_departmentTouched = false;
});
return;
}
_isSavingField = true;
try { try {
await ref.read(profileProvider.notifier).updateProfile( await ref.read(profileProvider.notifier).updateProfile(
name: nextName, name: nextName,
@@ -256,6 +326,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
_resetPhoneState(); _resetPhoneState();
} }
_editingField = null; _editingField = null;
_nameTouched = false;
_departmentTouched = false;
}); });
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('정보가 수정되었습니다.')), const SnackBar(content: Text('정보가 수정되었습니다.')),
@@ -267,6 +339,8 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
SnackBar(content: Text('수정 실패: $e')), SnackBar(content: Text('수정 실패: $e')),
); );
} }
} finally {
_isSavingField = false;
} }
} }
@@ -454,9 +528,31 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
children: [ children: [
Text(label, style: const TextStyle(fontWeight: FontWeight.w600)), Text(label, style: const TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 8), const SizedBox(height: 8),
Focus( Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Focus(
focusNode: field == 'name' ? _nameFocus : _departmentFocus, focusNode: field == 'name' ? _nameFocus : _departmentFocus,
onFocusChange: (hasFocus) { onFocusChange: (hasFocus) {
if (field == 'name') {
if (hasFocus) {
_nameTouched = true;
return;
}
if (!_nameTouched) {
return;
}
}
if (field == 'department') {
if (hasFocus) {
_departmentTouched = true;
return;
}
if (!_departmentTouched) {
return;
}
}
if (!hasFocus) { if (!hasFocus) {
_autoSaveIfEditing(profile, field); _autoSaveIfEditing(profile, field);
} }
@@ -472,6 +568,14 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
), ),
), ),
), ),
),
const SizedBox(width: 12),
OutlinedButton(
onPressed: isUpdating ? null : () => _cancelEditing(profile),
child: const Text('취소'),
),
],
),
], ],
); );
} }
@@ -504,6 +608,13 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
child: Focus( child: Focus(
focusNode: _phoneFocus, focusNode: _phoneFocus,
onFocusChange: (hasFocus) { onFocusChange: (hasFocus) {
if (hasFocus) {
_phoneTouched = true;
return;
}
if (!_phoneTouched) {
return;
}
if (!hasFocus) { if (!hasFocus) {
_handlePhoneFocusChange(profile); _handlePhoneFocusChange(profile);
} }
@@ -531,6 +642,11 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
onPressed: _isVerifying ? null : _sendCode, onPressed: _isVerifying ? null : _sendCode,
child: Text(_isCodeSent ? '재전송' : '인증요청'), child: Text(_isCodeSent ? '재전송' : '인증요청'),
), ),
const SizedBox(width: 8),
OutlinedButton(
onPressed: isUpdating ? null : () => _cancelEditing(profile),
child: const Text('취소'),
),
], ],
), ),
if (_isCodeSent && !_isPhoneVerified) ...[ if (_isCodeSent && !_isPhoneVerified) ...[
@@ -542,6 +658,13 @@ class _ProfilePageState extends ConsumerState<ProfilePage> {
child: Focus( child: Focus(
focusNode: _phoneCodeFocus, focusNode: _phoneCodeFocus,
onFocusChange: (hasFocus) { onFocusChange: (hasFocus) {
if (hasFocus) {
_phoneCodeTouched = true;
return;
}
if (!_phoneCodeTouched) {
return;
}
if (!hasFocus) { if (!hasFocus) {
_handlePhoneFocusChange(profile); _handlePhoneFocusChange(profile);
} }

View File

@@ -89,7 +89,7 @@ final _routerLogger = Logger('Router');
final _router = GoRouter( final _router = GoRouter(
initialLocation: '/', initialLocation: '/',
debugLogDiagnostics: true, // Enable diagnostic logs debugLogDiagnostics: !kReleaseMode,
refreshListenable: AuthNotifier.instance, refreshListenable: AuthNotifier.instance,
routes: [ routes: [
GoRoute( GoRoute(

View File

@@ -1,9 +1,9 @@
# Map ISO8601 time to "YYYY-MM-DD HH:mm:ss" format # ISO8601 시간을 "YYYY-MM-DD HH:mm:ss" 형식으로 변환
map $time_iso8601 $time_custom { map $time_iso8601 $time_custom {
"~^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})" "$1-$2-$3 $4:$5:$6"; "~^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})" "$1-$2-$3 $4:$5:$6";
} }
# Custom JSON Log Format matching Go slog # Go slog 포맷과 맞춘 JSON 액세스 로그
log_format json_combined escape=json log_format json_combined escape=json
'{' '{'
'"time":"$time_custom",' '"time":"$time_custom",'
@@ -23,66 +23,10 @@ server {
listen 5000; listen 5000;
include /etc/nginx/mime.types; include /etc/nginx/mime.types;
error_log /dev/stderr warn;
access_log /var/log/nginx/access.log json_combined; access_log /var/log/nginx/access.log json_combined;
# --- Backend API Proxy --- # --- UserFront 정적 파일 ---
location /api {
proxy_pass http://baron_backend:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# --- Ory Stack Proxy (via Oathkeeper) ---
# Kratos Public API
location /auth {
proxy_pass http://oathkeeper:4455;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Hydra Public API
location /oidc {
proxy_pass http://oathkeeper:4455;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# --- Internal Web Apps Proxy --- 초반에는 외부 오픈 없이 Private Net 내부에서만 운영
# AdminFront (Vite Dev Server or Nginx)
# location /admin {
# proxy_pass http://baron_adminfront:5173;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# # WebSocket support (for Vite HMR)
# proxy_http_version 1.1;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection "upgrade";
# }
# # DevFront (Vite Dev Server or Nginx)
# location /dev {
# proxy_pass http://baron_devfront:5173;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# # WebSocket support (for Vite HMR)
# proxy_http_version 1.1;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection "upgrade";
# }
# --- UserFront Static Files ---
location / { location / {
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;