v0.1.0
This commit is contained in:
34
src/App.jsx
Normal file
34
src/App.jsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import { Routes, Route, Navigate } from 'react-router-dom'
|
||||
import { Box } from '@mui/material'
|
||||
import Login from './pages/Login'
|
||||
import Register from './pages/Register'
|
||||
import Dashboard from './pages/Dashboard'
|
||||
import OAuthAuthorize from './pages/OAuthAuthorize'
|
||||
import { AuthProvider } from './contexts/AuthContext'
|
||||
import PrivateRoute from './components/PrivateRoute'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default' }}>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route path="/oauth/authorize" element={<OAuthAuthorize />} />
|
||||
<Route
|
||||
path="/dashboard"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Dashboard />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="/" element={<Navigate to="/login" replace />} />
|
||||
</Routes>
|
||||
</Box>
|
||||
</AuthProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
25
src/components/PrivateRoute.jsx
Normal file
25
src/components/PrivateRoute.jsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React from 'react'
|
||||
import { Navigate } from 'react-router-dom'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { CircularProgress, Box } from '@mui/material'
|
||||
|
||||
const PrivateRoute = ({ children }) => {
|
||||
const { user, loading } = useAuth()
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
minHeight="100vh"
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return user ? children : <Navigate to="/login" replace />
|
||||
}
|
||||
|
||||
export default PrivateRoute
|
91
src/contexts/AuthContext.jsx
Normal file
91
src/contexts/AuthContext.jsx
Normal file
@ -0,0 +1,91 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react'
|
||||
import axios from 'axios'
|
||||
|
||||
const AuthContext = createContext()
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext)
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`
|
||||
checkAuthStatus()
|
||||
} else {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const checkAuthStatus = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/auth/profile')
|
||||
setUser(response.data.data)
|
||||
} catch (error) {
|
||||
localStorage.removeItem('token')
|
||||
delete axios.defaults.headers.common['Authorization']
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const login = async (credentials) => {
|
||||
try {
|
||||
const response = await axios.post('/api/auth/login', credentials)
|
||||
const { token, user } = response.data.data
|
||||
localStorage.setItem('token', token)
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`
|
||||
setUser(user)
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.response?.data?.message || '登录失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const register = async (userData) => {
|
||||
try {
|
||||
const response = await axios.post('/api/auth/register', userData)
|
||||
const { token, user } = response.data.data
|
||||
localStorage.setItem('token', token)
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`
|
||||
setUser(user)
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.response?.data?.message || '注册失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('token')
|
||||
delete axios.defaults.headers.common['Authorization']
|
||||
setUser(null)
|
||||
}
|
||||
|
||||
const value = {
|
||||
user,
|
||||
loading,
|
||||
login,
|
||||
register,
|
||||
logout
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
}
|
28
src/main.jsx
Normal file
28
src/main.jsx
Normal file
@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { ThemeProvider, createTheme } from '@mui/material/styles'
|
||||
import CssBaseline from '@mui/material/CssBaseline'
|
||||
import App from './App'
|
||||
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
main: '#1976d2',
|
||||
},
|
||||
secondary: {
|
||||
main: '#dc004e',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>
|
||||
)
|
273
src/pages/Dashboard.jsx
Normal file
273
src/pages/Dashboard.jsx
Normal file
@ -0,0 +1,273 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import {
|
||||
Container,
|
||||
Paper,
|
||||
Typography,
|
||||
Box,
|
||||
Button,
|
||||
Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
CardActions,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemIcon,
|
||||
Divider,
|
||||
Chip,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
Alert
|
||||
} from '@mui/material'
|
||||
import {
|
||||
Person,
|
||||
Email,
|
||||
CalendarToday,
|
||||
Security,
|
||||
Add,
|
||||
Delete,
|
||||
Visibility,
|
||||
VisibilityOff,
|
||||
Logout
|
||||
} from '@mui/icons-material'
|
||||
import axios from 'axios'
|
||||
|
||||
const Dashboard = () => {
|
||||
const { user, logout } = useAuth()
|
||||
const [clients, setClients] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [openCreateDialog, setOpenCreateDialog] = useState(false)
|
||||
const [newClient, setNewClient] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
redirect_uris: [''],
|
||||
scopes: ['read', 'write']
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
fetchClients()
|
||||
}, [])
|
||||
|
||||
const fetchClients = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/oauth/clients')
|
||||
setClients(response.data.data.clients)
|
||||
} catch (error) {
|
||||
setError('获取客户端列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateClient = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
await axios.post('/api/oauth/clients', newClient)
|
||||
setOpenCreateDialog(false)
|
||||
setNewClient({ name: '', description: '', redirect_uris: [''], scopes: ['read', 'write'] })
|
||||
fetchClients()
|
||||
} catch (error) {
|
||||
setError(error.response?.data?.message || '创建客户端失败')
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const handleDeleteClient = async (clientId) => {
|
||||
if (window.confirm('确定要删除这个客户端吗?')) {
|
||||
try {
|
||||
await axios.delete(`/api/oauth/clients/${clientId}`)
|
||||
fetchClients()
|
||||
} catch (error) {
|
||||
setError('删除客户端失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
logout()
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{ py: 4 }}>
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h4" component="h1" gutterBottom>
|
||||
个人中心
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
欢迎回来,{user?.username}!
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }} onClose={() => setError('')}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{/* 用户信息卡片 */}
|
||||
<Grid item xs={12} md={4}>
|
||||
<Paper elevation={2} sx={{ p: 3 }}>
|
||||
<Box display="flex" alignItems="center" mb={2}>
|
||||
<Person sx={{ mr: 1, color: 'primary.main' }} />
|
||||
<Typography variant="h6">用户信息</Typography>
|
||||
</Box>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Person />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="用户名" secondary={user?.username} />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Email />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="邮箱" secondary={user?.email} />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CalendarToday />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="注册时间"
|
||||
secondary={new Date(user?.created_at).toLocaleDateString()}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
startIcon={<Logout />}
|
||||
onClick={handleLogout}
|
||||
fullWidth
|
||||
>
|
||||
退出登录
|
||||
</Button>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
{/* OAuth客户端管理 */}
|
||||
<Grid item xs={12} md={8}>
|
||||
<Paper elevation={2} sx={{ p: 3 }}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Security sx={{ mr: 1, color: 'primary.main' }} />
|
||||
<Typography variant="h6">OAuth客户端</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<Add />}
|
||||
onClick={() => setOpenCreateDialog(true)}
|
||||
>
|
||||
创建客户端
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{clients.length === 0 ? (
|
||||
<Box textAlign="center" py={4}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
还没有OAuth客户端,点击上方按钮创建一个
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Grid container spacing={2}>
|
||||
{clients.map((client) => (
|
||||
<Grid item xs={12} key={client.client_id}>
|
||||
<Card variant="outlined">
|
||||
<CardContent>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="flex-start">
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{client.name}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
{client.description}
|
||||
</Typography>
|
||||
<Box mt={1}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
客户端ID: {client.client_id}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box mt={1}>
|
||||
{client.scopes.map((scope) => (
|
||||
<Chip
|
||||
key={scope}
|
||||
label={scope}
|
||||
size="small"
|
||||
sx={{ mr: 0.5, mb: 0.5 }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
<Button
|
||||
color="error"
|
||||
size="small"
|
||||
onClick={() => handleDeleteClient(client.client_id)}
|
||||
>
|
||||
<Delete />
|
||||
</Button>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* 创建客户端对话框 */}
|
||||
<Dialog open={openCreateDialog} onClose={() => setOpenCreateDialog(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>创建OAuth客户端</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="客户端名称"
|
||||
value={newClient.name}
|
||||
onChange={(e) => setNewClient({ ...newClient, name: e.target.value })}
|
||||
margin="normal"
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="描述"
|
||||
value={newClient.description}
|
||||
onChange={(e) => setNewClient({ ...newClient, description: e.target.value })}
|
||||
margin="normal"
|
||||
multiline
|
||||
rows={2}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="重定向URI"
|
||||
value={newClient.redirect_uris[0]}
|
||||
onChange={(e) => setNewClient({
|
||||
...newClient,
|
||||
redirect_uris: [e.target.value]
|
||||
})}
|
||||
margin="normal"
|
||||
required
|
||||
placeholder="http://localhost:3001/callback"
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setOpenCreateDialog(false)}>取消</Button>
|
||||
<Button
|
||||
onClick={handleCreateClient}
|
||||
variant="contained"
|
||||
disabled={loading || !newClient.name}
|
||||
>
|
||||
{loading ? '创建中...' : '创建'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default Dashboard
|
153
src/pages/Login.jsx
Normal file
153
src/pages/Login.jsx
Normal file
@ -0,0 +1,153 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Link, Navigate } from 'react-router-dom'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import {
|
||||
Container,
|
||||
Paper,
|
||||
TextField,
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
Alert,
|
||||
InputAdornment,
|
||||
IconButton
|
||||
} from '@mui/material'
|
||||
import { Visibility, VisibilityOff, Login as LoginIcon } from '@mui/icons-material'
|
||||
|
||||
const Login = () => {
|
||||
const { login, user } = useAuth()
|
||||
const [formData, setFormData] = useState({
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
if (user) {
|
||||
return <Navigate to="/dashboard" replace />
|
||||
}
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setLoading(true)
|
||||
|
||||
const result = await login(formData)
|
||||
if (!result.success) {
|
||||
setError(result.message)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxWidth="sm">
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
p: 4,
|
||||
width: '100%',
|
||||
maxWidth: 400
|
||||
}}
|
||||
>
|
||||
<Box textAlign="center" mb={3}>
|
||||
<LoginIcon sx={{ fontSize: 48, color: 'primary.main', mb: 2 }} />
|
||||
<Typography variant="h4" component="h1" gutterBottom>
|
||||
登录
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
欢迎回来,请登录您的账户
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="用户名"
|
||||
name="username"
|
||||
value={formData.username}
|
||||
onChange={handleChange}
|
||||
margin="normal"
|
||||
required
|
||||
autoComplete="username"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="密码"
|
||||
name="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
margin="normal"
|
||||
required
|
||||
autoComplete="current-password"
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
edge="end"
|
||||
>
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
size="large"
|
||||
disabled={loading}
|
||||
sx={{ mt: 3, mb: 2 }}
|
||||
>
|
||||
{loading ? '登录中...' : '登录'}
|
||||
</Button>
|
||||
|
||||
<Box textAlign="center">
|
||||
<Typography variant="body2">
|
||||
还没有账户?{' '}
|
||||
<Link to="/register" style={{ textDecoration: 'none' }}>
|
||||
<Typography
|
||||
component="span"
|
||||
variant="body2"
|
||||
color="primary"
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
立即注册
|
||||
</Typography>
|
||||
</Link>
|
||||
</Typography>
|
||||
</Box>
|
||||
</form>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default Login
|
250
src/pages/OAuthAuthorize.jsx
Normal file
250
src/pages/OAuthAuthorize.jsx
Normal file
@ -0,0 +1,250 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useSearchParams, useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import {
|
||||
Container,
|
||||
Paper,
|
||||
Typography,
|
||||
Box,
|
||||
Button,
|
||||
Grid,
|
||||
Card,
|
||||
CardContent,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Chip,
|
||||
Alert,
|
||||
CircularProgress
|
||||
} from '@mui/material'
|
||||
import {
|
||||
Security,
|
||||
CheckCircle,
|
||||
Cancel,
|
||||
Person,
|
||||
Email,
|
||||
CalendarToday,
|
||||
Visibility,
|
||||
VisibilityOff
|
||||
} from '@mui/icons-material'
|
||||
import axios from 'axios'
|
||||
|
||||
const OAuthAuthorize = () => {
|
||||
const { user } = useAuth()
|
||||
const [searchParams] = useSearchParams()
|
||||
const navigate = useNavigate()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [clientInfo, setClientInfo] = useState(null)
|
||||
|
||||
const clientId = searchParams.get('client_id')
|
||||
const redirectUri = searchParams.get('redirect_uri')
|
||||
const scope = searchParams.get('scope')
|
||||
const state = searchParams.get('state')
|
||||
const responseType = searchParams.get('response_type')
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
navigate('/login')
|
||||
return
|
||||
}
|
||||
|
||||
if (!clientId || !redirectUri || responseType !== 'code') {
|
||||
setError('无效的授权请求')
|
||||
return
|
||||
}
|
||||
|
||||
// 这里可以添加客户端信息获取逻辑
|
||||
// 为了演示,我们使用默认信息
|
||||
setClientInfo({
|
||||
name: '第三方应用',
|
||||
description: '请求访问您的账户信息',
|
||||
scopes: scope ? scope.split(' ') : ['read', 'write']
|
||||
})
|
||||
}, [user, clientId, redirectUri, scope, responseType, navigate])
|
||||
|
||||
const handleAuthorize = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
response_type: 'code',
|
||||
client_id: clientId,
|
||||
redirect_uri: redirectUri,
|
||||
scope: scope || 'read write',
|
||||
state: state || ''
|
||||
})
|
||||
|
||||
const response = await axios.get(`/api/oauth/authorize?${params}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
const { redirect_url } = response.data.data
|
||||
window.location.href = redirect_url
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error.response?.data?.message || '授权失败')
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const handleDeny = () => {
|
||||
// 拒绝授权,重定向回应用
|
||||
const denyUrl = new URL(redirectUri)
|
||||
denyUrl.searchParams.set('error', 'access_denied')
|
||||
if (state) {
|
||||
denyUrl.searchParams.set('state', state)
|
||||
}
|
||||
window.location.href = denyUrl.toString()
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<Container maxWidth="sm">
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxWidth="sm">
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
p: 4,
|
||||
width: '100%',
|
||||
maxWidth: 500
|
||||
}}
|
||||
>
|
||||
<Box textAlign="center" mb={3}>
|
||||
<Security sx={{ fontSize: 48, color: 'primary.main', mb: 2 }} />
|
||||
<Typography variant="h4" component="h1" gutterBottom>
|
||||
授权请求
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
第三方应用请求访问您的账户
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{clientInfo && (
|
||||
<>
|
||||
{/* 应用信息 */}
|
||||
<Card variant="outlined" sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{clientInfo.name}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
{clientInfo.description}
|
||||
</Typography>
|
||||
<Box mt={2}>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
请求的权限:
|
||||
</Typography>
|
||||
{clientInfo.scopes.map((scope) => (
|
||||
<Chip
|
||||
key={scope}
|
||||
label={scope === 'read' ? '读取信息' : scope === 'write' ? '写入信息' : scope}
|
||||
size="small"
|
||||
sx={{ mr: 0.5, mb: 0.5 }}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 用户信息 */}
|
||||
<Card variant="outlined" sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
您的账户信息
|
||||
</Typography>
|
||||
<List dense>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Person />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="用户名" secondary={user.username} />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Email />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="邮箱" secondary={user.email} />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CalendarToday />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="注册时间"
|
||||
secondary={new Date(user.created_at).toLocaleDateString()}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
color="error"
|
||||
startIcon={<Cancel />}
|
||||
onClick={handleDeny}
|
||||
disabled={loading}
|
||||
>
|
||||
拒绝
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
startIcon={<CheckCircle />}
|
||||
onClick={handleAuthorize}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? '授权中...' : '授权'}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default OAuthAuthorize
|
210
src/pages/Register.jsx
Normal file
210
src/pages/Register.jsx
Normal file
@ -0,0 +1,210 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Link, Navigate } from 'react-router-dom'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import {
|
||||
Container,
|
||||
Paper,
|
||||
TextField,
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
Alert,
|
||||
InputAdornment,
|
||||
IconButton
|
||||
} from '@mui/material'
|
||||
import { Visibility, VisibilityOff, PersonAdd as RegisterIcon } from '@mui/icons-material'
|
||||
|
||||
const Register = () => {
|
||||
const { register, user } = useAuth()
|
||||
const [formData, setFormData] = useState({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
if (user) {
|
||||
return <Navigate to="/dashboard" replace />
|
||||
}
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value
|
||||
})
|
||||
}
|
||||
|
||||
const validateForm = () => {
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
setError('两次输入的密码不一致')
|
||||
return false
|
||||
}
|
||||
if (formData.password.length < 6) {
|
||||
setError('密码长度至少6位')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
|
||||
if (!validateForm()) {
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
|
||||
const { confirmPassword, ...userData } = formData
|
||||
const result = await register(userData)
|
||||
if (!result.success) {
|
||||
setError(result.message)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxWidth="sm">
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={3}
|
||||
sx={{
|
||||
p: 4,
|
||||
width: '100%',
|
||||
maxWidth: 400
|
||||
}}
|
||||
>
|
||||
<Box textAlign="center" mb={3}>
|
||||
<RegisterIcon sx={{ fontSize: 48, color: 'primary.main', mb: 2 }} />
|
||||
<Typography variant="h4" component="h1" gutterBottom>
|
||||
注册
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
创建您的账户
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="用户名"
|
||||
name="username"
|
||||
value={formData.username}
|
||||
onChange={handleChange}
|
||||
margin="normal"
|
||||
required
|
||||
autoComplete="username"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="邮箱"
|
||||
name="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
margin="normal"
|
||||
required
|
||||
autoComplete="email"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="密码"
|
||||
name="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
margin="normal"
|
||||
required
|
||||
autoComplete="new-password"
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
edge="end"
|
||||
>
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="确认密码"
|
||||
name="confirmPassword"
|
||||
type={showConfirmPassword ? 'text' : 'password'}
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleChange}
|
||||
margin="normal"
|
||||
required
|
||||
autoComplete="new-password"
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
edge="end"
|
||||
>
|
||||
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
size="large"
|
||||
disabled={loading}
|
||||
sx={{ mt: 3, mb: 2 }}
|
||||
>
|
||||
{loading ? '注册中...' : '注册'}
|
||||
</Button>
|
||||
|
||||
<Box textAlign="center">
|
||||
<Typography variant="body2">
|
||||
已有账户?{' '}
|
||||
<Link to="/login" style={{ textDecoration: 'none' }}>
|
||||
<Typography
|
||||
component="span"
|
||||
variant="body2"
|
||||
color="primary"
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
立即登录
|
||||
</Typography>
|
||||
</Link>
|
||||
</Typography>
|
||||
</Box>
|
||||
</form>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default Register
|
Reference in New Issue
Block a user