sitepaige-mcp-server 1.0.2 → 1.1.0
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/components/IntegrationComponent.tsx +1 -0
- package/components/admin.tsx +30 -27
- package/components/auth.tsx +9 -9
- package/components/cta.tsx +3 -10
- package/components/headerlogin.tsx +9 -9
- package/components/login.tsx +90 -11
- package/components/logincallback.tsx +1 -0
- package/components/menu.tsx +0 -6
- package/components/profile.tsx +12 -11
- package/defaultapp/api/Auth/resend-verification/route.ts +130 -0
- package/defaultapp/api/Auth/route.ts +39 -49
- package/defaultapp/api/Auth/signup/route.ts +5 -15
- package/defaultapp/api/Auth/verify-email/route.ts +12 -5
- package/defaultapp/api/admin/users/route.ts +5 -3
- package/defaultapp/auth/auth.ts +9 -9
- package/defaultapp/db-mysql.ts +1 -1
- package/defaultapp/db-password-auth.ts +37 -0
- package/defaultapp/db-postgres.ts +1 -1
- package/defaultapp/db-sqlite.ts +1 -1
- package/defaultapp/db-users.ts +73 -73
- package/defaultapp/middleware.ts +15 -17
- package/dist/components/IntegrationComponent.tsx +1 -0
- package/dist/components/admin.tsx +30 -27
- package/dist/components/auth.tsx +9 -9
- package/dist/components/cta.tsx +3 -10
- package/dist/components/headerlogin.tsx +9 -9
- package/dist/components/login.tsx +90 -11
- package/dist/components/logincallback.tsx +1 -0
- package/dist/components/menu.tsx +0 -6
- package/dist/components/profile.tsx +12 -11
- package/dist/defaultapp/api/Auth/resend-verification/route.ts +130 -0
- package/dist/defaultapp/api/Auth/route.ts +39 -49
- package/dist/defaultapp/api/Auth/signup/route.ts +5 -15
- package/dist/defaultapp/api/Auth/verify-email/route.ts +12 -5
- package/dist/defaultapp/api/admin/users/route.ts +5 -3
- package/dist/defaultapp/auth/auth.ts +9 -9
- package/dist/defaultapp/db-mysql.ts +1 -1
- package/dist/defaultapp/db-password-auth.ts +37 -0
- package/dist/defaultapp/db-postgres.ts +1 -1
- package/dist/defaultapp/db-sqlite.ts +1 -1
- package/dist/defaultapp/db-users.ts +73 -73
- package/dist/defaultapp/middleware.ts +15 -17
- package/dist/generators/sql.js +11 -3
- package/dist/generators/sql.js.map +1 -1
- package/dist/generators/views.js +14 -14
- package/dist/generators/views.js.map +1 -1
- package/package.json +1 -1
package/components/admin.tsx
CHANGED
|
@@ -5,15 +5,15 @@ import { useRouter } from 'next/navigation';
|
|
|
5
5
|
|
|
6
6
|
interface User {
|
|
7
7
|
userid: string;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
oauthid: string;
|
|
9
|
+
source: string;
|
|
10
|
+
username: string;
|
|
11
|
+
email?: string;
|
|
12
|
+
avatarurl?: string;
|
|
13
|
+
userlevel: number;
|
|
14
|
+
lastlogindate: string;
|
|
15
|
+
createddate: string;
|
|
16
|
+
isactive: boolean;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
interface UserStats {
|
|
@@ -46,7 +46,9 @@ export default function AdminPanel() {
|
|
|
46
46
|
setError(null);
|
|
47
47
|
|
|
48
48
|
// Fetch users - authorization is handled server-side via session cookie
|
|
49
|
-
const response = await fetch('/api/admin/users'
|
|
49
|
+
const response = await fetch('/api/admin/users', {
|
|
50
|
+
credentials: 'include'
|
|
51
|
+
});
|
|
50
52
|
if (!response.ok) {
|
|
51
53
|
if (response.status === 401) {
|
|
52
54
|
setError('Access denied. Admin privileges required.');
|
|
@@ -80,6 +82,7 @@ export default function AdminPanel() {
|
|
|
80
82
|
headers: {
|
|
81
83
|
'Content-Type': 'application/json',
|
|
82
84
|
},
|
|
85
|
+
credentials: 'include',
|
|
83
86
|
body: JSON.stringify({
|
|
84
87
|
userId,
|
|
85
88
|
permissionLevel: newLevel,
|
|
@@ -169,13 +172,13 @@ export default function AdminPanel() {
|
|
|
169
172
|
|
|
170
173
|
// Filter users based on search and permission level
|
|
171
174
|
const filteredUsers = users.filter(user => {
|
|
172
|
-
const matchesSearch = user.
|
|
173
|
-
(user.
|
|
175
|
+
const matchesSearch = user.username.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
176
|
+
(user.email && user.email.toLowerCase().includes(searchTerm.toLowerCase()));
|
|
174
177
|
|
|
175
178
|
const matchesFilter = filterLevel === 'all' ||
|
|
176
|
-
(filterLevel === 'admin' && user.
|
|
177
|
-
(filterLevel === 'registered' && user.
|
|
178
|
-
(filterLevel === 'guest' && user.
|
|
179
|
+
(filterLevel === 'admin' && user.userlevel === 2) ||
|
|
180
|
+
(filterLevel === 'registered' && user.userlevel === 1) ||
|
|
181
|
+
(filterLevel === 'guest' && user.userlevel === 0);
|
|
179
182
|
|
|
180
183
|
return matchesSearch && matchesFilter;
|
|
181
184
|
});
|
|
@@ -298,26 +301,26 @@ export default function AdminPanel() {
|
|
|
298
301
|
<tr key={user.userid} className="hover:bg-gray-50">
|
|
299
302
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
300
303
|
<div className="flex items-center">
|
|
301
|
-
{user.
|
|
304
|
+
{user.avatarurl ? (
|
|
302
305
|
<img
|
|
303
306
|
className="h-10 w-10 rounded-full"
|
|
304
|
-
src={user.
|
|
305
|
-
alt={user.
|
|
307
|
+
src={user.avatarurl}
|
|
308
|
+
alt={user.username}
|
|
306
309
|
/>
|
|
307
310
|
) : (
|
|
308
311
|
<div className="h-10 w-10 rounded-full bg-gray-300 flex items-center justify-center">
|
|
309
312
|
<span className="text-gray-600 font-medium">
|
|
310
|
-
{user.
|
|
313
|
+
{user.username.charAt(0).toUpperCase()}
|
|
311
314
|
</span>
|
|
312
315
|
</div>
|
|
313
316
|
)}
|
|
314
317
|
<div className="ml-4">
|
|
315
318
|
<div className="text-sm font-medium text-gray-900">
|
|
316
|
-
{user.
|
|
319
|
+
{user.username}
|
|
317
320
|
</div>
|
|
318
|
-
{user.
|
|
321
|
+
{user.email && (
|
|
319
322
|
<div className="text-sm text-gray-500">
|
|
320
|
-
{user.
|
|
323
|
+
{user.email}
|
|
321
324
|
</div>
|
|
322
325
|
)}
|
|
323
326
|
</div>
|
|
@@ -325,15 +328,15 @@ export default function AdminPanel() {
|
|
|
325
328
|
</td>
|
|
326
329
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
327
330
|
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">
|
|
328
|
-
{user.
|
|
331
|
+
{user.source}
|
|
329
332
|
</span>
|
|
330
333
|
</td>
|
|
331
334
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
332
335
|
<select
|
|
333
|
-
value={user.
|
|
336
|
+
value={user.userlevel}
|
|
334
337
|
onChange={(e) => handlePermissionChange(user.userid, parseInt(e.target.value))}
|
|
335
338
|
disabled={isUpdating === user.userid}
|
|
336
|
-
className={`px-2 py-1 text-xs leading-5 font-semibold rounded-full ${getPermissionLevelColor(user.
|
|
339
|
+
className={`px-2 py-1 text-xs leading-5 font-semibold rounded-full ${getPermissionLevelColor(user.userlevel)} cursor-pointer hover:opacity-80 disabled:opacity-50 disabled:cursor-not-allowed`}
|
|
337
340
|
>
|
|
338
341
|
<option value={0}>Guest</option>
|
|
339
342
|
<option value={1}>Registered User</option>
|
|
@@ -341,11 +344,11 @@ export default function AdminPanel() {
|
|
|
341
344
|
</select>
|
|
342
345
|
</td>
|
|
343
346
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
344
|
-
{formatDate(user.
|
|
347
|
+
{formatDate(user.lastlogindate)}
|
|
345
348
|
</td>
|
|
346
349
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
|
347
350
|
<button
|
|
348
|
-
onClick={() => handleDeleteUser(user.userid, user.
|
|
351
|
+
onClick={() => handleDeleteUser(user.userid, user.username)}
|
|
349
352
|
disabled={isDeleting === user.userid}
|
|
350
353
|
className="text-red-600 hover:text-red-900 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
351
354
|
>
|
package/components/auth.tsx
CHANGED
|
@@ -11,11 +11,11 @@ import { useUserStore } from '@/store/user';
|
|
|
11
11
|
|
|
12
12
|
interface AuthProps {
|
|
13
13
|
auth: {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
id: string;
|
|
15
|
+
username: string;
|
|
16
|
+
avatarurl: string;
|
|
17
|
+
userlevel: string;
|
|
18
|
+
isadmin: boolean;
|
|
19
19
|
} | null;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -25,10 +25,10 @@ export default function Auth({ auth }: AuthProps) {
|
|
|
25
25
|
useEffect(() => {
|
|
26
26
|
if (auth) {
|
|
27
27
|
setIsAuthenticated(true);
|
|
28
|
-
setUserLevel(auth.
|
|
29
|
-
setIsAdmin(auth.
|
|
30
|
-
setUserName(auth.
|
|
31
|
-
setAvatarURL(auth.
|
|
28
|
+
setUserLevel(auth.userlevel);
|
|
29
|
+
setIsAdmin(auth.isadmin);
|
|
30
|
+
setUserName(auth.username);
|
|
31
|
+
setAvatarURL(auth.avatarurl);
|
|
32
32
|
} else {
|
|
33
33
|
setIsAuthenticated(false);
|
|
34
34
|
setUserLevel('0');
|
package/components/cta.tsx
CHANGED
|
@@ -28,10 +28,9 @@ interface CTAData {
|
|
|
28
28
|
interface RCTAProps {
|
|
29
29
|
custom_view_description: string;
|
|
30
30
|
onNavigate: (pageId: string) => void;
|
|
31
|
-
isPaigeLoading?: boolean;
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
const RCTA: React.FC<RCTAProps> = ({ custom_view_description, onNavigate
|
|
33
|
+
const RCTA: React.FC<RCTAProps> = ({ custom_view_description, onNavigate }) => {
|
|
35
34
|
let ctaData: CTAData;
|
|
36
35
|
|
|
37
36
|
try {
|
|
@@ -101,10 +100,6 @@ const RCTA: React.FC<RCTAProps> = ({ custom_view_description, onNavigate, isPaig
|
|
|
101
100
|
};
|
|
102
101
|
|
|
103
102
|
const handleButtonClick = (pageId: string) => {
|
|
104
|
-
if (isPaigeLoading) {
|
|
105
|
-
console.log('Navigation blocked: Paige is currently processing a request');
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
103
|
if (pageId) {
|
|
109
104
|
onNavigate(pageId);
|
|
110
105
|
}
|
|
@@ -175,11 +170,9 @@ const RCTA: React.FC<RCTAProps> = ({ custom_view_description, onNavigate, isPaig
|
|
|
175
170
|
<button
|
|
176
171
|
key={index}
|
|
177
172
|
onClick={() => handleButtonClick(button.page)}
|
|
178
|
-
className={`px-6 py-3 rounded-lg transition-all ${hoverClass} ${
|
|
179
|
-
isPaigeLoading ? 'opacity-50 cursor-not-allowed' : ''
|
|
180
|
-
} ${!button.page ? 'opacity-75 cursor-not-allowed' : ''}`}
|
|
173
|
+
className={`px-6 py-3 rounded-lg transition-all ${hoverClass} ${!button.page ? 'opacity-75 cursor-not-allowed' : ''}`}
|
|
181
174
|
style={buttonStyles}
|
|
182
|
-
disabled={
|
|
175
|
+
disabled={!button.page}
|
|
183
176
|
>
|
|
184
177
|
{button.buttonTitle || 'Button'}
|
|
185
178
|
</button>
|
|
@@ -17,11 +17,11 @@ interface MenuItem {
|
|
|
17
17
|
|
|
18
18
|
interface UserData {
|
|
19
19
|
userid: string;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
username: string;
|
|
21
|
+
avatarurl: string;
|
|
22
|
+
email: string;
|
|
23
|
+
userlevel: number;
|
|
24
|
+
isadmin: boolean;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const getLocalizedText = (text: string, language: string = 'English'): string => {
|
|
@@ -210,11 +210,11 @@ const LoginSection: React.FC<LoginSectionProps> = ({ websiteLanguage = 'English'
|
|
|
210
210
|
className="flex items-center focus:outline-none relative group cursor-pointer"
|
|
211
211
|
style={{ color: textColor }}
|
|
212
212
|
>
|
|
213
|
-
<span className="mr-2" style={{ color: textColor }}>{userData.
|
|
213
|
+
<span className="mr-2" style={{ color: textColor }}>{userData.username || getLocalizedText('Sample User', websiteLanguage)}</span>
|
|
214
214
|
<div className="relative">
|
|
215
|
-
{userData.
|
|
215
|
+
{userData.avatarurl ? (
|
|
216
216
|
<img
|
|
217
|
-
src={userData.
|
|
217
|
+
src={userData.avatarurl}
|
|
218
218
|
alt="User avatar"
|
|
219
219
|
className="w-10 h-10 rounded-full"
|
|
220
220
|
referrerPolicy="no-referrer"
|
|
@@ -222,7 +222,7 @@ const LoginSection: React.FC<LoginSectionProps> = ({ websiteLanguage = 'English'
|
|
|
222
222
|
/>
|
|
223
223
|
) : (
|
|
224
224
|
<div className="w-10 h-10 rounded-full bg-blue-500 flex items-center justify-center text-white font-semibold">
|
|
225
|
-
{userData.
|
|
225
|
+
{userData.username ? userData.username.charAt(0).toUpperCase() : 'U'}
|
|
226
226
|
</div>
|
|
227
227
|
)}
|
|
228
228
|
</div>
|
package/components/login.tsx
CHANGED
|
@@ -9,7 +9,7 @@ checked in the system build settings. It is safe to modify this file without it
|
|
|
9
9
|
import React, { useState } from 'react';
|
|
10
10
|
|
|
11
11
|
interface LoginProps {
|
|
12
|
-
providers: ('apple' | 'facebook' | 'github' | 'google' | '
|
|
12
|
+
providers: ('apple' | 'facebook' | 'github' | 'google' | 'userpass')[];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export default function Login({ providers }: LoginProps) {
|
|
@@ -20,6 +20,8 @@ export default function Login({ providers }: LoginProps) {
|
|
|
20
20
|
const [confirmPassword, setConfirmPassword] = useState('');
|
|
21
21
|
const [isLoading, setIsLoading] = useState(false);
|
|
22
22
|
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
23
|
+
const [showResendVerification, setShowResendVerification] = useState(false);
|
|
24
|
+
const [resendEmail, setResendEmail] = useState('');
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
const handleProviderLogin = async (provider: string) => {
|
|
@@ -49,6 +51,37 @@ export default function Login({ providers }: LoginProps) {
|
|
|
49
51
|
|
|
50
52
|
};
|
|
51
53
|
|
|
54
|
+
const handleResendVerification = async () => {
|
|
55
|
+
setError(null);
|
|
56
|
+
setMessage(null);
|
|
57
|
+
setIsLoading(true);
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch('/api/Auth/resend-verification', {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: {
|
|
63
|
+
'Content-Type': 'application/json'
|
|
64
|
+
},
|
|
65
|
+
credentials: 'include', // Include cookies in request
|
|
66
|
+
body: JSON.stringify({ email: resendEmail || email })
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const data = await response.json();
|
|
70
|
+
|
|
71
|
+
if (response.ok && data.success) {
|
|
72
|
+
setMessage({ type: 'success', text: data.message || 'Verification email sent! Please check your inbox.' });
|
|
73
|
+
setShowResendVerification(false);
|
|
74
|
+
setResendEmail('');
|
|
75
|
+
} else {
|
|
76
|
+
setError(data.error || 'Failed to send verification email');
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
setError('An unexpected error occurred');
|
|
80
|
+
} finally {
|
|
81
|
+
setIsLoading(false);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
52
85
|
const handleUsernamePasswordAuth = async (e: React.FormEvent) => {
|
|
53
86
|
e.preventDefault();
|
|
54
87
|
setError(null);
|
|
@@ -66,7 +99,10 @@ export default function Login({ providers }: LoginProps) {
|
|
|
66
99
|
|
|
67
100
|
const response = await fetch('/api/Auth/signup', {
|
|
68
101
|
method: 'POST',
|
|
69
|
-
headers: {
|
|
102
|
+
headers: {
|
|
103
|
+
'Content-Type': 'application/json'
|
|
104
|
+
},
|
|
105
|
+
credentials: 'include', // Include cookies in request
|
|
70
106
|
body: JSON.stringify({ email, password })
|
|
71
107
|
});
|
|
72
108
|
|
|
@@ -84,8 +120,11 @@ export default function Login({ providers }: LoginProps) {
|
|
|
84
120
|
// Handle login
|
|
85
121
|
const response = await fetch('/api/Auth', {
|
|
86
122
|
method: 'POST',
|
|
87
|
-
headers: {
|
|
88
|
-
|
|
123
|
+
headers: {
|
|
124
|
+
'Content-Type': 'application/json'
|
|
125
|
+
},
|
|
126
|
+
credentials: 'include', // Include cookies in request
|
|
127
|
+
body: JSON.stringify({ email, password, provider: 'userpass' })
|
|
89
128
|
});
|
|
90
129
|
|
|
91
130
|
const data = await response.json();
|
|
@@ -95,6 +134,11 @@ export default function Login({ providers }: LoginProps) {
|
|
|
95
134
|
window.location.href = '/';
|
|
96
135
|
} else {
|
|
97
136
|
setError(data.error || 'Login failed');
|
|
137
|
+
// Check if error is about email verification
|
|
138
|
+
if (response.status === 403 || data.error?.toLowerCase().includes('verify')) {
|
|
139
|
+
setShowResendVerification(true);
|
|
140
|
+
setResendEmail(email);
|
|
141
|
+
}
|
|
98
142
|
}
|
|
99
143
|
}
|
|
100
144
|
} catch (err) {
|
|
@@ -104,8 +148,8 @@ export default function Login({ providers }: LoginProps) {
|
|
|
104
148
|
}
|
|
105
149
|
};
|
|
106
150
|
|
|
107
|
-
const showUsernamePasswordForm = providers?.includes('
|
|
108
|
-
const oauthProviders = providers?.filter(p => p !== '
|
|
151
|
+
const showUsernamePasswordForm = providers?.includes('userpass');
|
|
152
|
+
const oauthProviders = providers?.filter(p => p !== 'userpass') || [];
|
|
109
153
|
|
|
110
154
|
return (
|
|
111
155
|
<div className="flex flex-col items-center justify-center min-h-[500px] p-4">
|
|
@@ -179,17 +223,18 @@ export default function Login({ providers }: LoginProps) {
|
|
|
179
223
|
</div>
|
|
180
224
|
|
|
181
225
|
<div className="text-center">
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
onClick={() => {
|
|
226
|
+
<a
|
|
227
|
+
href="#"
|
|
228
|
+
onClick={(e) => {
|
|
229
|
+
e.preventDefault();
|
|
185
230
|
setIsSignup(!isSignup);
|
|
186
231
|
setError(null);
|
|
187
232
|
setMessage(null);
|
|
188
233
|
}}
|
|
189
|
-
className="text-sm text-indigo-600 hover:text-indigo-500"
|
|
234
|
+
className="text-sm text-indigo-600 hover:text-indigo-500 underline"
|
|
190
235
|
>
|
|
191
236
|
{isSignup ? 'Already have an account? Sign in' : "Don't have an account? Sign up"}
|
|
192
|
-
</
|
|
237
|
+
</a>
|
|
193
238
|
</div>
|
|
194
239
|
</form>
|
|
195
240
|
)}
|
|
@@ -234,6 +279,40 @@ export default function Login({ providers }: LoginProps) {
|
|
|
234
279
|
{error}
|
|
235
280
|
</div>
|
|
236
281
|
)}
|
|
282
|
+
|
|
283
|
+
{showResendVerification && (
|
|
284
|
+
<div className="mt-4 p-4 border border-gray-200 rounded-md bg-gray-50">
|
|
285
|
+
<p className="text-sm text-gray-700 mb-3">
|
|
286
|
+
Need a new verification email? Enter your email address and we'll send you a new link.
|
|
287
|
+
</p>
|
|
288
|
+
<div className="space-y-3">
|
|
289
|
+
<input
|
|
290
|
+
type="email"
|
|
291
|
+
value={resendEmail}
|
|
292
|
+
onChange={(e) => setResendEmail(e.target.value)}
|
|
293
|
+
placeholder="Email address"
|
|
294
|
+
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 text-sm"
|
|
295
|
+
/>
|
|
296
|
+
<button
|
|
297
|
+
onClick={handleResendVerification}
|
|
298
|
+
disabled={isLoading || !resendEmail}
|
|
299
|
+
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50"
|
|
300
|
+
>
|
|
301
|
+
{isLoading ? 'Sending...' : 'Resend Verification Email'}
|
|
302
|
+
</button>
|
|
303
|
+
<button
|
|
304
|
+
type="button"
|
|
305
|
+
onClick={() => {
|
|
306
|
+
setShowResendVerification(false);
|
|
307
|
+
setResendEmail('');
|
|
308
|
+
}}
|
|
309
|
+
className="w-full text-sm text-gray-500 hover:text-gray-700 bg-transparent border-0 p-0 underline"
|
|
310
|
+
>
|
|
311
|
+
Cancel
|
|
312
|
+
</button>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
237
316
|
</div>
|
|
238
317
|
</div>
|
|
239
318
|
);
|
package/components/menu.tsx
CHANGED
|
@@ -60,7 +60,6 @@ export default function Menu({ menu, onClick, pages = [] }: MenuProps) {
|
|
|
60
60
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
61
61
|
const [isMobile, setIsMobile] = useState(false);
|
|
62
62
|
const [selectedPage, setSelectedPage] = useState<string | null>(null);
|
|
63
|
-
const [isPaigeLoading, setIsPaigeLoading] = useState(false);
|
|
64
63
|
|
|
65
64
|
// Handle case where menu is undefined/null
|
|
66
65
|
if (!menu) {
|
|
@@ -230,7 +229,6 @@ export default function Menu({ menu, onClick, pages = [] }: MenuProps) {
|
|
|
230
229
|
items-center
|
|
231
230
|
justify-center
|
|
232
231
|
${isSelected ? 'border-blue-600 bg-blue-50' : ''}
|
|
233
|
-
${isPaigeLoading ? 'opacity-50 cursor-not-allowed' : ''}
|
|
234
232
|
`}>
|
|
235
233
|
<h3
|
|
236
234
|
className={`${isSelected ? 'font-bold' : 'font-medium'} text-gray-800`}
|
|
@@ -284,10 +282,6 @@ export default function Menu({ menu, onClick, pages = [] }: MenuProps) {
|
|
|
284
282
|
href={linkUrl}
|
|
285
283
|
onClick={(e) => {
|
|
286
284
|
e.preventDefault();
|
|
287
|
-
if (isPaigeLoading) {
|
|
288
|
-
console.log('Navigation blocked: Paige is currently processing a request');
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
285
|
setSelectedPage(item.page);
|
|
292
286
|
onClick?.();
|
|
293
287
|
}}
|
package/components/profile.tsx
CHANGED
|
@@ -9,9 +9,9 @@ checked in the system build settings. It is safe to modify this file without it
|
|
|
9
9
|
import { useState, useEffect } from 'react';
|
|
10
10
|
|
|
11
11
|
interface UserProfile {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
id: string;
|
|
13
|
+
username: string;
|
|
14
|
+
avatarurl: string | null;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export default function Profile() {
|
|
@@ -23,7 +23,8 @@ export default function Profile() {
|
|
|
23
23
|
const fetchProfile = async () => {
|
|
24
24
|
try {
|
|
25
25
|
const response = await fetch('/api/Auth', {
|
|
26
|
-
method: 'GET'
|
|
26
|
+
method: 'GET',
|
|
27
|
+
credentials: 'include'
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
if (!response.ok) {
|
|
@@ -39,9 +40,9 @@ export default function Profile() {
|
|
|
39
40
|
// Use the user object from the response
|
|
40
41
|
const userData = data.user;
|
|
41
42
|
setProfile({
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
id: userData.userid,
|
|
44
|
+
username: userData.username,
|
|
45
|
+
avatarurl: userData.avatarurl
|
|
45
46
|
});
|
|
46
47
|
} catch (err) {
|
|
47
48
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
|
@@ -68,17 +69,17 @@ export default function Profile() {
|
|
|
68
69
|
return (
|
|
69
70
|
<div className="max-w-md mx-auto mt-8 p-6 bg-white rounded-lg shadow-md">
|
|
70
71
|
<div className="flex flex-col items-center">
|
|
71
|
-
{profile.
|
|
72
|
+
{profile.avatarurl && (
|
|
72
73
|
<img
|
|
73
|
-
src={profile.
|
|
74
|
+
src={profile.avatarurl}
|
|
74
75
|
alt="Profile avatar"
|
|
75
76
|
className="w-32 h-32 rounded-full mb-4"
|
|
76
77
|
referrerPolicy="no-referrer"
|
|
77
78
|
crossOrigin="anonymous"
|
|
78
79
|
/>
|
|
79
80
|
)}
|
|
80
|
-
<h2 className="text-2xl font-semibold text-gray-800">{profile.
|
|
81
|
-
<p className="text-gray-500 mt-2">User ID: {profile.
|
|
81
|
+
<h2 className="text-2xl font-semibold text-gray-800">{profile.username}</h2>
|
|
82
|
+
<p className="text-gray-500 mt-2">User ID: {profile.id}</p>
|
|
82
83
|
</div>
|
|
83
84
|
</div>
|
|
84
85
|
);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Resend verification email endpoint
|
|
3
|
+
* Allows users to request a new verification email
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { NextResponse } from 'next/server';
|
|
7
|
+
import { regenerateVerificationToken } from '../../../db-password-auth';
|
|
8
|
+
import { send_email } from '../../../storage/email';
|
|
9
|
+
|
|
10
|
+
// Email validation regex
|
|
11
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
12
|
+
|
|
13
|
+
export async function POST(request: Request) {
|
|
14
|
+
try {
|
|
15
|
+
const { email } = await request.json();
|
|
16
|
+
|
|
17
|
+
// Validate email format
|
|
18
|
+
if (!email || !emailRegex.test(email)) {
|
|
19
|
+
return NextResponse.json(
|
|
20
|
+
{ error: 'Invalid email address' },
|
|
21
|
+
{ status: 400 }
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Regenerate verification token
|
|
26
|
+
const result = await regenerateVerificationToken(email);
|
|
27
|
+
|
|
28
|
+
if (!result) {
|
|
29
|
+
// Don't reveal whether the email exists or not
|
|
30
|
+
return NextResponse.json({
|
|
31
|
+
success: true,
|
|
32
|
+
message: 'If an unverified account exists with this email, a verification email has been sent.'
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { verificationToken } = result;
|
|
37
|
+
|
|
38
|
+
// Get site domain from environment or default
|
|
39
|
+
const siteDomain = process.env.SITE_DOMAIN || 'https://sitepaige.com';
|
|
40
|
+
const verificationUrl = `${siteDomain}/api/Auth/verify-email?token=${verificationToken}`;
|
|
41
|
+
|
|
42
|
+
// Send verification email
|
|
43
|
+
try {
|
|
44
|
+
const emailHtml = `
|
|
45
|
+
<!DOCTYPE html>
|
|
46
|
+
<html>
|
|
47
|
+
<head>
|
|
48
|
+
<style>
|
|
49
|
+
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
|
50
|
+
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
51
|
+
.button {
|
|
52
|
+
display: inline-block;
|
|
53
|
+
padding: 12px 24px;
|
|
54
|
+
background-color: #f0f0f0;
|
|
55
|
+
color: #000000;
|
|
56
|
+
text-decoration: none;
|
|
57
|
+
border: 1px solid #cccccc;
|
|
58
|
+
border-radius: 4px;
|
|
59
|
+
}
|
|
60
|
+
.footer { margin-top: 30px; font-size: 12px; color: #666; }
|
|
61
|
+
</style>
|
|
62
|
+
</head>
|
|
63
|
+
<body>
|
|
64
|
+
<div class="container">
|
|
65
|
+
<h2>Verify Your Email Address</h2>
|
|
66
|
+
<p>You requested a new verification email for your account at ${siteDomain}. Please verify your email address by clicking the button below:</p>
|
|
67
|
+
<p style="margin: 30px 0;">
|
|
68
|
+
<a href="${verificationUrl}" class="button">Verify Email Address</a>
|
|
69
|
+
</p>
|
|
70
|
+
<p>Or copy and paste this link into your browser:</p>
|
|
71
|
+
<p style="word-break: break-all; color: #0066cc;">${verificationUrl}</p>
|
|
72
|
+
<p>This link will expire in 24 hours.</p>
|
|
73
|
+
<div class="footer">
|
|
74
|
+
<p>If you didn't request this email, you can safely ignore it.</p>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</body>
|
|
78
|
+
</html>
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
const emailText = `
|
|
82
|
+
Verify Your Email Address
|
|
83
|
+
|
|
84
|
+
You requested a new verification email for your account at ${siteDomain}. Please verify your email address by clicking the link below:
|
|
85
|
+
|
|
86
|
+
${verificationUrl}
|
|
87
|
+
|
|
88
|
+
This link will expire in 24 hours.
|
|
89
|
+
|
|
90
|
+
If you didn't request this email, you can safely ignore it.
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
await send_email({
|
|
94
|
+
to: email,
|
|
95
|
+
from: process.env.EMAIL_FROM || 'noreply@sitepaige.com',
|
|
96
|
+
subject: 'Verify your email address',
|
|
97
|
+
html: emailHtml,
|
|
98
|
+
text: emailText
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return NextResponse.json({
|
|
102
|
+
success: true,
|
|
103
|
+
message: 'Verification email sent successfully! Please check your email.'
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
} catch (emailError) {
|
|
107
|
+
console.error('Failed to send verification email:', emailError);
|
|
108
|
+
return NextResponse.json({
|
|
109
|
+
success: false,
|
|
110
|
+
error: 'Failed to send verification email. Please try again later.'
|
|
111
|
+
}, { status: 500 });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
} catch (error: any) {
|
|
115
|
+
console.error('Resend verification error:', error);
|
|
116
|
+
|
|
117
|
+
if (error.message === 'Email is already verified') {
|
|
118
|
+
return NextResponse.json(
|
|
119
|
+
{ error: 'This email is already verified. Please sign in.' },
|
|
120
|
+
{ status: 400 }
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Generic response to avoid revealing whether email exists
|
|
125
|
+
return NextResponse.json({
|
|
126
|
+
success: true,
|
|
127
|
+
message: 'If an unverified account exists with this email, a verification email has been sent.'
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|