diff --git a/apisix/config.yaml b/apisix/config.yaml index 01e75e4..93594e5 100644 --- a/apisix/config.yaml +++ b/apisix/config.yaml @@ -49,7 +49,6 @@ plugins: - limit-count - limit-req - node-status - - oauth - prometheus - proxy-cache - proxy-mirror diff --git a/oauth/backend/app/api/v1/endpoints/__init__.py b/oauth/backend/app/api/v1/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oauth/backend/app/api/v1/endpoints/admin.py b/oauth/backend/app/api/v1/endpoints/admin.py new file mode 100644 index 0000000..7395c7f --- /dev/null +++ b/oauth/backend/app/api/v1/endpoints/admin.py @@ -0,0 +1,25 @@ +from fastapi import APIRouter, Depends, HTTPException +from typing import List +from app.core.security import get_current_user, require_admin +from app.models.user import User + +router = APIRouter() + +@router.get("/users", dependencies=[Depends(require_admin)]) +async def get_all_users(): + # TODO: Implement get all users logic + return {"users": []} + +@router.get("/stats", dependencies=[Depends(require_admin)]) +async def get_system_stats(): + # TODO: Implement system statistics logic + return { + "total_users": 0, + "total_applications": 0, + "active_sessions": 0 + } + +@router.post("/users/{user_id}/role", dependencies=[Depends(require_admin)]) +async def update_user_role(user_id: str, role: str): + # TODO: Implement role update logic + return {"message": f"User {user_id} role updated to {role}"} \ No newline at end of file diff --git a/oauth/backend/app/api/v1/endpoints/applications.py b/oauth/backend/app/api/v1/endpoints/applications.py new file mode 100644 index 0000000..0a85ef5 --- /dev/null +++ b/oauth/backend/app/api/v1/endpoints/applications.py @@ -0,0 +1,37 @@ +from fastapi import APIRouter, Depends, HTTPException +from typing import List +from app.core.security import get_current_user +from app.models.user import User +from app.models.application import Application + +router = APIRouter() + +@router.get("/", response_model=List[Application]) +async def get_applications(current_user: User = Depends(get_current_user)): + # TODO: Implement application list logic + return [] + +@router.post("/", response_model=Application) +async def create_application( + app_data: dict, + current_user: User = Depends(get_current_user) +): + # TODO: Implement application creation logic + return {"message": "Application created"} + +@router.put("/{app_id}") +async def update_application( + app_id: str, + app_data: dict, + current_user: User = Depends(get_current_user) +): + # TODO: Implement application update logic + return {"message": f"Application {app_id} updated"} + +@router.delete("/{app_id}") +async def delete_application( + app_id: str, + current_user: User = Depends(get_current_user) +): + # TODO: Implement application deletion logic + return {"message": f"Application {app_id} deleted"} \ No newline at end of file diff --git a/oauth/backend/app/api/v1/endpoints/auth.py b/oauth/backend/app/api/v1/endpoints/auth.py new file mode 100644 index 0000000..7ffdece --- /dev/null +++ b/oauth/backend/app/api/v1/endpoints/auth.py @@ -0,0 +1,38 @@ +from fastapi import APIRouter, HTTPException, Depends, status +from fastapi.security import OAuth2PasswordRequestForm +from app.core.security import create_access_token, get_current_user +from app.models.user import User +from app.core.config import settings + +router = APIRouter() + +@router.post("/login") +async def login(form_data: OAuth2PasswordRequestForm = Depends()): + # TODO: Implement actual authentication + return { + "access_token": create_access_token({"sub": form_data.username}), + "token_type": "bearer" + } + +@router.post("/logout") +async def logout(current_user: User = Depends(get_current_user)): + # TODO: Implement logout logic + return {"message": "Logged out successfully"} + +@router.post("/refresh") +async def refresh_token(current_user: User = Depends(get_current_user)): + # TODO: Implement token refresh logic + return { + "access_token": create_access_token({"sub": current_user.email}), + "token_type": "bearer" + } + +@router.get("/authorize") +async def authorize(): + # TODO: Implement OAuth authorization + return {"message": "Authorization endpoint"} + +@router.post("/token") +async def token(): + # TODO: Implement OAuth token endpoint + return {"message": "Token endpoint"} \ No newline at end of file diff --git a/oauth/backend/app/api/v1/endpoints/users.py b/oauth/backend/app/api/v1/endpoints/users.py new file mode 100644 index 0000000..ddca1dc --- /dev/null +++ b/oauth/backend/app/api/v1/endpoints/users.py @@ -0,0 +1,25 @@ +from fastapi import APIRouter, Depends, HTTPException +from app.core.security import get_current_user +from app.models.user import User + +router = APIRouter() + +@router.get("/me") +async def get_me(current_user: User = Depends(get_current_user)): + return current_user + +@router.put("/me") +async def update_me( + user_update: dict, + current_user: User = Depends(get_current_user) +): + # TODO: Implement user update logic + return {"message": "User updated", "user": current_user} + +@router.post("/me/password") +async def change_password( + password_data: dict, + current_user: User = Depends(get_current_user) +): + # TODO: Implement password change logic + return {"message": "Password changed successfully"} \ No newline at end of file diff --git a/oauth/backend/app/core/security.py b/oauth/backend/app/core/security.py new file mode 100644 index 0000000..9d73d1a --- /dev/null +++ b/oauth/backend/app/core/security.py @@ -0,0 +1,51 @@ +from datetime import datetime, timedelta +from typing import Optional, Dict, Any +from jose import JWTError, jwt +from passlib.context import CryptContext +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from app.core.config import settings + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login") + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) + +def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) + return encoded_jwt + +async def get_current_user(token: str = Depends(oauth2_scheme)): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + except JWTError: + raise credentials_exception + + # TODO: Get user from database + return {"email": username, "role": "user"} + +async def require_admin(current_user = Depends(get_current_user)): + if current_user.get("role") != "system_admin": + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not enough permissions" + ) + return current_user \ No newline at end of file diff --git a/oauth/backend/requirements.txt b/oauth/backend/requirements.txt index 436bcac..3a212cb 100644 --- a/oauth/backend/requirements.txt +++ b/oauth/backend/requirements.txt @@ -4,6 +4,7 @@ python-multipart==0.0.9 python-jose[cryptography]==3.3.0 passlib[bcrypt]==1.7.4 motor==3.5.1 +pymongo==4.8.0 redis==5.0.7 pydantic==2.9.1 pydantic-settings==2.4.0 diff --git a/oauth/frontend/package-lock.json b/oauth/frontend/package-lock.json index 054c7d1..a4d514b 100644 --- a/oauth/frontend/package-lock.json +++ b/oauth/frontend/package-lock.json @@ -14,6 +14,7 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-hook-form": "^7.62.0", + "react-hot-toast": "^2.6.0", "react-router-dom": "^7.8.2", "zod": "^4.1.5", "zustand": "^5.0.8" @@ -2152,7 +2153,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "devOptional": true, "license": "MIT" }, "node_modules/debug": { @@ -2770,6 +2770,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -3389,6 +3398,23 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-hot-toast": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", diff --git a/oauth/frontend/package.json b/oauth/frontend/package.json index d6d2e1b..e2a47d6 100644 --- a/oauth/frontend/package.json +++ b/oauth/frontend/package.json @@ -16,6 +16,7 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-hook-form": "^7.62.0", + "react-hot-toast": "^2.6.0", "react-router-dom": "^7.8.2", "zod": "^4.1.5", "zustand": "^5.0.8" diff --git a/oauth/frontend/src/App.tsx b/oauth/frontend/src/App.tsx index 3d7ded3..b19eae9 100644 --- a/oauth/frontend/src/App.tsx +++ b/oauth/frontend/src/App.tsx @@ -1,35 +1,70 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import LoginPage from './pages/LoginPage' +import Dashboard from './pages/Dashboard' +import Applications from './pages/Applications' +import Profile from './pages/Profile' +import AdminPanel from './pages/AdminPanel' +import AuthCallback from './pages/AuthCallback' +import { AuthProvider } from './contexts/AuthContext' +import ProtectedRoute from './components/ProtectedRoute' import './App.css' -function App() { - const [count, setCount] = useState(0) +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}) +function App() { return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- + + + + + } /> + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + } /> + + + + ) } -export default App +export default App \ No newline at end of file diff --git a/oauth/frontend/src/components/ProtectedRoute.tsx b/oauth/frontend/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..5f4ce6f --- /dev/null +++ b/oauth/frontend/src/components/ProtectedRoute.tsx @@ -0,0 +1,31 @@ +import { Navigate } from 'react-router-dom' +import { useAuth } from '../contexts/AuthContext' + +interface ProtectedRouteProps { + children: React.ReactNode + requireAdmin?: boolean +} + +const ProtectedRoute: React.FC = ({ children, requireAdmin = false }) => { + const { user, isLoading } = useAuth() + + if (isLoading) { + return ( +
+
+
+ ) + } + + if (!user) { + return + } + + if (requireAdmin && user.role !== 'system_admin') { + return + } + + return <>{children} +} + +export default ProtectedRoute \ No newline at end of file diff --git a/oauth/frontend/src/contexts/AuthContext.tsx b/oauth/frontend/src/contexts/AuthContext.tsx new file mode 100644 index 0000000..c53021f --- /dev/null +++ b/oauth/frontend/src/contexts/AuthContext.tsx @@ -0,0 +1,108 @@ +import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react' + +interface User { + id: string + email: string + name: string + role: 'system_admin' | 'group_admin' | 'user' + avatar?: string +} + +interface AuthContextType { + user: User | null + isLoading: boolean + login: (email: string, password: string) => Promise + logout: () => void + checkAuth: () => Promise +} + +const AuthContext = createContext(undefined) + +export const useAuth = () => { + const context = useContext(AuthContext) + if (!context) { + throw new Error('useAuth must be used within an AuthProvider') + } + return context +} + +interface AuthProviderProps { + children: ReactNode +} + +export const AuthProvider: React.FC = ({ children }) => { + const [user, setUser] = useState(null) + const [isLoading, setIsLoading] = useState(true) + + const checkAuth = async () => { + try { + const token = localStorage.getItem('access_token') + if (!token) { + setIsLoading(false) + return + } + + // TODO: Verify token with backend + const response = await fetch('/api/v1/users/me', { + headers: { + Authorization: `Bearer ${token}`, + }, + }) + + if (response.ok) { + const userData = await response.json() + setUser(userData) + } else { + localStorage.removeItem('access_token') + } + } catch (error) { + console.error('Auth check failed:', error) + } finally { + setIsLoading(false) + } + } + + const login = async (email: string, password: string) => { + try { + const response = await fetch('/api/v1/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + username: email, + password, + }), + }) + + if (!response.ok) { + throw new Error('Login failed') + } + + const data = await response.json() + localStorage.setItem('access_token', data.access_token) + + await checkAuth() + window.location.href = '/dashboard' + } catch (error) { + console.error('Login failed:', error) + throw error + } + } + + const logout = () => { + localStorage.removeItem('access_token') + setUser(null) + window.location.href = '/login' + } + + useEffect(() => { + checkAuth() + }, []) + + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/oauth/frontend/src/index.css b/oauth/frontend/src/index.css index 08a3ac9..72e2f61 100644 --- a/oauth/frontend/src/index.css +++ b/oauth/frontend/src/index.css @@ -1,68 +1,148 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap'); +@tailwind base; +@tailwind components; +@tailwind utilities; + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + :root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; + --gradient-1: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + --gradient-2: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + --gradient-3: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + --gradient-4: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); + --gradient-dark: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); +} - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; +body { + font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + margin: 0; + padding: 0; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; +/* Glass effect */ +.glass { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.18); } -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; +.glass-dark { + background: rgba(0, 0, 0, 0.2); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.1); } -h1 { - font-size: 3.2em; - line-height: 1.1; +/* Animations */ +@keyframes float { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-20px); } } -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; +@keyframes pulse-slow { + 0%, 100% { opacity: 0.3; } + 50% { opacity: 0.8; } } -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } +@keyframes gradient-shift { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } } + +.animate-float { + animation: float 6s ease-in-out infinite; +} + +.animate-pulse-slow { + animation: pulse-slow 4s ease-in-out infinite; +} + +.animate-gradient { + background-size: 200% 200%; + animation: gradient-shift 8s ease infinite; +} + +/* Custom scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; +} + +::-webkit-scrollbar-thumb { + background: #888; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #555; +} + +/* Input focus effects */ +.input-modern { + @apply relative; +} + +.input-modern::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 0; + height: 2px; + background: linear-gradient(90deg, #667eea, #764ba2); + transition: width 0.3s ease; +} + +.input-modern:focus-within::after { + width: 100%; +} + +/* Button hover effects */ +.btn-glow { + position: relative; + overflow: hidden; + transition: all 0.3s ease; +} + +.btn-glow::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.5); + transform: translate(-50%, -50%); + transition: width 0.6s, height 0.6s; +} + +.btn-glow:hover::before { + width: 300px; + height: 300px; +} + +/* Neumorphism effects */ +.neu-shadow { + box-shadow: + 12px 12px 24px rgba(0, 0, 0, 0.1), + -12px -12px 24px rgba(255, 255, 255, 0.1); +} + +.neu-shadow-inset { + box-shadow: + inset 6px 6px 12px rgba(0, 0, 0, 0.1), + inset -6px -6px 12px rgba(255, 255, 255, 0.1); +} \ No newline at end of file diff --git a/oauth/frontend/src/pages/AdminPanel.tsx b/oauth/frontend/src/pages/AdminPanel.tsx new file mode 100644 index 0000000..6636be3 --- /dev/null +++ b/oauth/frontend/src/pages/AdminPanel.tsx @@ -0,0 +1,10 @@ +const AdminPanel = () => { + return ( +
+

