diff --git a/.idea/rup.iml b/.idea/rup.iml index 088e44c..b8f276c 100644 --- a/.idea/rup.iml +++ b/.idea/rup.iml @@ -6,8 +6,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/.metadata b/app/.metadata index 08c2478..6650897 100644 --- a/app/.metadata +++ b/app/.metadata @@ -41,5 +41,5 @@ migration: # # Files that are not part of the templates will be ignored by default. unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' + - "lib/main.dart" + - "ios/Runner.xcodeproj/project.pbxproj" diff --git a/app/assets/fonts/SCDream-regular.otf b/app/assets/fonts/SCDream-regular.otf new file mode 100644 index 0000000..1978ea4 Binary files /dev/null and b/app/assets/fonts/SCDream-regular.otf differ diff --git a/app/assets/icons/appointmenticon.svg b/app/assets/icons/appointmenticon.svg new file mode 100644 index 0000000..eaef2b8 --- /dev/null +++ b/app/assets/icons/appointmenticon.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/catdogicon.svg b/app/assets/icons/catdogicon.svg new file mode 100644 index 0000000..0ef6def --- /dev/null +++ b/app/assets/icons/catdogicon.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/assets/icons/homeicon.svg b/app/assets/icons/homeicon.svg new file mode 100644 index 0000000..fc6de4c --- /dev/null +++ b/app/assets/icons/homeicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/icons/myicon.svg b/app/assets/icons/myicon.svg new file mode 100644 index 0000000..2253fb4 --- /dev/null +++ b/app/assets/icons/myicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/assets/icons/shopicon.svg b/app/assets/icons/shopicon.svg new file mode 100644 index 0000000..75df13b --- /dev/null +++ b/app/assets/icons/shopicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/assets/img/catdog_off.png b/app/assets/img/catdog_off.png new file mode 100644 index 0000000..114cb07 Binary files /dev/null and b/app/assets/img/catdog_off.png differ diff --git a/app/assets/img/catdog_on.png b/app/assets/img/catdog_on.png new file mode 100644 index 0000000..c719865 Binary files /dev/null and b/app/assets/img/catdog_on.png differ diff --git a/app/lib/data/terms_data.dart b/app/lib/data/terms_data.dart new file mode 100644 index 0000000..3e0a67f --- /dev/null +++ b/app/lib/data/terms_data.dart @@ -0,0 +1,34 @@ +class TermsData { + static const List> terms = [ + { + 'title': '[제1조 목적]', + 'content': + '본 약관은 RUP(이하 "회사")가 제공하는 서비스 이용과 관련하여 회사와 회원의 권리, 의무 및 책임사항을 규정함을 목적으로 합니다.\\n\\n[제2조 정의]\\n1. "서비스"란 구현되는 단말기와 상관없이 회원이 이용할 수 있는 RUP 및 관련 제반 서비스를 의미합니다.\\n2. "회원"이란 회사의 서비스에 접속하여 본 약관에 따라 회사와 이용계약을 체결하고 회사가 제공하는 서비스를 이용하는 고객을 말합니다.', + }, + { + 'title': '[개인정보 수집 및 이용 동의]', + 'content': + '회사는 회원가입, 고객상담, 서비스 신청 등을 위해 아래와 같은 개인정보를 수집하고 있습니다.\\n\\n1. 수집 항목\\n- 필수항목: 이메일, 닉네임, 비밀번호, 서비스 이용 기록\\n- 선택항목: 프로필 사진, 위치 정보\\n\\n2. 수집 목적\\n- 서비스 제공, 회원 관리, 신규 서비스 개발 및 마케팅 광고에의 활용', + }, + { + 'title': '[개인정보 제3자 제공 동의]', + 'content': + '회사는 이용자의 개인정보를 원칙적으로 외부에 제공하지 않습니다. 다만, 아래의 경우에는 예외로 합니다.\\n\\n1. 이용자들이 사전에 동의한 경우\\n2. 법령의 규정에 의거하거나, 수사 목적으로 법령에 정해진 절차와 방법에 따라 수사기관의 요구가 있는 경우', + }, + { + 'title': '[만 14세 이상 이용 동의]', + 'content': + '본 서비스는 만 14세 이상만 이용 가능합니다.\\n만 14세 미만 아동의 경우 회원가입 및 서비스 이용이 제한될 수 있습니다.', + }, + { + 'title': '[위치기반 서비스 이용약관]', + 'content': + '본 약관은 회사가 제공하는 위치기반서비스와 관련하여 회사와 개인위치정보주체와의 권리, 의무 및 책임사항, 기타 필요한 사항을 규정함을 목적으로 합니다.\\n\\n회사는 이용자의 위치 정보를 이용하여 주변 펫 프렌들리 장소 추천 등의 서비스를 제공합니다.', + }, + { + 'title': '[마케팅 정보 수신 동의]', + 'content': + '회사가 제공하는 이벤트, 혜택, 뉴스레터 등 다양한 마케팅 정보를 이메일, 앱 푸시 알림 등으로 받아보실 수 있습니다.\\n\\n동의를 거부하시더라도 기본 서비스 이용에는 제한이 없으나, 이벤트 참여 및 혜택 제공이 제한될 수 있습니다.', + }, + ]; +} diff --git a/app/lib/main.dart b/app/lib/main.dart index 24fbcc2..10958ef 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -1,16 +1,33 @@ import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; import 'package:firebase_core/firebase_core.dart'; -import 'screens/splash_screen.dart'; import 'dart:developer'; +import 'screens/splash_screen.dart'; +import 'utils/log_manager.dart'; + +final GlobalKey navigatorKey = GlobalKey(); void main() async { WidgetsFlutterBinding.ensureInitialized(); + + // 글로벌 에러 핸들링 + FlutterError.onError = (FlutterErrorDetails details) { + FlutterError.presentError(details); + LogManager().addLog('[APP ERROR] ${details.exception}'); + }; + + PlatformDispatcher.instance.onError = (error, stack) { + LogManager().addLog('[Uncaught Error] $error'); + return true; + }; + try { await Firebase.initializeApp(); } catch (e) { log('Firebase initialization failed: $e'); + LogManager().addLog('[Firebase Init Error] $e'); } - runApp(RupApp()); + runApp(const RupApp()); } class RupApp extends StatelessWidget { @@ -18,6 +35,10 @@ class RupApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp(debugShowCheckedModeBanner: false, home: SplashScreen()); + return MaterialApp( + navigatorKey: navigatorKey, + debugShowCheckedModeBanner: false, + home: const SplashScreen(), + ); } } diff --git a/app/lib/screens/home_screen.dart b/app/lib/screens/home_screen.dart index 028ed83..ebeec3f 100644 --- a/app/lib/screens/home_screen.dart +++ b/app/lib/screens/home_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'welcome_screen.dart'; class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); @@ -7,33 +6,16 @@ class HomeScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text( - '홈', - style: TextStyle(fontFamily: 'SCDream', fontWeight: FontWeight.bold), - ), - centerTitle: true, - actions: [ - IconButton( - icon: const Icon(Icons.logout), - onPressed: () { - // Mock Logout: Go back to Welcome Screen - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const WelcomeScreen()), - ); - }, - ), - ], - ), - body: const Center( - child: Text( - '로그인 성공!\n여기는 메인 홈 화면입니다.', - textAlign: TextAlign.center, - style: TextStyle( - fontFamily: 'SCDream', - fontWeight: FontWeight.w500, - fontSize: 18, + body: const SafeArea( + child: Center( + child: Text( + '로그인 성공!\n여기는 메인 홈 화면입니다.', + textAlign: TextAlign.center, + style: TextStyle( + fontFamily: 'SCDream', + fontWeight: FontWeight.w500, + fontSize: 18, + ), ), ), ), diff --git a/app/lib/screens/identity_verification_screen.dart b/app/lib/screens/identity_verification_screen.dart new file mode 100644 index 0000000..049c6f3 --- /dev/null +++ b/app/lib/screens/identity_verification_screen.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import 'main_screen.dart'; + +class IdentityVerificationScreen extends StatelessWidget { + const IdentityVerificationScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: Colors.black, size: 20), + onPressed: () => Navigator.pop(context), + ), + title: const Text( + '본인 인증', + style: TextStyle( + fontSize: 15, + fontFamily: 'SCDream', + fontWeight: FontWeight.w500, + color: Colors.black, + ), + ), + centerTitle: true, + ), + body: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 20), + const Text( + '더 안전한 서비스 이용을 위해\n본인 인증을 진행해 주세요.', + style: TextStyle( + fontSize: 20, + fontFamily: 'SCDream', + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + const SizedBox(height: 40), + + // 본인 인증 UI (Placeholder) + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.grey[300]!), + ), + child: Column( + children: [ + const Icon( + Icons.shield_outlined, + size: 50, + color: Color(0xFFFF7500), + ), + const SizedBox(height: 10), + const Text( + 'PASS / 문자 인증', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 10), + const Text( + '(현재 UI만 구현된 상태입니다)', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 12, + color: Colors.grey, + ), + ), + ], + ), + ), + + const Spacer(), + + // 건너뛰기 버튼 + TextButton( + onPressed: () { + // 홈 화면으로 이동 (로그인 프로세스 완료) + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) => const MainScreen()), + (route) => false, + ); + }, + child: const Text( + '다음에 하기 (건너뛰기)', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.grey, + decoration: TextDecoration.underline, + ), + ), + ), + const SizedBox(height: 10), + + // 인증하기 버튼 (현재는 동작 X) + SizedBox( + height: 50, + child: ElevatedButton( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('본인 인증 기능은 추후 구현 예정입니다.')), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFFF7500), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + '인증하기', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + const SizedBox(height: 10), + ], + ), + ), + ); + } +} diff --git a/app/lib/screens/login_screen.dart b/app/lib/screens/login_screen.dart index bc50af3..67005be 100644 --- a/app/lib/screens/login_screen.dart +++ b/app/lib/screens/login_screen.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../services/auth_service.dart'; -import 'home_screen.dart'; +import 'main_screen.dart'; +import 'terms_agreement_screen.dart'; // Import TermsAgreementScreen class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @@ -20,15 +21,27 @@ class _LoginScreenState extends State { try { final authService = AuthService(); - final user = await authService.signInWithGoogle(); + final result = await authService.signInWithGoogle(); if (!mounted) return; - if (user != null) { - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - ); + if (result != null) { + final isNewUser = result['isNewUser'] as bool; + + if (isNewUser) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + TermsAgreementScreen(idToken: result['idToken']), + ), + ); + } else { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const MainScreen()), + ); + } } else { ScaffoldMessenger.of( context, diff --git a/app/lib/screens/main_screen.dart b/app/lib/screens/main_screen.dart new file mode 100644 index 0000000..cfeab69 --- /dev/null +++ b/app/lib/screens/main_screen.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'home_screen.dart'; +import 'reservation_screen.dart'; +import 'mungnyangz_screen.dart'; +import 'shop_screen.dart'; +import 'my_info_screen.dart'; + +import '../theme/app_colors.dart'; + +class MainScreen extends StatefulWidget { + const MainScreen({super.key}); + + @override + State createState() => _MainScreenState(); +} + +class _MainScreenState extends State { + int _selectedIndex = 0; + + // 탭별 화면 리스트 + final List _screens = [ + const HomeScreen(), + const ReservationScreen(), + const MungNyangzScreen(), + const ShopScreen(), + const MyInfoScreen(), + ]; + + void _onItemTapped(int index) { + setState(() { + _selectedIndex = index; + }); + } + + // SVG 아이콘 빌더 (선택 여부에 따라 색상 변경) + Widget _buildSvgIcon(String assetName, int index) { + return SvgPicture.asset( + assetName, + width: 24, + height: 24, + colorFilter: ColorFilter.mode( + _selectedIndex == index + ? AppColors.highlight + : AppColors.inactive, // 선택됨: 강조색, 안됨: 비활성화색 + BlendMode.srcIn, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: IndexedStack(index: _selectedIndex, children: _screens), + bottomNavigationBar: BottomNavigationBar( + currentIndex: _selectedIndex, + onTap: _onItemTapped, + type: BottomNavigationBarType.fixed, + selectedItemColor: AppColors.highlight, + unselectedItemColor: AppColors.inactive, + selectedLabelStyle: TextStyle( + fontFamily: 'SCDream', + fontSize: 12, + fontWeight: FontWeight.w500, // Medium + ), + unselectedLabelStyle: TextStyle( + fontFamily: 'SCDream', + fontSize: 12, + fontWeight: FontWeight.w400, // Regular + ), + showUnselectedLabels: true, + items: [ + BottomNavigationBarItem( + icon: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: _buildSvgIcon('assets/icons/homeicon.svg', 0), + ), + label: '홈', + ), + BottomNavigationBarItem( + icon: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: _buildSvgIcon('assets/icons/appointmenticon.svg', 1), + ), + label: '예약/조회', + ), + BottomNavigationBarItem( + icon: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Image.asset( + _selectedIndex == 2 + ? 'assets/img/catdog_on.png' + : 'assets/img/catdog_off.png', + width: 24, + height: 24, + ), + ), + label: '멍냥즈', + ), + BottomNavigationBarItem( + icon: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: _buildSvgIcon('assets/icons/shopicon.svg', 3), + ), + label: '상점', + ), + BottomNavigationBarItem( + icon: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: _buildSvgIcon('assets/icons/myicon.svg', 4), + ), + label: '내 정보', + ), + ], + ), + ); + } +} diff --git a/app/lib/screens/mungnyangz_screen.dart b/app/lib/screens/mungnyangz_screen.dart new file mode 100644 index 0000000..2ddfb6f --- /dev/null +++ b/app/lib/screens/mungnyangz_screen.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +import '../utils/log_manager.dart'; + +class MungNyangzScreen extends StatelessWidget { + const MungNyangzScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: ValueListenableBuilder>( + valueListenable: LogManager().logs, + builder: (context, logs, child) { + if (logs.isEmpty) { + return const Center( + child: Text('로그가 없습니다.', style: TextStyle(color: Colors.grey)), + ); + } + return ListView.builder( + padding: const EdgeInsets.all(10), + itemCount: logs.length, + itemBuilder: (context, index) { + return Container( + margin: const EdgeInsets.only(bottom: 5), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.black12, + borderRadius: BorderRadius.circular(5), + ), + child: Text( + logs[index], + style: const TextStyle(fontSize: 12, fontFamily: 'SCDream'), + ), + ); + }, + ); + }, + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + LogManager().clear(); + }, + mini: true, + child: const Icon(Icons.delete), + ), + ); + } +} diff --git a/app/lib/screens/my_info_screen.dart b/app/lib/screens/my_info_screen.dart new file mode 100644 index 0000000..36c2d1e --- /dev/null +++ b/app/lib/screens/my_info_screen.dart @@ -0,0 +1,398 @@ +import 'package:flutter/material.dart'; +import '../services/auth_service.dart'; +import 'welcome_screen.dart'; +import 'notice_screen.dart'; // 공지사항 화면 임포트 +import '../data/terms_data.dart'; // 데이터 임포트 + +class MyInfoScreen extends StatefulWidget { + const MyInfoScreen({super.key}); + + @override + State createState() => _MyInfoScreenState(); +} + +class _MyInfoScreenState extends State { + final AuthService _authService = AuthService(); + Map? _userInfo; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _fetchUserInfo(); + } + + Future _fetchUserInfo() async { + try { + final info = await _authService.getUserInfo(); + if (mounted) { + setState(() { + _userInfo = info; + }); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('정보를 불러올 수 없습니다. 잠시 후 다시 시도해주세요.')), + ); + } + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + Future _handleLogout() async { + await _authService.signOut(); + if (mounted) { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (context) => const WelcomeScreen()), + (route) => false, + ); + } + } + + Future _handleWithdraw() async { + bool? confirm = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('회원 탈퇴'), + content: const Text('정말로 탈퇴하시겠습니까?\n모든 데이터가 삭제되며 복구할 수 없습니다.'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('취소'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('탈퇴하기', style: TextStyle(color: Colors.red)), + ), + ], + ), + ); + + if (confirm == true) { + if (!mounted) return; + final success = await _authService.withdrawAccount(); + if (success && mounted) { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (context) => const WelcomeScreen()), + (route) => false, + ); + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('탈퇴 처리에 실패했습니다. 다시 시도해주세요.')), + ); + } + } + } + } + + // 통합 약관 모달 보여주기 + void _showAllTermsModal(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, // 둥근 모서리 적용을 위해 투명 + builder: (context) { + return DraggableScrollableSheet( + initialChildSize: 0.85, // 화면의 85% 높이로 시작 + minChildSize: 0.5, + maxChildSize: 0.95, + builder: (_, controller) { + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + padding: const EdgeInsets.fromLTRB(20, 10, 20, 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 핸들 바 + Center( + child: Container( + width: 40, + height: 4, + margin: const EdgeInsets.only(bottom: 20, top: 10), + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2), + ), + ), + ), + + // 헤더 + const Row( + children: [ + Icon(Icons.description, size: 24, color: Colors.black87), + SizedBox(width: 8), + Text( + '서비스 이용 약관 전체보기', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + fontFamily: 'SCDream', + ), + ), + ], + ), + const SizedBox(height: 20), + + // 스크롤 가능한 본문 (모든 약관 내용 통합) + Expanded( + child: ListView.separated( + controller: controller, // 드래그 가능한 스크롤 컨트롤러 연결 + itemCount: TermsData.terms.length, + separatorBuilder: (context, index) => const Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: Divider(color: Color(0xFFEEEEEE), thickness: 1), + ), + itemBuilder: (context, index) { + final term = TermsData.terms[index]; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + term['title']!, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFFFF7500), // 강조색 + fontFamily: 'SCDream', + ), + ), + const SizedBox(height: 10), + Text( + term['content']!, + style: const TextStyle( + fontSize: 14, + height: 1.6, + color: Colors.black87, + fontFamily: 'SCDream', + ), + ), + ], + ); + }, + ), + ), + + const SizedBox(height: 10), + // 닫기 버튼 + SizedBox( + width: double.infinity, + height: 52, + child: ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFFF7500), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + elevation: 0, + ), + child: const Text( + '닫기', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + fontFamily: 'SCDream', + ), + ), + ), + ), + ], + ), + ); + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + if (_isLoading) { + return const Scaffold(body: Center(child: CircularProgressIndicator())); + } + + return Scaffold( + backgroundColor: Colors.white, + body: _userInfo == null + ? const Center(child: Text('정보를 불러올 수 없습니다.')) + : SafeArea( + child: Column( + // Column으로 변경하여 하단 고정 영역 확보 + children: [ + Expanded( + // 상단 스크롤 영역 + child: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + const CircleAvatar( + radius: 40, + backgroundColor: Colors.grey, + child: Icon( + Icons.person, + size: 50, + color: Colors.white, + ), + ), + const SizedBox(height: 16), + Text( + _userInfo!['nickname'] ?? '이름 없음', + style: const TextStyle( + fontFamily: 'SCDream', + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + _userInfo!['email'] ?? '이메일 없음', + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 14, + color: Colors.grey[600], + ), + ), + ], + ), + ), + const SizedBox(height: 30), + _buildMenuItem( + title: '공지사항', + icon: Icons.campaign_outlined, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const NoticeScreen(), + ), + ); + }, + ), + const SizedBox(height: 10), + _buildMenuItem( + title: '서비스 이용 약관', + icon: Icons.description_outlined, + onTap: () => _showAllTermsModal(context), + ), + const SizedBox(height: 10), + _buildMenuItem( + title: '버전 정보', + icon: Icons.info_outline, + trailingText: '1.0.0', + onTap: () {}, // 클릭 효과를 위해 빈 함수 전달 + ), + // 회원 탈퇴 버튼 removed from here + ], + ), + ), + ), + + // 하단 고정 영역 (로그아웃 & 탈퇴) + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 30), + child: Column( + children: [ + // 로그아웃 버튼 (회원탈퇴 위로 이동) + _buildMenuItem( + title: '로그아웃', + icon: Icons.logout, + onTap: _handleLogout, + ), + + const SizedBox(height: 10), + + // 회원 탈퇴 버튼 + _buildMenuItem( + title: '회원 탈퇴', + icon: Icons.person_off_outlined, + isDestructive: true, + onTap: _handleWithdraw, + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildMenuItem({ + required String title, + required IconData icon, + VoidCallback? onTap, // onTap을 nullable로 변경 + bool isDestructive = false, + String? trailingText, // 뒤에 텍스트를 표시할 수 있도록 추가 + }) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey[200]!), + borderRadius: BorderRadius.circular(12), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), + child: Row( + children: [ + Icon(icon, color: isDestructive ? Colors.red : Colors.black54), + const SizedBox(width: 16), + Expanded( + child: Text( + title, + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 16, + fontWeight: FontWeight.w500, // Medium + color: isDestructive ? Colors.red : Colors.black, + ), + ), + ), + if (trailingText != null) + Text( + trailingText, + style: const TextStyle( + fontFamily: 'SCDream', + fontSize: 14, + color: Colors.grey, + fontWeight: FontWeight.bold, + ), + ) + else + Icon( + Icons.arrow_forward_ios, + size: 16, + color: isDestructive ? Colors.red : Colors.grey, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/app/lib/screens/notice_screen.dart b/app/lib/screens/notice_screen.dart new file mode 100644 index 0000000..318a0da --- /dev/null +++ b/app/lib/screens/notice_screen.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; + +class NoticeScreen extends StatelessWidget { + const NoticeScreen({super.key}); + + final List> notices = const [ + { + 'title': 'RUP 서비스 런칭 안내', + 'date': '2024.01.20', + 'content': '반려동물 통합 관리 플랫폼 RUP가 정식 런칭되었습니다. 많은 이용 부탁드립니다.', + }, + { + 'title': '시스템 점검 안내', + 'date': '2024.01.15', + 'content': '더나은 서비스를 위해 시스템 점검이 진행될 예정입니다.\n일시: 2024.01.25 02:00 ~ 04:00', + }, + { + 'title': '이용약관 개정 안내', + 'date': '2024.01.10', + 'content': '서비스 이용약관이 일부 개정되었습니다. 주요 변경사항을 확인해주세요.', + }, + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + title: const Text( + '공지사항', + style: TextStyle( + fontFamily: 'SCDream', + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + centerTitle: true, + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: Colors.black, size: 20), + onPressed: () => Navigator.pop(context), + ), + ), + body: ListView.separated( + itemCount: notices.length, + separatorBuilder: (context, index) => + const Divider(height: 1, color: Color(0xFFEEEEEE)), + itemBuilder: (context, index) { + final notice = notices[index]; + return ExpansionTile( + tilePadding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 8, + ), + title: Text( + notice['title']!, + style: const TextStyle( + fontFamily: 'SCDream', + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + ), + subtitle: Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + notice['date']!, + style: TextStyle( + fontFamily: 'SCDream', + fontSize: 12, + color: Colors.grey[500], + ), + ), + ), + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 20, + ), + color: Colors.grey[50], + child: Text( + notice['content']!, + style: const TextStyle( + fontFamily: 'SCDream', + fontSize: 14, + height: 1.5, + color: Colors.black87, + ), + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/app/lib/screens/reservation_screen.dart b/app/lib/screens/reservation_screen.dart new file mode 100644 index 0000000..e79e429 --- /dev/null +++ b/app/lib/screens/reservation_screen.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class ReservationScreen extends StatelessWidget { + const ReservationScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: const SafeArea( + child: Center( + child: Text( + '예약 조회 화면 준비 중입니다.', + style: TextStyle(fontFamily: 'SCDream'), + ), + ), + ), + ); + } +} diff --git a/app/lib/screens/shop_screen.dart b/app/lib/screens/shop_screen.dart new file mode 100644 index 0000000..f1f834c --- /dev/null +++ b/app/lib/screens/shop_screen.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class ShopScreen extends StatelessWidget { + const ShopScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: const SafeArea( + child: Center( + child: Text( + '상점 화면 준비 중입니다.', + style: TextStyle(fontFamily: 'SCDream'), + ), + ), + ), + ); + } +} diff --git a/app/lib/screens/signup_screen.dart b/app/lib/screens/signup_screen.dart index 8f8baf8..0cf1f7e 100644 --- a/app/lib/screens/signup_screen.dart +++ b/app/lib/screens/signup_screen.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../services/auth_service.dart'; -import 'home_screen.dart'; +import 'main_screen.dart'; +import 'terms_agreement_screen.dart'; class SignupScreen extends StatefulWidget { const SignupScreen({super.key}); @@ -20,15 +21,30 @@ class _SignupScreenState extends State { try { final authService = AuthService(); - final user = await authService.signInWithGoogle(); + // Returns Map? { 'credential': ..., 'isNewUser': bool } + final result = await authService.signInWithGoogle(); if (!mounted) return; - if (user != null) { - Navigator.pushReplacement( - context, - MaterialPageRoute(builder: (context) => const HomeScreen()), - ); + if (result != null) { + final isNewUser = result['isNewUser'] as bool; + + if (isNewUser) { + // 신규 가입자 -> 약관 동의 화면으로 이동 + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + TermsAgreementScreen(idToken: result['idToken']), + ), + ); + } else { + // 기존 가입자 -> 메인 화면으로 이동 + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const MainScreen()), + ); + } } else { ScaffoldMessenger.of( context, @@ -88,7 +104,7 @@ class _SignupScreenState extends State { Image.asset('assets/img/foot.png', width: 30), const SizedBox(width: 10), const Text( - '서비스 이용 약관', + '간편 로그인', style: TextStyle( fontSize: 24, fontFamily: 'SCDream', diff --git a/app/lib/screens/splash_screen.dart b/app/lib/screens/splash_screen.dart index b4d0af4..c9bdb46 100644 --- a/app/lib/screens/splash_screen.dart +++ b/app/lib/screens/splash_screen.dart @@ -1,8 +1,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; -// import 'package:video_player/video_player.dart'; import 'welcome_screen.dart'; -import 'home_screen.dart'; +import 'main_screen.dart'; +import '../services/auth_service.dart'; // Import AuthService +import '../theme/app_colors.dart'; class SplashScreen extends StatefulWidget { const SplashScreen({super.key}); @@ -12,9 +13,6 @@ class SplashScreen extends StatefulWidget { } class _SplashScreenState extends State { - // Mock login history flag - final bool hasLoginHistory = true; - @override void initState() { super.initState(); @@ -22,17 +20,22 @@ class _SplashScreenState extends State { } Future _checkLoginHistory() async { - // Simulate loading time (e.g. 2 seconds) + // Simulate loading time (minimum) await Future.delayed(const Duration(seconds: 2)); if (!mounted) return; - if (hasLoginHistory) { + final AuthService authService = AuthService(); + final String? token = await authService.getAccessToken(); + + if (token != null) { + // 토큰이 있으면 메인 화면으로 (자동 로그인) Navigator.pushReplacement( context, - MaterialPageRoute(builder: (context) => const HomeScreen()), + MaterialPageRoute(builder: (context) => const MainScreen()), ); } else { + // 토큰이 없으면 웰컴 화면으로 Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => const WelcomeScreen()), @@ -43,7 +46,7 @@ class _SplashScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0xFFFF7500), + backgroundColor: AppColors.highlight, body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/app/lib/screens/terms_agreement_screen.dart b/app/lib/screens/terms_agreement_screen.dart new file mode 100644 index 0000000..c25e3db --- /dev/null +++ b/app/lib/screens/terms_agreement_screen.dart @@ -0,0 +1,404 @@ +import 'package:flutter/material.dart'; +import 'identity_verification_screen.dart'; +import '../services/auth_service.dart'; // Import AuthService +import '../data/terms_data.dart'; + +class TermsAgreementScreen extends StatefulWidget { + final bool isViewOnly; + final String? idToken; + + const TermsAgreementScreen({ + super.key, + this.isViewOnly = false, + this.idToken, + }); + + @override + State createState() => _TermsAgreementScreenState(); +} + +class _TermsAgreementScreenState extends State { + final List _checks = [false, false, false, false, false, false]; + + bool get _isAllChecked => _checks.every((completed) => completed); + + bool get _isRequiredChecked => + _checks[0] && _checks[1] && _checks[2] && _checks[3]; + + void _toggleAll() { + setState(() { + bool newValue = !_isAllChecked; + for (int i = 0; i < _checks.length; i++) { + _checks[i] = newValue; + } + }); + } + + void _toggleItem(int index) { + setState(() { + _checks[index] = !_checks[index]; + }); + } + + String _getTermContent(int index) { + if (index >= 0 && index < TermsData.terms.length) { + return TermsData.terms[index]['content'] ?? '내용이 없습니다.'; + } + return '내용이 없습니다.'; + } + + void _showTermDetail(BuildContext context, String title, int index) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) { + return DraggableScrollableSheet( + initialChildSize: 0.7, + minChildSize: 0.5, + maxChildSize: 0.9, + builder: (_, controller) { + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2), + ), + ), + ), + const SizedBox(height: 20), + Text( + title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + fontFamily: 'SCDream', + ), + ), + const SizedBox(height: 20), + Expanded( + child: SingleChildScrollView( + controller: controller, + child: Text( + _getTermContent(index), + style: const TextStyle( + fontSize: 15, + height: 1.6, + fontFamily: 'SCDream', + color: Colors.black87, + ), + ), + ), + ), + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + height: 52, + child: ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFFF7500), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + elevation: 0, + ), + child: const Text( + '확인', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + fontFamily: 'SCDream', + ), + ), + ), + ), + ], + ), + ); + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: Colors.black, size: 20), + onPressed: () => Navigator.pop(context), + ), + title: Text( + widget.isViewOnly ? '이용 약관' : '회원가입', + style: const TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w600, + fontFamily: 'SCDream', + ), + ), + centerTitle: true, + ), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + // Header Area + Row( + children: [ + Image.asset( + 'assets/img/foot.png', + width: 24, + height: 24, + ), + const SizedBox(width: 8), + const Text( + '서비스 이용 약관', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + fontFamily: 'SCDream', + color: Colors.black, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + widget.isViewOnly + ? 'RUP 서비스의 이용 약관 내용입니다.' + : '서비스 이용을 위해 약관에 동의해주세요.', + style: const TextStyle( + fontSize: 15, + color: Colors.black54, + fontFamily: 'SCDream', + ), + ), + const SizedBox(height: 30), + + // All Agree Box - *ViewOnly 모드에서는 숨김* + if (!widget.isViewOnly) ...[ + InkWell( + onTap: () => _toggleAll(), + borderRadius: BorderRadius.circular(8), + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 16, + ), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon( + _isAllChecked + ? Icons.radio_button_checked + : Icons.radio_button_off, + color: _isAllChecked + ? const Color(0xFFFF7500) + : Colors.grey, + ), + const SizedBox(width: 12), + const Text( + '모든 약관에 동의합니다.', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + fontFamily: 'SCDream', + ), + ), + ], + ), + ), + ), + const SizedBox(height: 20), + const Divider(height: 1, color: Color(0xFFEEEEEE)), + const SizedBox(height: 10), + ], + + // Individual Items + _buildTermItem(0, '이용약관 동의', isRequired: true), + _buildTermItem(1, '개인정보 수집 및 이용 동의', isRequired: true), + _buildTermItem(2, '제 3자 제공 동의', isRequired: true), + _buildTermItem(3, '만 14세 이상 사용자', isRequired: true), + _buildTermItem(4, '위치정보 이용 동의', isRequired: false), + _buildTermItem(5, '마케팅 수신 동의', isRequired: false), + ], + ), + ), + ), + + // Bottom Button + Padding( + padding: const EdgeInsets.fromLTRB(20, 10, 20, 20), + child: SizedBox( + width: double.infinity, + height: 52, + child: ElevatedButton( + onPressed: widget.isViewOnly + ? () => Navigator.pop(context) + : (_isRequiredChecked + ? () async { + if (widget.idToken != null) { + // 실제 가입 요청 (DB 생성) + final success = await AuthService() + .registerWithGoogle(widget.idToken!); + if (success && context.mounted) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const IdentityVerificationScreen(), + ), + ); + } else if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('회원가입 처리에 실패했습니다.'), + ), + ); + } + } else { + // idToken이 없는 경우 (테스트 등) - 그냥 진행 + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const IdentityVerificationScreen(), + ), + ); + } + } + : null), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFFF7500), + disabledBackgroundColor: Colors.grey[300], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + elevation: 0, + ), + child: Text( + widget.isViewOnly ? '닫기' : '동의하고 본인 인증하기', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + fontFamily: 'SCDream', + ), + ), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildTermItem(int index, String title, {required bool isRequired}) { + // ViewOnly 모드에서는 체크박스 숨김 + // ViewOnly 모드에서는 텍스트 클릭 시 모달 띄우기 (토글 X) + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + Expanded( + child: InkWell( + onTap: () { + if (!widget.isViewOnly) { + _toggleItem(index); + } else { + // ViewOnly일 땐 텍스트 눌러도 모달 띄우기 (편의성) + _showTermDetail(context, title, index); + } + }, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 8, + ), + child: Row( + children: [ + // 체크 Icon - *ViewOnly 모드에서는 숨김* + if (!widget.isViewOnly) ...[ + Icon( + Icons.check, + size: 20, + color: _checks[index] + ? const Color(0xFFFF7500) + : Colors.grey[300], + ), + const SizedBox(width: 12), + ], + + Text( + isRequired ? '필수' : '선택', + style: TextStyle( + color: isRequired + ? const Color(0xFFFF7500) + : Colors.grey, + fontWeight: FontWeight.bold, + fontSize: 14, + fontFamily: 'SCDream', + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + title, + style: const TextStyle( + fontSize: 15, + color: Colors.black87, + fontFamily: 'SCDream', + ), + ), + ), + ], + ), + ), + ), + ), + IconButton( + onPressed: () => _showTermDetail(context, title, index), + icon: const Icon( + Icons.arrow_forward_ios, + size: 14, + color: Colors.black, + ), + splashRadius: 20, + padding: const EdgeInsets.all(12), + constraints: const BoxConstraints(), + ), + ], + ), + ); + } +} diff --git a/app/lib/services/auth_service.dart b/app/lib/services/auth_service.dart index d98aaff..ed38488 100644 --- a/app/lib/services/auth_service.dart +++ b/app/lib/services/auth_service.dart @@ -1,51 +1,256 @@ import 'package:firebase_auth/firebase_auth.dart'; import 'package:google_sign_in/google_sign_in.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter/material.dart'; +import '../main.dart'; +import '../screens/welcome_screen.dart'; +import '../utils/log_manager.dart'; class AuthService { final FirebaseAuth _auth = FirebaseAuth.instance; - final GoogleSignIn _googleSignIn = GoogleSignIn(); + // Google Sign-In 설정 (Backend와 통신하기 위해 serverClientId 지정) + final GoogleSignIn _googleSignIn = GoogleSignIn( + serverClientId: + '379988243470-g6490l8gucc3ljras93i28c3l4qlroi4.apps.googleusercontent.com', + ); + final Dio _dio = Dio(); + final FlutterSecureStorage _storage = const FlutterSecureStorage(); - // 구글 로그인 - Future signInWithGoogle() async { - // 1. 구글 로그인 흐름 시작 - print('[DEBUG] Google Sign-In: Starting signIn()'); - final GoogleSignInAccount? googleUser = await _googleSignIn.signIn(); - print( - '[DEBUG] Google Sign-In: signIn() completed, user: ${googleUser?.email}', + // Backend Base URL + final String _baseUrl = 'http://10.0.2.2:3000/auth'; + + AuthService() { + // Dio Options 설정 (Timeout 추가) + _dio.options.connectTimeout = const Duration(seconds: 5); // 5초 연결 타임아웃 + _dio.options.receiveTimeout = const Duration(seconds: 5); // 5초 응답 타임아웃 + + // Dio Interceptor 설정 + _dio.interceptors.add( + InterceptorsWrapper( + onRequest: (options, handler) async { + // 모든 요청 헤더에 Access Token 추가 + final accessToken = await _storage.read(key: 'accessToken'); + if (accessToken != null) { + options.headers['Authorization'] = 'Bearer $accessToken'; + } + return handler.next(options); + }, + onError: (DioException error, handler) async { + // 401 에러 발생 시 (Access Token 만료) + if (error.response?.statusCode == 401) { + String msg1 = '[Auth] Access Token expired. Attempting refresh...'; + print(msg1); + LogManager().addLog(msg1); // LOG + + final isRefreshed = await _refreshToken(); + if (isRefreshed) { + // 토큰 갱신 성공 -> 원래 요청 재시도 + final newAccessToken = await _storage.read(key: 'accessToken'); + + // 헤더 업데이트 + error.requestOptions.headers['Authorization'] = + 'Bearer $newAccessToken'; + + // 재요청 + try { + final response = await _dio.fetch(error.requestOptions); + return handler.resolve(response); + } catch (e) { + return handler.reject(error); + } + } else { + // 토큰 갱신 실패 (리프레시 토큰도 만료됨) -> 로그아웃 & 화면 이동 + String msg2 = '[Auth] Refresh Token expired. Logging out...'; + print(msg2); + LogManager().addLog(msg2); // LOG + await signOut(); + + // Force Navigation to Welcome Screen + navigatorKey.currentState?.pushAndRemoveUntil( + MaterialPageRoute(builder: (context) => const WelcomeScreen()), + (route) => false, + ); + } + } else { + // Log other errors + LogManager().addLog( + '[DioError] ${error.message} (Status: ${error.response?.statusCode})', + ); + } + return handler.next(error); + }, + ), ); + } - if (googleUser == null) { - // 사용자가 로그인 창을 닫음 - print('[DEBUG] Google Sign-In: User canceled'); + // 토큰 갱신 로직 + Future _refreshToken() async { + try { + final refreshToken = await _storage.read(key: 'refreshToken'); + if (refreshToken == null) return false; + + final response = await _dio.post( + '$_baseUrl/refresh', + data: {'refreshToken': refreshToken}, + ); + + if (response.statusCode == 200 && response.data['success'] == true) { + final newAccessToken = response.data['accessToken']; + await _storage.write(key: 'accessToken', value: newAccessToken); + String msg = '[Auth] Token refreshed successfully.'; + print(msg); + LogManager().addLog(msg); + return true; + } + return false; + } catch (e) { + String msg = '[Auth] Token refresh failed: $e'; + print(msg); + LogManager().addLog(msg); + return false; + } + } + + // 구글 로그인 (Check or Login) + Future?> signInWithGoogle() async { + try { + LogManager().addLog('[DEBUG] Google Sign-In: Starting signIn()'); + final GoogleSignInAccount? googleUser = await _googleSignIn.signIn(); + + if (googleUser == null) { + LogManager().addLog('[DEBUG] Google Sign-In: User canceled'); + return null; + } + + final GoogleSignInAuthentication googleAuth = + await googleUser.authentication; + + if (googleAuth.idToken != null) { + try { + // 1. 서버에 토큰 전송 (Login Check) + final response = await _dio.post( + '$_baseUrl/google', + data: {'idToken': googleAuth.idToken}, + ); + + if (response.statusCode == 200 && response.data['success'] == true) { + final isNewUser = response.data['isNewUser'] ?? false; + + if (isNewUser) { + // 신규 유저: 토큰 저장하지 않고 idToken 반환 (약관 동의 화면으로 전달) + return { + 'isNewUser': true, + 'idToken': googleAuth.idToken, + 'email': response.data['email'], + 'nickname': response.data['nickname'], + }; + } else { + // 기존 유저: 토큰 저장 및 로그인 처리 + final accessToken = response.data['accessToken']; + final refreshToken = response.data['refreshToken']; + + await _storage.write(key: 'accessToken', value: accessToken); + await _storage.write(key: 'refreshToken', value: refreshToken); + + // Firebase 로그인 (선택 사항, 필요하다면 유지) + final OAuthCredential credential = GoogleAuthProvider.credential( + accessToken: googleAuth.accessToken, + idToken: googleAuth.idToken, + ); + await _auth.signInWithCredential(credential); + + return {'isNewUser': false}; + } + } + } catch (e) { + String msg = '[ERROR] Backend Auth API Error: $e'; + print(msg); + LogManager().addLog(msg); + } + } + return null; + } catch (e) { + String msg = '[ERROR] Google Sign-In Error: $e'; + print(msg); + LogManager().addLog(msg); return null; } + } - // 2. 인증 세부 정보 요청 - print('[DEBUG] Google Sign-In: Getting authentication...'); - final GoogleSignInAuthentication googleAuth = - await googleUser.authentication; - print( - '[DEBUG] Google Sign-In: Authentication received. AccessToken: ${googleAuth.accessToken != null}, IDToken: ${googleAuth.idToken != null}', - ); + // 구글 회원가입 (Register - 약관 동의 후 호출) + Future registerWithGoogle(String idToken) async { + try { + final response = await _dio.post( + '$_baseUrl/google/register', + data: {'idToken': idToken}, + ); - // 3. 자격 증명 생성 - final OAuthCredential credential = GoogleAuthProvider.credential( - accessToken: googleAuth.accessToken, - idToken: googleAuth.idToken, - ); + if (response.statusCode == 200 && response.data['success'] == true) { + final accessToken = response.data['accessToken']; + final refreshToken = response.data['refreshToken']; - // 4. Firebase에 로그인 - print('[DEBUG] Google Sign-In: Signing in with credential to Firebase...'); - final userCredential = await _auth.signInWithCredential(credential); - print( - '[DEBUG] Google Sign-In: Firebase sign-in completed. User: ${userCredential.user?.uid}', - ); - return userCredential; + await _storage.write(key: 'accessToken', value: accessToken); + await _storage.write(key: 'refreshToken', value: refreshToken); + + // Firebase 로그인 처리 (필요시) - idToken으로 credential 생성 가능하지만 access token이 없으므로 생략하거나 + // 이미 signInWithGoogle에서 받아온 credential을 재사용할 수 있으면 좋음. + // 하지만 여기서는 백엔드 세션이 중요하므로 일단 백엔드 토큰만 저장. + // (Firebase Auth와 Custom Backend Auth를 혼용 중이라 복잡함. + // 일단 백엔드 로직이 메인이므로 백엔드 토큰 처리에 집중) + + return true; + } + return false; + } catch (e) { + LogManager().addLog('[Auth] Register Failed: $e'); + return false; + } } // 로그아웃 Future signOut() async { await _googleSignIn.signOut(); await _auth.signOut(); + await _storage.deleteAll(); // 토큰 삭제 + print('[DEBUG] User signed out and tokens cleared.'); } + + // Access Token 가져오기 + Future getAccessToken() async { + return await _storage.read(key: 'accessToken'); + } + + // 유저 정보 가져오기 + Future?> getUserInfo() async { + try { + final response = await _dio.get('$_baseUrl/me'); + if (response.statusCode == 200 && response.data['success'] == true) { + return response.data['user']; + } + return null; + } catch (e) { + LogManager().addLog('[Auth] Get User Info Failed: $e'); + return null; + } + } + + // 회원 탈퇴 + Future withdrawAccount() async { + try { + final response = await _dio.delete('$_baseUrl/withdraw'); + if (response.statusCode == 200 && response.data['success'] == true) { + await _googleSignIn.signOut(); + await _auth.signOut(); + await _storage.deleteAll(); + return true; + } + return false; + } catch (e) { + LogManager().addLog('[Auth] Withdraw Account Failed: $e'); + return false; + } + } + + Dio get dio => _dio; } diff --git a/app/lib/theme/app_colors.dart b/app/lib/theme/app_colors.dart new file mode 100644 index 0000000..21111c3 --- /dev/null +++ b/app/lib/theme/app_colors.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class AppColors { + // 기본 텍스트 색상: #1F1F1F (기존 '기본색') + static const Color text = Color(0xFF1F1F1F); + + // (Old alias for compatibility, if needed) + static const Color basic = text; + + // 비활성화색: #C8C8C8 + static const Color inactive = Color(0xFFC8C8C8); + + // 강조색: #FF7500 (버튼, 호버 등 중요!) + static const Color highlight = Color(0xFFFF7500); +} diff --git a/app/lib/utils/log_manager.dart b/app/lib/utils/log_manager.dart new file mode 100644 index 0000000..e0a15f9 --- /dev/null +++ b/app/lib/utils/log_manager.dart @@ -0,0 +1,24 @@ +import 'package:flutter/foundation.dart'; + +class LogManager { + static final LogManager _instance = LogManager._internal(); + factory LogManager() => _instance; + LogManager._internal(); + + final ValueNotifier> logs = ValueNotifier([]); + + void addLog(String message) { + try { + final timestamp = DateTime.now().toString().split(' ')[1].split('.')[0]; + final logMessage = "[$timestamp] $message"; + + logs.value = [logMessage, ...logs.value]; + } catch (e) { + print('LogManager Error: $e'); + } + } + + void clear() { + logs.value = []; + } +} diff --git a/app/linux/flutter/generated_plugin_registrant.cc b/app/linux/flutter/generated_plugin_registrant.cc index e71a16d..d0e7f79 100644 --- a/app/linux/flutter/generated_plugin_registrant.cc +++ b/app/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); } diff --git a/app/linux/flutter/generated_plugins.cmake b/app/linux/flutter/generated_plugins.cmake index 2e1de87..b29e9ba 100644 --- a/app/linux/flutter/generated_plugins.cmake +++ b/app/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/app/macos/Flutter/GeneratedPluginRegistrant.swift b/app/macos/Flutter/GeneratedPluginRegistrant.swift index 3e80842..f824f2f 100644 --- a/app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,12 +7,14 @@ import Foundation import firebase_auth import firebase_core +import flutter_secure_storage_macos import google_sign_in_ios import video_player_avfoundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) } diff --git a/app/pubspec.lock b/app/pubspec.lock index 3ad0580..3709a16 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -49,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" collection: dependency: transitive description: @@ -57,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" csslib: dependency: transitive description: @@ -73,6 +89,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dio: + dependency: "direct main" + description: + name: dio + sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + url: "https://pub.dev" + source: hosted + version: "5.9.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" fake_async: dependency: transitive description: @@ -81,6 +113,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c + url: "https://pub.dev" + source: hosted + version: "2.1.5" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" firebase_auth: dependency: "direct main" description: @@ -142,6 +190,54 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + url: "https://pub.dev" + source: hosted + version: "9.2.4" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" flutter_svg: dependency: "direct main" description: @@ -160,6 +256,14 @@ packages: description: flutter source: sdk version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" google_identity_services_web: dependency: transitive description: @@ -208,6 +312,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.12.4+4" + hooks: + dependency: transitive + description: + name: hooks + sha256: "5d309c86e7ce34cd8e37aa71cb30cb652d3829b900ab145e4d9da564b31d59f7" + url: "https://pub.dev" + source: hosted + version: "1.0.0" html: dependency: transitive description: @@ -232,6 +344,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" leak_tracker: dependency: transitive description: @@ -264,6 +384,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" matcher: dependency: transitive description: @@ -288,6 +416,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" + url: "https://pub.dev" + source: hosted + version: "0.17.4" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "9922a1ad59ac5afb154cc948aa6ded01987a75003651d0a2866afc23f4da624e" + url: "https://pub.dev" + source: hosted + version: "9.2.3" path: dependency: transitive description: @@ -304,6 +456,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" petitparser: dependency: transitive description: @@ -312,6 +512,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -320,6 +528,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" sky_engine: dependency: transitive description: flutter @@ -469,6 +685,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" xml: dependency: transitive description: @@ -477,6 +709,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: dart: ">=3.10.7 <4.0.0" - flutter: ">=3.38.0" + flutter: ">=3.38.4" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 42b3efe..8a0bb07 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -39,6 +39,8 @@ dependencies: firebase_core: ^3.0.0 # 파이어베이스 기본 (Updated) firebase_auth: ^5.0.0 # 인증 기능 (Updated) google_sign_in: ^6.2.1 # 구글 로그인 UI/기능 (Updated) + dio: ^5.4.0 + flutter_secure_storage: ^9.0.0 dev_dependencies: flutter_test: @@ -66,6 +68,8 @@ flutter: weight: 700 - asset: assets/fonts/SCDream-medium.otf weight: 500 + - asset: assets/fonts/SCDream-regular.otf + weight: 400 assets: - assets/img/ diff --git a/app/test/widget_test.dart b/app/test/widget_test.dart index 4e2a713..7d6fc73 100644 --- a/app/test/widget_test.dart +++ b/app/test/widget_test.dart @@ -11,20 +11,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:app/main.dart'; void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { + testWidgets('App starts with Splash Screen', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); + await tester.pumpWidget(const RupApp()); - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); + // Verify that Splash Screen is shown + expect(find.byType(MaterialApp), findsOneWidget); }); } diff --git a/app/windows/flutter/generated_plugin_registrant.cc b/app/windows/flutter/generated_plugin_registrant.cc index d141b74..ea9741e 100644 --- a/app/windows/flutter/generated_plugin_registrant.cc +++ b/app/windows/flutter/generated_plugin_registrant.cc @@ -8,10 +8,13 @@ #include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FirebaseAuthPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); } diff --git a/app/windows/flutter/generated_plugins.cmake b/app/windows/flutter/generated_plugins.cmake index 29944d5..b8ca912 100644 --- a/app/windows/flutter/generated_plugins.cmake +++ b/app/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST firebase_auth firebase_core + flutter_secure_storage_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..c81b8d3 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,3 @@ +node_modules +npm-debug.log +.env diff --git a/backend/config/db.js b/backend/config/db.js new file mode 100644 index 0000000..5fdebf3 --- /dev/null +++ b/backend/config/db.js @@ -0,0 +1,40 @@ +const { Sequelize } = require('sequelize'); +require('dotenv').config(); + +const sequelize = new Sequelize( + process.env.MYSQL_DATABASE, + process.env.MYSQL_USER, + process.env.MYSQL_PASSWORD, + { + host: process.env.DB_HOST, + dialect: 'mysql', + logging: false, + pool: { + max: 5, + min: 0, + acquire: 30000, + idle: 10000 + } + } +); + +// Test Connection +const connectDB = async () => { + let retries = 20; // Increased retries for slow DB startup + while (retries > 0) { + try { + console.log(`[Database] Attempting to connect... (Retries left: ${retries})`); + await sequelize.authenticate(); + console.log('[Database] Connection has been established successfully.'); + return; + } catch (error) { + console.error(`[Database] Unable to connect. Retrying in 5s...`, error.message); + retries -= 1; + await new Promise(res => setTimeout(res, 5000)); // Wait 5 seconds + } + } + console.error('[Database] Failed to connect after multiple attempts.'); + process.exit(1); +}; + +module.exports = { sequelize, connectDB }; diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js new file mode 100644 index 0000000..6049ff6 --- /dev/null +++ b/backend/controllers/authController.js @@ -0,0 +1,285 @@ +const { OAuth2Client } = require('google-auth-library'); +const jwt = require('jsonwebtoken'); +const User = require('../models/user'); +require('dotenv').config(); + +// Google Client ID should be in env, but for now assuming it handles verification +const googleClient = new OAuth2Client(process.env.GOOGLE_CLIENT_ID); + +const generateTokens = (user) => { + const payload = { id: user.id, email: user.email, nickname: user.nickname }; + + const accessToken = jwt.sign(payload, process.env.JWT_SECRET, { + expiresIn: '1h', + }); + + const refreshToken = jwt.sign({ id: user.id }, process.env.JWT_REFRESH_SECRET, { + expiresIn: '14d', + }); + + return { accessToken, refreshToken }; +}; + +// Generic Social Login Logic +const socialLogin = async (provider, socialInfo, res) => { + try { + const { socialId, email, nickname } = socialInfo; + + // Find or Create User + const [user, created] = await User.findOrCreate({ + where: { provider, socialId }, + defaults: { email, nickname }, + }); + + if (!created) { + // Update info if needed (e.g., changed nickname on social side) - Optional + user.email = email || user.email; + user.nickname = nickname || user.nickname; + } + + // Generate Tokens + const { accessToken, refreshToken } = generateTokens(user); + + // Save Refresh Token to DB + user.refreshToken = refreshToken; + await user.save(); + + console.log(`[Auth] User ${user.id} logged in via ${provider}`); + + return res.status(200).json({ + success: true, + accessToken, + refreshToken, + isNewUser: created, // 신규 가입 여부 + user: { + id: user.id, + email: user.email, + nickname: user.nickname, + }, + }); + } catch (error) { + console.error('[Auth Error]', error); + return res.status(500).json({ success: false, message: 'Internal Server Error' }); + } +}; + +// Google Login Handler +// Google Login Handler (Check only, or Login if exists) +exports.loginWithGoogle = async (req, res) => { + const { idToken } = req.body; + + if (!idToken) { + return res.status(400).json({ success: false, message: 'Missing idToken' }); + } + + try { + // Verify Google Token + const ticket = await googleClient.verifyIdToken({ + idToken, + audience: [ + process.env.GOOGLE_CLIENT_ID, + process.env.GOOGLE_ANDROID_CLIENT_ID + ], + }); + + const payload = ticket.getPayload(); + const socialId = payload.sub; + + // Check if user exists + const user = await User.findOne({ where: { provider: 'google', socialId } }); + + if (user) { + // User exists -> Login + const { accessToken, refreshToken } = generateTokens(user); + user.refreshToken = refreshToken; + await user.save(); + + console.log(`[Auth] User ${user.id} logged in via google`); + return res.status(200).json({ + success: true, + accessToken, + refreshToken, + isNewUser: false, + user: { + id: user.id, + email: user.email, + nickname: user.nickname, + }, + }); + } else { + // User does not exist -> Return isNewUser: true (Do NOT create yet) + return res.status(200).json({ + success: true, + isNewUser: true, + // Optional: Return partial info if needed for UI (e.g. pre-filling name) + email: payload.email, + nickname: payload.name, + }); + } + + } catch (error) { + console.error('[Google Verify Error]', error); + return res.status(401).json({ + success: false, + message: 'Invalid Google Token', + debug: error.message + }); + } +}; + +// Google Register Handler (Create User) +exports.registerWithGoogle = async (req, res) => { + const { idToken } = req.body; + + if (!idToken) { + return res.status(400).json({ success: false, message: 'Missing idToken' }); + } + + try { + // Verify Google Token again + const ticket = await googleClient.verifyIdToken({ + idToken, + audience: [ + process.env.GOOGLE_CLIENT_ID, + process.env.GOOGLE_ANDROID_CLIENT_ID + ], + }); + + const payload = ticket.getPayload(); + const socialId = payload.sub; + const email = payload.email; + const nickname = payload.name; + + // Find or Create User + const [user, created] = await User.findOrCreate({ + where: { provider: 'google', socialId }, + defaults: { email, nickname }, + }); + + // If existing user calls register, just log them in (idempotent) + const { accessToken, refreshToken } = generateTokens(user); + user.refreshToken = refreshToken; + await user.save(); + + console.log(`[Auth] User ${user.id} registered/logged in via google`); + + return res.status(200).json({ + success: true, + accessToken, + refreshToken, + isNewUser: created, + user: { + id: user.id, + email: user.email, + nickname: user.nickname, + }, + }); + + } catch (error) { + console.error('[Google Register Error]', error); + return res.status(401).json({ + success: false, + message: 'Invalid Google Token', + debug: error.message + }); + } +}; + +// Test Login Handler (For verification only) +exports.testLogin = async (req, res) => { + const { email, nickname } = req.body; + + if (!email || !nickname) { + return res.status(400).json({ success: false, message: 'Missing email or nickname' }); + } + + // Use 'google' as provider to satisfy DB Enum constraint + const socialId = `test_${email}`; + + return socialLogin('google', { socialId, email, nickname }, res); +}; + +// Refresh Token Handler +exports.refreshToken = async (req, res) => { + const { refreshToken } = req.body; + + if (!refreshToken) { + return res.status(400).json({ success: false, message: 'Refresh Token required' }); + } + + try { + const secret = process.env.JWT_REFRESH_SECRET; + // 1. Verify Refresh Token + const decoded = jwt.verify(refreshToken, secret); + + // 2. Check DB + const user = await User.findByPk(decoded.id); + if (!user || user.refreshToken !== refreshToken) { + return res.status(403).json({ success: false, message: 'Invalid Refresh Token' }); + } + + // 3. Issue new Access Token + const payload = { id: user.id, email: user.email, nickname: user.nickname }; + const newAccessToken = jwt.sign(payload, process.env.JWT_SECRET, { + expiresIn: '1h', + }); + + console.log(`[Auth] Access Token refreshed for User ${user.id}`); + + return res.status(200).json({ + success: true, + accessToken: newAccessToken, + }); + + } catch (error) { + console.error('[Refresh Error]', error); + if (error.name === 'TokenExpiredError') { + return res.status(403).json({ success: false, message: 'Refresh Token expired' }); + } + return res.status(403).json({ success: false, message: 'Invalid Refresh Token' }); + } +}; + +// Get User Info +exports.getMe = async (req, res) => { + try { + // req.user is set by middleware + const user = await User.findByPk(req.user.id); + if (!user) { + return res.status(404).json({ success: false, message: 'User not found' }); + } + + return res.status(200).json({ + success: true, + user: { + id: user.id, + email: user.email, + nickname: user.nickname, + }, + }); + } catch (error) { + console.error('[GetMe Error]', error); + return res.status(500).json({ success: false, message: 'Internal Server Error' }); + } +}; + +// Withdraw (Delete Account) +exports.withdraw = async (req, res) => { + try { + const userId = req.user.id; + const user = await User.findByPk(userId); + + if (!user) { + return res.status(404).json({ success: false, message: 'User not found' }); + } + + // Hard Delete + await user.destroy(); + + console.log(`[Auth] User ${userId} withdrew from the service.`); + return res.status(200).json({ success: true, message: 'Account deleted successfully' }); + } catch (error) { + console.error('[Withdraw Error]', error); + return res.status(500).json({ success: false, message: 'Internal Server Error' }); + } +}; diff --git a/backend/dockerfile b/backend/dockerfile index 350ec29..1ba34d7 100644 --- a/backend/dockerfile +++ b/backend/dockerfile @@ -2,7 +2,6 @@ FROM node:24-alpine WORKDIR /app -# 패키지 파일 복사 # 패키지 파일 복사 COPY package*.json ./ diff --git a/backend/index.js b/backend/index.js index ab6d938..9a7eb05 100644 --- a/backend/index.js +++ b/backend/index.js @@ -1,11 +1,34 @@ const express = require('express'); +const cors = require('cors'); +const { connectDB, sequelize } = require('./config/db'); +const authRoutes = require('./routes/auth'); + const app = express(); const port = 3000; +// Middleware +app.use(cors()); +app.use(express.json()); // Body parser for JSON + +// Routes +app.use('/auth', authRoutes); + app.get('/', (req, res) => { res.send('Hello from Express Backend!'); }); -app.listen(port, '0.0.0.0', () => { - console.log(`Backend app listening on port ${port}`); -}); +// Database Connection & Server Start +const startServer = async () => { + await connectDB(); + + // Sync models (in production, use migration instead of sync({alter: true})) + // For dev: force: false to keep data, alter: true to update schema + await sequelize.sync({ alter: true }); + console.log('Database synced'); + + app.listen(port, '0.0.0.0', () => { + console.log(`Backend app listening on port ${port}`); + }); +}; + +startServer(); diff --git a/backend/middleware/authMiddleware.js b/backend/middleware/authMiddleware.js new file mode 100644 index 0000000..14b76e0 --- /dev/null +++ b/backend/middleware/authMiddleware.js @@ -0,0 +1,26 @@ +const jwt = require('jsonwebtoken'); +require('dotenv').config(); + +exports.verifyToken = (req, res, next) => { + const authHeader = req.headers.authorization; + if (!authHeader) { + return res.status(401).json({ success: false, message: 'No token provided' }); + } + + const token = authHeader.split(' ')[1]; // "Bearer " + if (!token) { + return res.status(401).json({ success: false, message: 'Invalid token format' }); + } + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + req.user = decoded; // { id, email, nickname, ... } + next(); + } catch (error) { + console.error('[Auth Middleware] Token Verification Failed:', error.message); + if (error.name === 'TokenExpiredError') { + return res.status(401).json({ success: false, message: 'Token expired' }); + } + return res.status(401).json({ success: false, message: 'Invalid token' }); + } +}; diff --git a/backend/models/user.js b/backend/models/user.js new file mode 100644 index 0000000..f83ca43 --- /dev/null +++ b/backend/models/user.js @@ -0,0 +1,46 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../config/db'); + +const User = sequelize.define('User', { + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + email: { + type: DataTypes.STRING, + allowNull: true, + validate: { + isEmail: true, + }, + }, + nickname: { + type: DataTypes.STRING, + allowNull: true, + }, + provider: { + type: DataTypes.ENUM('google', 'naver', 'kakao'), + allowNull: false, + comment: 'Social login provider', + }, + socialId: { + type: DataTypes.STRING, + allowNull: false, + comment: 'Unique ID from the social provider', + }, + refreshToken: { + type: DataTypes.TEXT, + allowNull: true, + comment: 'Generic refresh token for valid session', + }, +}, { + indexes: [ + { + unique: true, + fields: ['provider', 'socialId'], // Prevent duplicate users for same provider + }, + ], + timestamps: true, +}); + +module.exports = User; diff --git a/backend/package-lock.json b/backend/package-lock.json index dfb18f1..c57678a 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,6 +1,1543 @@ { - "name": "backend", + "name": "rup-backend", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, - "packages": {} + "packages": { + "": { + "name": "rup-backend", + "version": "1.0.0", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "google-auth-library": "^9.4.1", + "jsonwebtoken": "^9.0.2", + "mysql2": "^3.6.5", + "sequelize": "^6.35.1" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz", + "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru.min": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.3.tgz", + "integrity": "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.16.1.tgz", + "integrity": "sha512-b75qsDB3ieYEzMsT1uRGsztM/sy6vWPY40uPZlVVl8eefAotFCoS7jaDB5DxDNtlW5kdVGd9jptSpkvujNxI2A==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.6", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pg-connection-string": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.10.1.tgz", + "integrity": "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/retry-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", + "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/sequelize": { + "version": "6.37.7", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz", + "integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/sequelize/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sequelize/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/sequelize/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==", + "license": "MIT" + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + } + } } diff --git a/backend/package.json b/backend/package.json index 73751f5..432318d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,6 +7,12 @@ "start": "node index.js" }, "dependencies": { - "express": "^4.18.2" + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "google-auth-library": "^9.4.1", + "jsonwebtoken": "^9.0.2", + "mysql2": "^3.6.5", + "sequelize": "^6.35.1" } -} +} \ No newline at end of file diff --git a/backend/routes/auth.js b/backend/routes/auth.js new file mode 100644 index 0000000..ee254ad --- /dev/null +++ b/backend/routes/auth.js @@ -0,0 +1,23 @@ +const express = require('express'); +const router = express.Router(); +const authController = require('../controllers/authController'); + +router.post('/google', authController.loginWithGoogle); +router.post('/google/register', authController.registerWithGoogle); // Add register route +router.post('/test-login', authController.testLogin); +router.post('/refresh', authController.refreshToken); + +const verifyToken = require('../middleware/authMiddleware').verifyToken; // Correct import + +router.get('/test-protected', verifyToken, (req, res) => { + res.json({ success: true, message: 'You have a valid token', user: req.user }); +}); + +router.get('/me', verifyToken, authController.getMe); +router.delete('/withdraw', verifyToken, authController.withdraw); + +// Future place for: +// router.post('/kakao', authController.loginWithKakao); +// router.post('/naver', authController.loginWithNaver); + +module.exports = router; diff --git a/docker-compose.yml b/docker-compose.yml index 2cdc9c0..53fae74 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: "3.8" + services: db: @@ -27,6 +27,7 @@ services: restart: always ports: - "3000:3000" + env_file: - ./backend/.env.production depends_on: diff --git a/reinstall.bat b/reinstall.bat new file mode 100644 index 0000000..95127e9 --- /dev/null +++ b/reinstall.bat @@ -0,0 +1,23 @@ +@echo off +cd /d "%~dp0" +echo ============================================== +echo [RUP Project] Cleaning and Rebuilding Backend +echo ============================================== + +echo 1. Stopping Docker services... +docker compose down + +echo. +echo 2. Forcing removal of old backend image (to clear cache)... +docker image rm -f rup-backend + +echo. +echo 3. Rebuilding Docker containers... +docker compose up -d --build + +echo. +echo 4. Checking logs (Press Ctrl+C to exit log view)... +echo ============================================== +docker compose logs -f backend + +pause