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 + 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 */}
+
+
+
+ {/* Right side - Login Form */}
+
+
+ {/* Form Header */}
+
+
Welcome Back
+
보안 인증을 통해 시스템에 접속하세요
+
+
+ {/* Login Form */}
+
+
+ {/* 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