관리자 패널

+

시스템 관리자 전용 페이지입니다.

+
+ ) +} + +export default AdminPanel \ No newline at end of file diff --git a/oauth/frontend/src/pages/Applications.tsx b/oauth/frontend/src/pages/Applications.tsx new file mode 100644 index 0000000..5873c39 --- /dev/null +++ b/oauth/frontend/src/pages/Applications.tsx @@ -0,0 +1,10 @@ +const Applications = () => { + return ( +
+

애플리케이션 관리

+

OAuth 애플리케이션을 관리합니다.

+
+ ) +} + +export default Applications \ No newline at end of file diff --git a/oauth/frontend/src/pages/AuthCallback.tsx b/oauth/frontend/src/pages/AuthCallback.tsx new file mode 100644 index 0000000..3704e22 --- /dev/null +++ b/oauth/frontend/src/pages/AuthCallback.tsx @@ -0,0 +1,54 @@ +import { useEffect } from 'react' +import { useNavigate } from 'react-router-dom' + +const AuthCallback = () => { + const navigate = useNavigate() + + useEffect(() => { + // Handle OAuth callback + const params = new URLSearchParams(window.location.search) + const code = params.get('code') + const state = params.get('state') + + if (code) { + // Exchange code for token + handleOAuthCallback(code, state) + } else { + navigate('/login') + } + }, [navigate]) + + const handleOAuthCallback = async (code: string, state: string | null) => { + try { + const response = await fetch('/api/v1/auth/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ code, state }), + }) + + if (response.ok) { + const data = await response.json() + localStorage.setItem('access_token', data.access_token) + navigate('/dashboard') + } else { + navigate('/login') + } + } catch (error) { + console.error('OAuth callback error:', error) + navigate('/login') + } + } + + return ( +
+
+
+

인증 처리 중...

+
+
+ ) +} + +export default AuthCallback \ No newline at end of file diff --git a/oauth/frontend/src/pages/Dashboard.tsx b/oauth/frontend/src/pages/Dashboard.tsx new file mode 100644 index 0000000..b9e3811 --- /dev/null +++ b/oauth/frontend/src/pages/Dashboard.tsx @@ -0,0 +1,190 @@ +import { useState } from 'react' +import { useAuth } from '../contexts/AuthContext' +import { + LayoutDashboard, + Users, + Settings, + LogOut, + ChevronDown, + Bell, + Search, + Menu, + X, + Shield, + Key, + Activity, + Clock +} from 'lucide-react' + +const Dashboard = () => { + const { user, logout } = useAuth() + const [isSidebarOpen, setIsSidebarOpen] = useState(true) + const [isProfileOpen, setIsProfileOpen] = useState(false) + + const stats = [ + { title: '활성 세션', value: '24', icon: Users, change: '+12%' }, + { title: '등록된 앱', value: '8', icon: Key, change: '+2' }, + { title: '이번 달 로그인', value: '1,429', icon: Activity, change: '+48%' }, + { title: '평균 응답시간', value: '132ms', icon: Clock, change: '-12%' }, + ] + + const recentActivities = [ + { id: 1, action: '새로운 애플리케이션 등록', app: 'E-Commerce Platform', time: '5분 전' }, + { id: 2, action: '권한 업데이트', app: 'Mobile App', time: '2시간 전' }, + { id: 3, action: '토큰 갱신', app: 'Analytics Dashboard', time: '4시간 전' }, + { id: 4, action: '새로운 사용자 추가', app: 'CRM System', time: '1일 전' }, + ] + + return ( +
+ {/* Sidebar */} + + + {/* Main Content */} +
+ {/* Header */} +
+
+
+ +
+ + +
+
+ +
+ + +
+ + + {isProfileOpen && ( +
+ + 프로필 설정 + +
+ +
+ )} +
+
+
+
+ + {/* Dashboard Content */} +
+
+

대시보드

+

OAuth 시스템 현황을 한눈에 확인하세요

+
+ + {/* Stats Grid */} +
+ {stats.map((stat) => ( +
+
+
+ +
+ + {stat.change} + +
+

{stat.value}

+

{stat.title}

+
+ ))} +
+ + {/* Recent Activities */} +
+
+

최근 활동

+
+
+
+ {recentActivities.map((activity) => ( +
+
+

{activity.action}

+

{activity.app}

+
+ {activity.time} +
+ ))} +
+
+
+
+
+
+ ) +} + +export default Dashboard \ No newline at end of file diff --git a/oauth/frontend/src/pages/LoginPage.tsx b/oauth/frontend/src/pages/LoginPage.tsx new file mode 100644 index 0000000..52e4e08 --- /dev/null +++ b/oauth/frontend/src/pages/LoginPage.tsx @@ -0,0 +1,316 @@ +import { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { useAuth } from '../contexts/AuthContext' +import { + Eye, + EyeOff, + Loader2, + Fingerprint, + Shield, + Sparkles, + Lock, + Mail, + ArrowRight, + CheckCircle2 +} from 'lucide-react' + +const LoginPage = () => { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [showPassword, setShowPassword] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [rememberMe, setRememberMe] = useState(false) + const [focusedInput, setFocusedInput] = useState(null) + const [theme, setTheme] = useState(null) + + const { login, user } = useAuth() + const navigate = useNavigate() + + useEffect(() => { + if (user) { + navigate('/dashboard') + } + + // Get theme from query params (for OAuth applications) + const params = new URLSearchParams(window.location.search) + const appId = params.get('app_id') + + if (appId) { + fetchApplicationTheme(appId) + } + }, [user, navigate]) + + const fetchApplicationTheme = async (appId: string) => { + try { + const response = await fetch(`/api/v1/applications/${appId}/theme`) + if (response.ok) { + const themeData = await response.json() + setTheme(themeData) + applyTheme(themeData) + } + } catch (error) { + console.error('Failed to fetch theme:', error) + } + } + + const applyTheme = (themeData: any) => { + if (themeData?.primaryColor) { + document.documentElement.style.setProperty('--primary-color', themeData.primaryColor) + } + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + + try { + await login(email, password) + } catch (error) { + console.error('Login error:', error) + } finally { + setIsLoading(false) + } + } + + return ( +
+ {/* Animated background elements */} +
+
+
+
+
+ + {/* Grid pattern overlay */} +
+ + {/* Main content */} +
+
+ + {/* Left side - Branding */} +
+
+
+
+ +
+

+ {theme?.title || 'AiMond Authorization'} +

+
+

+ 엔터프라이즈급 통합 인증 시스템 +

+
+ +
+
+
+ +
+
+

완벽한 보안

+

OAuth 2.0 표준 준수 및 엔드투엔드 암호화

+
+
+ +
+
+ +
+
+

생체 인증 지원

+

Touch ID, Face ID 등 최신 인증 기술 통합

+
+
+ +
+
+ +
+
+

실시간 모니터링

+

모든 인증 활동을 실시간으로 추적 및 분석

+
+
+
+ + {/* Stats */} +
+
+
99.9%
+
가동률
+
+
+
2FA
+
이중 인증
+
+
+
256bit
+
AES 암호화
+
+
+
+ + {/* Right side - Login Form */} +
+
+ {/* Form Header */} +
+

Welcome Back

+

보안 인증을 통해 시스템에 접속하세요

+
+ + {/* Login Form */} +
+ {/* Email Input */} +
+ +
+ setEmail(e.target.value)} + onFocus={() => setFocusedInput('email')} + onBlur={() => setFocusedInput(null)} + className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-gray-400 focus:outline-none focus:border-purple-400 focus:bg-white/10 transition-all duration-300" + placeholder="your@email.com" + disabled={isLoading} + /> +
+
+
+ + {/* Password Input */} +
+ +
+ setPassword(e.target.value)} + onFocus={() => setFocusedInput('password')} + onBlur={() => setFocusedInput(null)} + className="w-full px-4 py-3 pr-12 bg-white/5 border border-white/10 rounded-xl text-white placeholder-gray-400 focus:outline-none focus:border-purple-400 focus:bg-white/10 transition-all duration-300" + placeholder="••••••••" + disabled={isLoading} + /> + +
+
+
+ + {/* Remember Me & Forgot Password */} +
+ + + 비밀번호를 잊으셨나요? + +
+ + {/* Submit Button */} + +
+ + {/* Divider */} +
+
+
+
+
+ 또는 +
+
+ + {/* Social Login */} +
+ + + +
+ + {/* Sign up link */} +

+ 아직 계정이 없으신가요?{' '} + + 지금 가입하기 + +

+
+ + {/* Security Badge */} +
+ + 256-bit SSL 암호화로 보호됨 +
+
+
+
+
+ ) +} + +export default LoginPage \ No newline at end of file diff --git a/oauth/frontend/src/pages/Profile.tsx b/oauth/frontend/src/pages/Profile.tsx new file mode 100644 index 0000000..d691d0c --- /dev/null +++ b/oauth/frontend/src/pages/Profile.tsx @@ -0,0 +1,10 @@ +const Profile = () => { + return ( +
+

프로필 설정

+

사용자 프로필을 관리합니다.

+
+ ) +} + +export default Profile \ No newline at end of file