create-velox-app 0.6.31 → 0.6.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +126 -0
- package/GUIDE.md +230 -0
- package/dist/cli.js +1 -0
- package/dist/index.js +14 -4
- package/dist/templates/auth.js +10 -0
- package/dist/templates/index.js +30 -1
- package/dist/templates/placeholders.js +0 -3
- package/dist/templates/rsc-auth.d.ts +12 -0
- package/dist/templates/rsc-auth.js +208 -0
- package/dist/templates/rsc.js +40 -1
- package/dist/templates/shared/css-generator.d.ts +26 -0
- package/dist/templates/shared/css-generator.js +553 -0
- package/dist/templates/shared/index.d.ts +3 -0
- package/dist/templates/shared/index.js +3 -0
- package/dist/templates/shared/rsc-styles.d.ts +54 -0
- package/dist/templates/shared/rsc-styles.js +68 -0
- package/dist/templates/shared/theme.d.ts +133 -0
- package/dist/templates/shared/theme.js +141 -0
- package/dist/templates/spa.js +10 -0
- package/dist/templates/trpc.js +10 -0
- package/dist/templates/types.d.ts +2 -1
- package/dist/templates/types.js +6 -0
- package/package.json +6 -3
- package/src/templates/source/api/config/database.ts +13 -32
- package/src/templates/source/api/docker-compose.yml +21 -0
- package/src/templates/source/root/CLAUDE.auth.md +6 -0
- package/src/templates/source/root/CLAUDE.default.md +6 -0
- package/src/templates/source/rsc/CLAUDE.md +56 -2
- package/src/templates/source/rsc/app/actions/posts.ts +1 -1
- package/src/templates/source/rsc/app/actions/users.ts +111 -20
- package/src/templates/source/rsc/app/layouts/dashboard.tsx +21 -16
- package/src/templates/source/rsc/app/layouts/marketing.tsx +34 -0
- package/src/templates/source/rsc/app/layouts/minimal-content.tsx +21 -0
- package/src/templates/source/rsc/app/layouts/minimal.tsx +86 -5
- package/src/templates/source/rsc/app/layouts/root.tsx +148 -44
- package/src/templates/source/rsc/docker-compose.yml +21 -0
- package/src/templates/source/rsc/package.json +3 -3
- package/src/templates/source/rsc/src/api/database.ts +13 -32
- package/src/templates/source/rsc/src/api/handler.ts +1 -1
- package/src/templates/source/rsc/src/entry.client.tsx +65 -18
- package/src/templates/source/rsc-auth/CLAUDE.md +230 -0
- package/src/templates/source/rsc-auth/app/actions/auth.ts +112 -0
- package/src/templates/source/rsc-auth/app/actions/users.ts +289 -0
- package/src/templates/source/rsc-auth/app/layouts/dashboard.tsx +132 -0
- package/src/templates/source/rsc-auth/app/layouts/marketing.tsx +59 -0
- package/src/templates/source/rsc-auth/app/layouts/minimal-content.tsx +21 -0
- package/src/templates/source/rsc-auth/app/layouts/minimal.tsx +111 -0
- package/src/templates/source/rsc-auth/app/layouts/root.tsx +355 -0
- package/src/templates/source/rsc-auth/app/pages/_not-found.tsx +15 -0
- package/src/templates/source/rsc-auth/app/pages/auth/login.tsx +198 -0
- package/src/templates/source/rsc-auth/app/pages/auth/register.tsx +225 -0
- package/src/templates/source/rsc-auth/app/pages/dashboard/index.tsx +267 -0
- package/src/templates/source/rsc-auth/app/pages/index.tsx +83 -0
- package/src/templates/source/rsc-auth/app/pages/users.tsx +47 -0
- package/src/templates/source/rsc-auth/app.config.ts +12 -0
- package/src/templates/source/rsc-auth/docker-compose.yml +21 -0
- package/src/templates/source/rsc-auth/env.example +11 -0
- package/src/templates/source/rsc-auth/gitignore +34 -0
- package/src/templates/source/rsc-auth/package.json +44 -0
- package/src/templates/source/rsc-auth/prisma/schema.prisma +23 -0
- package/src/templates/source/rsc-auth/prisma.config.ts +22 -0
- package/src/templates/source/rsc-auth/public/favicon.svg +4 -0
- package/src/templates/source/rsc-auth/src/api/database.ts +129 -0
- package/src/templates/source/rsc-auth/src/api/handler.ts +85 -0
- package/src/templates/source/rsc-auth/src/api/procedures/auth.ts +262 -0
- package/src/templates/source/rsc-auth/src/api/procedures/health.ts +48 -0
- package/src/templates/source/rsc-auth/src/api/procedures/users.ts +87 -0
- package/src/templates/source/rsc-auth/src/api/schemas/auth.ts +79 -0
- package/src/templates/source/rsc-auth/src/api/schemas/user.ts +38 -0
- package/src/templates/source/rsc-auth/src/api/utils/auth.ts +157 -0
- package/src/templates/source/rsc-auth/src/entry.client.tsx +63 -0
- package/src/templates/source/rsc-auth/src/entry.server.tsx +262 -0
- package/src/templates/source/rsc-auth/tsconfig.json +24 -0
- package/src/templates/source/shared/scripts/check-client-imports.sh +75 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login Page
|
|
3
|
+
*
|
|
4
|
+
* Client component for authentication using server actions.
|
|
5
|
+
* Tokens are stored in httpOnly cookies automatically by the server action.
|
|
6
|
+
*/
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import { useState } from 'react';
|
|
10
|
+
|
|
11
|
+
import { login } from '@/app/actions/auth';
|
|
12
|
+
|
|
13
|
+
export default function LoginPage() {
|
|
14
|
+
const [email, setEmail] = useState('');
|
|
15
|
+
const [password, setPassword] = useState('');
|
|
16
|
+
const [error, setError] = useState('');
|
|
17
|
+
const [loading, setLoading] = useState(false);
|
|
18
|
+
|
|
19
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
setError('');
|
|
22
|
+
setLoading(true);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Call server action - tokens are stored in httpOnly cookies automatically
|
|
26
|
+
const result = await login({ email, password });
|
|
27
|
+
|
|
28
|
+
if (!result.success) {
|
|
29
|
+
throw new Error(result.error.message);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Redirect to dashboard (cookies are already set by server action)
|
|
33
|
+
window.location.href = '/dashboard';
|
|
34
|
+
} catch (err) {
|
|
35
|
+
setError(err instanceof Error ? err.message : 'Login failed');
|
|
36
|
+
} finally {
|
|
37
|
+
setLoading(false);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="auth-container">
|
|
43
|
+
<style>{`
|
|
44
|
+
.auth-container {
|
|
45
|
+
max-width: 450px;
|
|
46
|
+
margin: 0 auto;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.auth-card {
|
|
50
|
+
background: #111;
|
|
51
|
+
padding: 2rem;
|
|
52
|
+
border-radius: 8px;
|
|
53
|
+
border: 1px solid #222;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.auth-card h1 {
|
|
57
|
+
font-size: 1.5rem;
|
|
58
|
+
font-weight: 700;
|
|
59
|
+
margin-bottom: 1.5rem;
|
|
60
|
+
text-align: center;
|
|
61
|
+
color: #ededed;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.auth-form {
|
|
65
|
+
margin-top: 1.5rem;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.form-group {
|
|
69
|
+
margin-bottom: 1.25rem;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.form-label {
|
|
73
|
+
display: block;
|
|
74
|
+
margin-bottom: 0.5rem;
|
|
75
|
+
color: #ededed;
|
|
76
|
+
font-weight: 500;
|
|
77
|
+
font-size: 0.875rem;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.form-input {
|
|
81
|
+
width: 100%;
|
|
82
|
+
padding: 0.75rem 1rem;
|
|
83
|
+
border-radius: 4px;
|
|
84
|
+
border: 1px solid #222;
|
|
85
|
+
background: #0a0a0a;
|
|
86
|
+
color: #ededed;
|
|
87
|
+
font-size: 0.875rem;
|
|
88
|
+
transition: border-color 0.2s, background 0.2s;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.form-input:focus {
|
|
92
|
+
outline: none;
|
|
93
|
+
border-color: #00d9ff;
|
|
94
|
+
background: #111;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.form-input:hover {
|
|
98
|
+
border-color: #333;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.btn {
|
|
102
|
+
width: 100%;
|
|
103
|
+
padding: 0.75rem 1.25rem;
|
|
104
|
+
border: none;
|
|
105
|
+
border-radius: 4px;
|
|
106
|
+
cursor: pointer;
|
|
107
|
+
font-weight: 500;
|
|
108
|
+
font-size: 0.875rem;
|
|
109
|
+
transition: background 0.2s, opacity 0.2s;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.btn-primary {
|
|
113
|
+
background: #00d9ff;
|
|
114
|
+
color: #000;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.btn-primary:hover:not(:disabled) {
|
|
118
|
+
background: rgba(0, 217, 255, 0.8);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.btn-primary:disabled {
|
|
122
|
+
opacity: 0.6;
|
|
123
|
+
cursor: not-allowed;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.error-message {
|
|
127
|
+
padding: 0.75rem;
|
|
128
|
+
background: #2a1111;
|
|
129
|
+
color: #ff6666;
|
|
130
|
+
border-radius: 4px;
|
|
131
|
+
margin-bottom: 1rem;
|
|
132
|
+
border: 1px solid #ff4444;
|
|
133
|
+
font-size: 0.875rem;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.auth-footer {
|
|
137
|
+
margin-top: 1.5rem;
|
|
138
|
+
text-align: center;
|
|
139
|
+
color: #888;
|
|
140
|
+
font-size: 0.875rem;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.auth-footer a {
|
|
144
|
+
color: #00d9ff;
|
|
145
|
+
font-weight: 500;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.auth-footer a:hover {
|
|
149
|
+
opacity: 0.8;
|
|
150
|
+
}
|
|
151
|
+
`}</style>
|
|
152
|
+
|
|
153
|
+
<div className="auth-card">
|
|
154
|
+
<h1>Login</h1>
|
|
155
|
+
|
|
156
|
+
<form onSubmit={handleSubmit} className="auth-form">
|
|
157
|
+
{error && <div className="error-message">{error}</div>}
|
|
158
|
+
|
|
159
|
+
<div className="form-group">
|
|
160
|
+
<label htmlFor="email" className="form-label">
|
|
161
|
+
Email
|
|
162
|
+
</label>
|
|
163
|
+
<input
|
|
164
|
+
id="email"
|
|
165
|
+
type="email"
|
|
166
|
+
value={email}
|
|
167
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
168
|
+
required
|
|
169
|
+
className="form-input"
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<div className="form-group">
|
|
174
|
+
<label htmlFor="password" className="form-label">
|
|
175
|
+
Password
|
|
176
|
+
</label>
|
|
177
|
+
<input
|
|
178
|
+
id="password"
|
|
179
|
+
type="password"
|
|
180
|
+
value={password}
|
|
181
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
182
|
+
required
|
|
183
|
+
className="form-input"
|
|
184
|
+
/>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<button type="submit" disabled={loading} className="btn btn-primary">
|
|
188
|
+
{loading ? 'Signing in...' : 'Sign In'}
|
|
189
|
+
</button>
|
|
190
|
+
</form>
|
|
191
|
+
|
|
192
|
+
<p className="auth-footer">
|
|
193
|
+
Don't have an account? <a href="/auth/register">Register</a>
|
|
194
|
+
</p>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registration Page
|
|
3
|
+
*
|
|
4
|
+
* Client component for user registration using server actions.
|
|
5
|
+
* Tokens are stored in httpOnly cookies automatically by the server action.
|
|
6
|
+
*/
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import { useState } from 'react';
|
|
10
|
+
|
|
11
|
+
import { register } from '@/app/actions/auth';
|
|
12
|
+
|
|
13
|
+
export default function RegisterPage() {
|
|
14
|
+
const [name, setName] = useState('');
|
|
15
|
+
const [email, setEmail] = useState('');
|
|
16
|
+
const [password, setPassword] = useState('');
|
|
17
|
+
const [error, setError] = useState('');
|
|
18
|
+
const [loading, setLoading] = useState(false);
|
|
19
|
+
|
|
20
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
setError('');
|
|
23
|
+
setLoading(true);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Call server action - tokens are stored in httpOnly cookies automatically
|
|
27
|
+
const result = await register({ name, email, password });
|
|
28
|
+
|
|
29
|
+
if (!result.success) {
|
|
30
|
+
throw new Error(result.error.message);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Redirect to dashboard (cookies are already set by server action)
|
|
34
|
+
window.location.href = '/dashboard';
|
|
35
|
+
} catch (err) {
|
|
36
|
+
setError(err instanceof Error ? err.message : 'Registration failed');
|
|
37
|
+
} finally {
|
|
38
|
+
setLoading(false);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="auth-container">
|
|
44
|
+
<style>{`
|
|
45
|
+
.auth-container {
|
|
46
|
+
max-width: 450px;
|
|
47
|
+
margin: 0 auto;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.auth-card {
|
|
51
|
+
background: #111;
|
|
52
|
+
padding: 2rem;
|
|
53
|
+
border-radius: 8px;
|
|
54
|
+
border: 1px solid #222;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.auth-card h1 {
|
|
58
|
+
font-size: 1.5rem;
|
|
59
|
+
font-weight: 700;
|
|
60
|
+
margin-bottom: 1.5rem;
|
|
61
|
+
text-align: center;
|
|
62
|
+
color: #ededed;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.auth-form {
|
|
66
|
+
margin-top: 1.5rem;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.form-group {
|
|
70
|
+
margin-bottom: 1.25rem;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.form-label {
|
|
74
|
+
display: block;
|
|
75
|
+
margin-bottom: 0.5rem;
|
|
76
|
+
color: #ededed;
|
|
77
|
+
font-weight: 500;
|
|
78
|
+
font-size: 0.875rem;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.form-input {
|
|
82
|
+
width: 100%;
|
|
83
|
+
padding: 0.75rem 1rem;
|
|
84
|
+
border-radius: 4px;
|
|
85
|
+
border: 1px solid #222;
|
|
86
|
+
background: #0a0a0a;
|
|
87
|
+
color: #ededed;
|
|
88
|
+
font-size: 0.875rem;
|
|
89
|
+
transition: border-color 0.2s, background 0.2s;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.form-input:focus {
|
|
93
|
+
outline: none;
|
|
94
|
+
border-color: #00d9ff;
|
|
95
|
+
background: #111;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.form-input:hover {
|
|
99
|
+
border-color: #333;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.form-hint {
|
|
103
|
+
display: block;
|
|
104
|
+
color: #888;
|
|
105
|
+
font-size: 0.75rem;
|
|
106
|
+
margin-top: 0.5rem;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.btn {
|
|
110
|
+
width: 100%;
|
|
111
|
+
padding: 0.75rem 1.25rem;
|
|
112
|
+
border: none;
|
|
113
|
+
border-radius: 4px;
|
|
114
|
+
cursor: pointer;
|
|
115
|
+
font-weight: 500;
|
|
116
|
+
font-size: 0.875rem;
|
|
117
|
+
transition: background 0.2s, opacity 0.2s;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.btn-primary {
|
|
121
|
+
background: #00d9ff;
|
|
122
|
+
color: #000;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.btn-primary:hover:not(:disabled) {
|
|
126
|
+
background: rgba(0, 217, 255, 0.8);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.btn-primary:disabled {
|
|
130
|
+
opacity: 0.6;
|
|
131
|
+
cursor: not-allowed;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.error-message {
|
|
135
|
+
padding: 0.75rem;
|
|
136
|
+
background: #2a1111;
|
|
137
|
+
color: #ff6666;
|
|
138
|
+
border-radius: 4px;
|
|
139
|
+
margin-bottom: 1rem;
|
|
140
|
+
border: 1px solid #ff4444;
|
|
141
|
+
font-size: 0.875rem;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.auth-footer {
|
|
145
|
+
margin-top: 1.5rem;
|
|
146
|
+
text-align: center;
|
|
147
|
+
color: #888;
|
|
148
|
+
font-size: 0.875rem;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.auth-footer a {
|
|
152
|
+
color: #00d9ff;
|
|
153
|
+
font-weight: 500;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.auth-footer a:hover {
|
|
157
|
+
opacity: 0.8;
|
|
158
|
+
}
|
|
159
|
+
`}</style>
|
|
160
|
+
|
|
161
|
+
<div className="auth-card">
|
|
162
|
+
<h1>Create Account</h1>
|
|
163
|
+
|
|
164
|
+
<form onSubmit={handleSubmit} className="auth-form">
|
|
165
|
+
{error && <div className="error-message">{error}</div>}
|
|
166
|
+
|
|
167
|
+
<div className="form-group">
|
|
168
|
+
<label htmlFor="name" className="form-label">
|
|
169
|
+
Name
|
|
170
|
+
</label>
|
|
171
|
+
<input
|
|
172
|
+
id="name"
|
|
173
|
+
type="text"
|
|
174
|
+
value={name}
|
|
175
|
+
onChange={(e) => setName(e.target.value)}
|
|
176
|
+
required
|
|
177
|
+
minLength={2}
|
|
178
|
+
className="form-input"
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div className="form-group">
|
|
183
|
+
<label htmlFor="email" className="form-label">
|
|
184
|
+
Email
|
|
185
|
+
</label>
|
|
186
|
+
<input
|
|
187
|
+
id="email"
|
|
188
|
+
type="email"
|
|
189
|
+
value={email}
|
|
190
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
191
|
+
required
|
|
192
|
+
className="form-input"
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<div className="form-group">
|
|
197
|
+
<label htmlFor="password" className="form-label">
|
|
198
|
+
Password
|
|
199
|
+
</label>
|
|
200
|
+
<input
|
|
201
|
+
id="password"
|
|
202
|
+
type="password"
|
|
203
|
+
value={password}
|
|
204
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
205
|
+
required
|
|
206
|
+
minLength={12}
|
|
207
|
+
className="form-input"
|
|
208
|
+
/>
|
|
209
|
+
<small className="form-hint">
|
|
210
|
+
At least 12 characters with uppercase, lowercase, and number
|
|
211
|
+
</small>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<button type="submit" disabled={loading} className="btn btn-primary">
|
|
215
|
+
{loading ? 'Creating account...' : 'Create Account'}
|
|
216
|
+
</button>
|
|
217
|
+
</form>
|
|
218
|
+
|
|
219
|
+
<p className="auth-footer">
|
|
220
|
+
Already have an account? <a href="/auth/login">Login</a>
|
|
221
|
+
</p>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard Page
|
|
3
|
+
*
|
|
4
|
+
* Protected page that requires authentication.
|
|
5
|
+
* Uses httpOnly cookie-based authentication (tokens stored in cookies by server actions).
|
|
6
|
+
*/
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import { useEffect, useState } from 'react';
|
|
10
|
+
|
|
11
|
+
import { logout } from '@/app/actions/auth';
|
|
12
|
+
|
|
13
|
+
interface User {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
email: string;
|
|
17
|
+
roles: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function DashboardPage() {
|
|
21
|
+
const [user, setUser] = useState<User | null>(null);
|
|
22
|
+
const [loading, setLoading] = useState(true);
|
|
23
|
+
const [error, setError] = useState('');
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const fetchUser = async () => {
|
|
27
|
+
try {
|
|
28
|
+
// Cookies are sent automatically with credentials: 'include'
|
|
29
|
+
const response = await fetch('/api/auth/me', {
|
|
30
|
+
credentials: 'include',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
// Token invalid, expired, or not present - redirect to login
|
|
35
|
+
window.location.href = '/auth/login';
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const userData = await response.json();
|
|
40
|
+
setUser(userData);
|
|
41
|
+
} catch {
|
|
42
|
+
setError('Failed to load user data');
|
|
43
|
+
} finally {
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
fetchUser();
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const handleLogout = async () => {
|
|
52
|
+
try {
|
|
53
|
+
// Call server action to clear auth cookies
|
|
54
|
+
await logout();
|
|
55
|
+
} catch {
|
|
56
|
+
// Ignore errors - we're redirecting anyway
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Redirect to home
|
|
60
|
+
window.location.href = '/';
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (loading) {
|
|
64
|
+
return (
|
|
65
|
+
<div className="dashboard-loading">
|
|
66
|
+
<style>{`
|
|
67
|
+
.dashboard-loading {
|
|
68
|
+
padding: 2rem;
|
|
69
|
+
text-align: center;
|
|
70
|
+
color: #888;
|
|
71
|
+
}
|
|
72
|
+
`}</style>
|
|
73
|
+
Loading...
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (error) {
|
|
79
|
+
return (
|
|
80
|
+
<div className="dashboard-error">
|
|
81
|
+
<style>{`
|
|
82
|
+
.dashboard-error {
|
|
83
|
+
padding: 2rem;
|
|
84
|
+
text-align: center;
|
|
85
|
+
color: #ff6666;
|
|
86
|
+
}
|
|
87
|
+
`}</style>
|
|
88
|
+
{error}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!user) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className="dashboard">
|
|
99
|
+
<style>{`
|
|
100
|
+
.dashboard {
|
|
101
|
+
max-width: 800px;
|
|
102
|
+
margin: 0 auto;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.dashboard-header {
|
|
106
|
+
display: flex;
|
|
107
|
+
justify-content: space-between;
|
|
108
|
+
align-items: center;
|
|
109
|
+
margin-bottom: 2rem;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.dashboard-header h1 {
|
|
113
|
+
font-size: 1.75rem;
|
|
114
|
+
font-weight: 700;
|
|
115
|
+
color: #ededed;
|
|
116
|
+
margin: 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.btn-logout {
|
|
120
|
+
padding: 0.5rem 1rem;
|
|
121
|
+
background: transparent;
|
|
122
|
+
color: #ff6666;
|
|
123
|
+
border: 1px solid #ff6666;
|
|
124
|
+
border-radius: 4px;
|
|
125
|
+
cursor: pointer;
|
|
126
|
+
font-weight: 500;
|
|
127
|
+
font-size: 0.875rem;
|
|
128
|
+
transition: background 0.2s, color 0.2s;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.btn-logout:hover {
|
|
132
|
+
background: #ff6666;
|
|
133
|
+
color: #000;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.user-card {
|
|
137
|
+
padding: 1.5rem;
|
|
138
|
+
background: #111;
|
|
139
|
+
border: 1px solid #222;
|
|
140
|
+
border-radius: 8px;
|
|
141
|
+
margin-bottom: 1.5rem;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.user-card h2 {
|
|
145
|
+
font-size: 1.25rem;
|
|
146
|
+
font-weight: 600;
|
|
147
|
+
color: #ededed;
|
|
148
|
+
margin: 0 0 1rem 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.user-info {
|
|
152
|
+
color: #888;
|
|
153
|
+
font-size: 0.875rem;
|
|
154
|
+
line-height: 1.8;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.user-info strong {
|
|
158
|
+
color: #ededed;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.actions-card {
|
|
162
|
+
padding: 1.5rem;
|
|
163
|
+
background: #0d1a26;
|
|
164
|
+
border: 1px solid #1a3a5c;
|
|
165
|
+
border-radius: 8px;
|
|
166
|
+
margin-bottom: 1.5rem;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.actions-card h3 {
|
|
170
|
+
font-size: 1rem;
|
|
171
|
+
font-weight: 600;
|
|
172
|
+
color: #00d9ff;
|
|
173
|
+
margin: 0 0 0.75rem 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.actions-card p {
|
|
177
|
+
color: #888;
|
|
178
|
+
font-size: 0.875rem;
|
|
179
|
+
margin: 0 0 1rem 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.actions-list {
|
|
183
|
+
list-style: none;
|
|
184
|
+
padding: 0;
|
|
185
|
+
margin: 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.actions-list li {
|
|
189
|
+
color: #888;
|
|
190
|
+
font-size: 0.875rem;
|
|
191
|
+
padding: 0.5rem 0;
|
|
192
|
+
border-bottom: 1px solid #1a3a5c;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.actions-list li:last-child {
|
|
196
|
+
border-bottom: none;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.admin-tag {
|
|
200
|
+
display: inline-block;
|
|
201
|
+
padding: 0.125rem 0.5rem;
|
|
202
|
+
background: #00d9ff;
|
|
203
|
+
color: #000;
|
|
204
|
+
border-radius: 4px;
|
|
205
|
+
font-size: 0.75rem;
|
|
206
|
+
font-weight: 600;
|
|
207
|
+
margin-right: 0.5rem;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.back-link {
|
|
211
|
+
display: inline-block;
|
|
212
|
+
color: #00d9ff;
|
|
213
|
+
font-size: 0.875rem;
|
|
214
|
+
text-decoration: none;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.back-link:hover {
|
|
218
|
+
opacity: 0.8;
|
|
219
|
+
}
|
|
220
|
+
`}</style>
|
|
221
|
+
|
|
222
|
+
<div className="dashboard-header">
|
|
223
|
+
<h1>Dashboard</h1>
|
|
224
|
+
<button type="button" onClick={handleLogout} className="btn-logout">
|
|
225
|
+
Logout
|
|
226
|
+
</button>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<div className="user-card">
|
|
230
|
+
<h2>Welcome, {user.name || 'User'}!</h2>
|
|
231
|
+
<div className="user-info">
|
|
232
|
+
<p>
|
|
233
|
+
<strong>Email:</strong> {user.email}
|
|
234
|
+
</p>
|
|
235
|
+
<p>
|
|
236
|
+
<strong>Roles:</strong> {user.roles.join(', ')}
|
|
237
|
+
</p>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<div className="actions-card">
|
|
242
|
+
<h3>Your Actions</h3>
|
|
243
|
+
<p>As an authenticated user, you can:</p>
|
|
244
|
+
<ul className="actions-list">
|
|
245
|
+
<li>View your profile</li>
|
|
246
|
+
<li>Update your settings</li>
|
|
247
|
+
{user.roles.includes('admin') && (
|
|
248
|
+
<>
|
|
249
|
+
<li>
|
|
250
|
+
<span className="admin-tag">Admin</span>
|
|
251
|
+
Manage users
|
|
252
|
+
</li>
|
|
253
|
+
<li>
|
|
254
|
+
<span className="admin-tag">Admin</span>
|
|
255
|
+
View all data
|
|
256
|
+
</li>
|
|
257
|
+
</>
|
|
258
|
+
)}
|
|
259
|
+
</ul>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<a href="/" className="back-link">
|
|
263
|
+
← Back to Home
|
|
264
|
+
</a>
|
|
265
|
+
</div>
|
|
266
|
+
);
|
|
267
|
+
}
|