sitepaige-mcp-server 1.1.0 → 1.2.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.
@@ -45,7 +45,6 @@ const LoginSection: React.FC<LoginSectionProps> = ({ websiteLanguage = 'English'
45
45
  const [userData, setUserData] = useState<UserData | null>(null);
46
46
  const [isLoading, setIsLoading] = useState(true);
47
47
  const [isAuthenticated, setIsAuthenticated] = useState(false);
48
- const [profileMenuItems, setProfileMenuItems] = useState<MenuItem[]>([]);
49
48
  const userMenuRef = useRef<HTMLDivElement>(null);
50
49
 
51
50
  // Hardcoded values
@@ -53,33 +52,16 @@ const LoginSection: React.FC<LoginSectionProps> = ({ websiteLanguage = 'English'
53
52
  const align = 'Right' as "Left" | "Center" | "Right";
54
53
  const mobileViewMode = 'desktop' as 'desktop' | 'phone' | 'sm-tablet' | 'lg-tablet';
55
54
 
56
- // Load profile menu items from localStorage
57
- useEffect(() => {
58
- const loadProfileMenuItems = () => {
59
- try {
60
- const storedMenuData = localStorage.getItem('blueprint_profile_menu');
61
- if (storedMenuData) {
62
- const menuData = JSON.parse(storedMenuData);
63
- setProfileMenuItems(menuData);
64
- } else {
65
- // Default fallback menu items
66
- setProfileMenuItems([
67
- { label: 'Profile', link: '/profile' },
68
- { label: 'Settings', link: '/settings' }
69
- ]);
70
- }
71
- } catch (error) {
72
- console.error('Failed to load profile menu from localStorage:', error);
73
- // Default fallback menu items
74
- setProfileMenuItems([
75
- { label: 'Profile', link: '/profile' },
76
- { label: 'Settings', link: '/settings' }
77
- ]);
78
- }
79
- };
80
-
81
- loadProfileMenuItems();
82
- }, []);
55
+ // Build menu items based on user level
56
+ const profileMenuItems: MenuItem[] = [
57
+ { label: 'Dashboard', link: '/dashboard' },
58
+ { label: 'Profile', link: '/profile' }
59
+ ];
60
+
61
+ // Add Admin Dashboard only if user is an admin (userlevel == 2)
62
+ if (userData?.userlevel === 2) {
63
+ profileMenuItems.push({ label: 'Admin Dashboard', link: '/admin_dashboard' });
64
+ }
83
65
 
84
66
  // Fetch user authentication status and data
85
67
  useEffect(() => {
@@ -1,86 +1,342 @@
1
- /*
2
- Sitepaige Components v1.0.0
3
- Sitepaige components are automatically added to your project the first time it is built, and are only added again if the "Build Components" button is
4
- checked in the system build settings. It is safe to modify this file without it being overwritten unless that setting is selected.
5
- */
6
-
7
1
  'use client';
8
2
 
9
3
  import { useState, useEffect } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ // import { useCsrfToken } from './csrf-provider'; // Disabled for now
10
6
 
11
- interface UserProfile {
12
- id: string;
7
+ interface UserData {
8
+ userid: string;
13
9
  username: string;
14
- avatarurl: string | null;
10
+ avatarurl?: string;
11
+ email?: string;
12
+ userlevel: number;
13
+ source: string;
15
14
  }
16
15
 
17
16
  export default function Profile() {
18
- const [profile, setProfile] = useState<UserProfile | null>(null);
17
+ const router = useRouter();
18
+ // const csrfToken = useCsrfToken(); // Disabled for now
19
+ const [userData, setUserData] = useState<UserData | null>(null);
19
20
  const [loading, setLoading] = useState(true);
20
21
  const [error, setError] = useState<string | null>(null);
22
+ const [success, setSuccess] = useState<string | null>(null);
23
+
24
+ // Password change state
25
+ const [currentPassword, setCurrentPassword] = useState('');
26
+ const [newPassword, setNewPassword] = useState('');
27
+ const [confirmPassword, setConfirmPassword] = useState('');
28
+ const [isChangingPassword, setIsChangingPassword] = useState(false);
29
+
30
+ // Account deletion state
31
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
32
+ const [deleteConfirmText, setDeleteConfirmText] = useState('');
33
+ const [isDeleting, setIsDeleting] = useState(false);
21
34
 
22
35
  useEffect(() => {
23
- const fetchProfile = async () => {
36
+ fetchUserData();
37
+ }, []);
38
+
39
+ const fetchUserData = async () => {
24
40
  try {
41
+ setLoading(true);
25
42
  const response = await fetch('/api/Auth', {
26
- method: 'GET',
27
43
  credentials: 'include'
28
44
  });
29
45
 
30
46
  if (!response.ok) {
31
- throw new Error('Failed to fetch profile');
47
+ throw new Error('Failed to fetch user data');
32
48
  }
33
49
 
34
50
  const data = await response.json();
35
-
36
- if (!data.user) {
37
- throw new Error('User not authenticated');
38
- }
39
-
40
- // Use the user object from the response
41
- const userData = data.user;
42
- setProfile({
43
- id: userData.userid,
44
- username: userData.username,
45
- avatarurl: userData.avatarurl
46
- });
51
+ if (data.user) {
52
+ setUserData(data.user);
53
+ } else {
54
+ // Not authenticated, redirect to login
55
+ router.push('/login');
56
+ }
47
57
  } catch (err) {
48
- setError(err instanceof Error ? err.message : 'An error occurred');
58
+ setError(err instanceof Error ? err.message : 'Failed to load profile');
49
59
  } finally {
50
60
  setLoading(false);
51
61
  }
52
62
  };
53
63
 
54
- fetchProfile();
55
- }, []);
64
+ const handlePasswordChange = async (e: React.FormEvent) => {
65
+ e.preventDefault();
66
+ setError(null);
67
+ setSuccess(null);
68
+
69
+ // Validate passwords
70
+ if (newPassword !== confirmPassword) {
71
+ setError('Passwords do not match');
72
+ return;
73
+ }
74
+
75
+ if (newPassword.length < 8) {
76
+ setError('Password must be at least 8 characters long');
77
+ return;
78
+ }
79
+
80
+ setIsChangingPassword(true);
81
+
82
+ try {
83
+ const response = await fetch('/api/profile', {
84
+ method: 'PUT',
85
+ headers: {
86
+ 'Content-Type': 'application/json'
87
+ // 'X-CSRF-Token': csrfToken || '' // Disabled for now
88
+ },
89
+ credentials: 'include',
90
+ body: JSON.stringify({
91
+ currentPassword,
92
+ newPassword
93
+ })
94
+ });
95
+
96
+ const data = await response.json();
97
+
98
+ if (!response.ok) {
99
+ throw new Error(data.error || 'Failed to update password');
100
+ }
101
+
102
+ setSuccess('Password updated successfully');
103
+ setCurrentPassword('');
104
+ setNewPassword('');
105
+ setConfirmPassword('');
106
+ } catch (err) {
107
+ setError(err instanceof Error ? err.message : 'Failed to update password');
108
+ } finally {
109
+ setIsChangingPassword(false);
110
+ }
111
+ };
56
112
 
57
- if (loading) {
58
- return <div className="flex justify-center items-center min-h-[200px]">Loading...</div>;
59
- }
113
+ const handleDeleteAccount = async () => {
114
+ if (deleteConfirmText !== 'DELETE') {
115
+ setError('Please type DELETE to confirm account deletion');
116
+ return;
117
+ }
118
+
119
+ setIsDeleting(true);
120
+ setError(null);
121
+
122
+ try {
123
+ const response = await fetch('/api/profile', {
124
+ method: 'DELETE',
125
+ headers: {
126
+ 'Content-Type': 'application/json'
127
+ // 'X-CSRF-Token': csrfToken || '' // Disabled for now
128
+ },
129
+ credentials: 'include',
130
+ body: JSON.stringify({
131
+ confirmDelete: true
132
+ })
133
+ });
134
+
135
+ const data = await response.json();
136
+
137
+ if (!response.ok) {
138
+ throw new Error(data.error || 'Failed to delete account');
139
+ }
140
+
141
+ // Redirect to home page after successful deletion
142
+ window.location.href = '/';
143
+ } catch (err) {
144
+ setError(err instanceof Error ? err.message : 'Failed to delete account');
145
+ setIsDeleting(false);
146
+ }
147
+ };
148
+
149
+ const getUserLevelName = (level: number) => {
150
+ switch (level) {
151
+ case 0: return 'Guest';
152
+ case 1: return 'Registered User';
153
+ case 2: return 'Admin';
154
+ default: return 'Unknown';
155
+ }
156
+ };
157
+
158
+ const getAuthMethodName = (source: string) => {
159
+ switch (source) {
160
+ case 'userpass': return 'Email/Password';
161
+ case 'google': return 'Google';
162
+ case 'facebook': return 'Facebook';
163
+ case 'apple': return 'Apple';
164
+ case 'github': return 'GitHub';
165
+ default: return source;
166
+ }
167
+ };
60
168
 
61
- if (error) {
62
- return <div className="text-red-500 text-center">{error}</div>;
169
+ if (loading) {
170
+ return (
171
+ <div className="flex items-center justify-center min-h-screen">
172
+ <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-600"></div>
173
+ </div>
174
+ );
63
175
  }
64
176
 
65
- if (!profile) {
66
- return <div className="text-center">No profile found</div>;
177
+ if (!userData) {
178
+ return null; // Will redirect to login
67
179
  }
68
180
 
69
181
  return (
70
- <div className="max-w-md mx-auto mt-8 p-6 bg-white rounded-lg shadow-md">
71
- <div className="flex flex-col items-center">
72
- {profile.avatarurl && (
73
- <img
74
- src={profile.avatarurl}
75
- alt="Profile avatar"
76
- className="w-32 h-32 rounded-full mb-4"
182
+ <div className="container mx-auto px-4 py-8 max-w-4xl">
183
+ <h1 className="text-3xl font-bold mb-8">Profile Settings</h1>
184
+
185
+ {/* User Information */}
186
+ <div className="bg-white rounded-lg shadow p-6 mb-8">
187
+ <h2 className="text-xl font-semibold mb-4">User Information</h2>
188
+ <div className="grid md:grid-cols-2 gap-4">
189
+ <div className="flex items-center space-x-4">
190
+ {userData.avatarurl ? (
191
+ <img
192
+ src={userData.avatarurl}
193
+ alt={userData.username}
194
+ className="w-16 h-16 rounded-full"
77
195
  referrerPolicy="no-referrer"
78
196
  crossOrigin="anonymous"
79
197
  />
80
- )}
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>
198
+ ) : (
199
+ <div className="w-16 h-16 rounded-full bg-gray-300 flex items-center justify-center">
200
+ <span className="text-2xl text-gray-600">
201
+ {userData.username.charAt(0).toUpperCase()}
202
+ </span>
203
+ </div>
204
+ )}
205
+ <div>
206
+ <p className="font-semibold">{userData.username}</p>
207
+ {userData.email && (
208
+ <p className="text-sm text-gray-600">{userData.email}</p>
209
+ )}
210
+ </div>
211
+ </div>
212
+ <div className="space-y-2">
213
+ <p><span className="font-semibold">User Level:</span> {getUserLevelName(userData.userlevel)}</p>
214
+ <p><span className="font-semibold">Auth Method:</span> {getAuthMethodName(userData.source)}</p>
215
+ </div>
216
+ </div>
217
+ </div>
218
+
219
+ {/* Error and Success Messages */}
220
+ {error && (
221
+ <div className="mb-4 p-4 bg-red-50 border border-red-200 text-red-700 rounded">
222
+ {error}
223
+ </div>
224
+ )}
225
+ {success && (
226
+ <div className="mb-4 p-4 bg-green-50 border border-green-200 text-green-700 rounded">
227
+ {success}
228
+ </div>
229
+ )}
230
+
231
+ {/* Password Change Section - Only for email/password users */}
232
+ {userData.source === 'userpass' && (
233
+ <div className="bg-white rounded-lg shadow p-6 mb-8">
234
+ <h2 className="text-xl font-semibold mb-4">Change Password</h2>
235
+ <form onSubmit={handlePasswordChange} className="space-y-4">
236
+ <div>
237
+ <label htmlFor="currentPassword" className="block text-sm font-medium text-gray-700 mb-1">
238
+ Current Password
239
+ </label>
240
+ <input
241
+ type="password"
242
+ id="currentPassword"
243
+ value={currentPassword}
244
+ onChange={(e) => setCurrentPassword(e.target.value)}
245
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
246
+ required
247
+ />
248
+ </div>
249
+ <div>
250
+ <label htmlFor="newPassword" className="block text-sm font-medium text-gray-700 mb-1">
251
+ New Password
252
+ </label>
253
+ <input
254
+ type="password"
255
+ id="newPassword"
256
+ value={newPassword}
257
+ onChange={(e) => setNewPassword(e.target.value)}
258
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
259
+ required
260
+ minLength={8}
261
+ />
262
+ <p className="text-xs text-gray-500 mt-1">Must be at least 8 characters long</p>
263
+ </div>
264
+ <div>
265
+ <label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 mb-1">
266
+ Confirm New Password
267
+ </label>
268
+ <input
269
+ type="password"
270
+ id="confirmPassword"
271
+ value={confirmPassword}
272
+ onChange={(e) => setConfirmPassword(e.target.value)}
273
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
274
+ required
275
+ />
276
+ </div>
277
+ <button
278
+ type="submit"
279
+ disabled={isChangingPassword}
280
+ className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
281
+ >
282
+ {isChangingPassword ? 'Updating...' : 'Update Password'}
283
+ </button>
284
+ </form>
285
+ </div>
286
+ )}
287
+
288
+ {/* Delete Account Section */}
289
+ <div className="bg-white rounded-lg shadow p-6">
290
+ <h2 className="text-xl font-semibold mb-4 text-red-600">Danger Zone</h2>
291
+ <div className="border border-red-200 rounded-md p-4">
292
+ <h3 className="font-semibold mb-2">Delete Account</h3>
293
+ <p className="text-sm text-gray-600 mb-4">
294
+ Once you delete your account, there is no going back. Please be certain.
295
+ </p>
296
+
297
+ {!showDeleteConfirm ? (
298
+ <button
299
+ onClick={() => setShowDeleteConfirm(true)}
300
+ className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700"
301
+ >
302
+ Delete Account
303
+ </button>
304
+ ) : (
305
+ <div className="space-y-4">
306
+ <p className="text-sm font-semibold">
307
+ To confirm deletion, please type <span className="font-mono bg-gray-100 px-1">DELETE</span> below:
308
+ </p>
309
+ <input
310
+ type="text"
311
+ value={deleteConfirmText}
312
+ onChange={(e) => setDeleteConfirmText(e.target.value)}
313
+ placeholder="Type DELETE to confirm"
314
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-red-500"
315
+ />
316
+ <div className="flex space-x-4">
317
+ <button
318
+ onClick={handleDeleteAccount}
319
+ disabled={isDeleting || deleteConfirmText !== 'DELETE'}
320
+ className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
321
+ >
322
+ {isDeleting ? 'Deleting...' : 'Confirm Delete'}
323
+ </button>
324
+ <button
325
+ onClick={() => {
326
+ setShowDeleteConfirm(false);
327
+ setDeleteConfirmText('');
328
+ setError(null);
329
+ }}
330
+ disabled={isDeleting}
331
+ className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400"
332
+ >
333
+ Cancel
334
+ </button>
335
+ </div>
336
+ </div>
337
+ )}
338
+ </div>
83
339
  </div>
84
340
  </div>
85
341
  );
86
- }
342
+ }
@@ -36,7 +36,7 @@ export async function POST(request: Request) {
36
36
  const { verificationToken } = result;
37
37
 
38
38
  // Get site domain from environment or default
39
- const siteDomain = process.env.SITE_DOMAIN || 'https://sitepaige.com';
39
+ const siteDomain = process.env.NODE_ENV === 'production' ? process.env.DOMAIN : process.env.LOCAL_DOMAIN;
40
40
  const verificationUrl = `${siteDomain}/api/Auth/verify-email?token=${verificationToken}`;
41
41
 
42
42
  // Send verification email
@@ -37,7 +37,7 @@ export async function POST(request: Request) {
37
37
  const { passwordAuth, verificationToken } = await createPasswordAuth(email, password);
38
38
 
39
39
  // Get site domain from environment or default
40
- const siteDomain = process.env.SITE_DOMAIN || 'https://sitepaige.com';
40
+ const siteDomain = process.env.NODE_ENV === 'production' ? process.env.DOMAIN : process.env.LOCAL_DOMAIN;
41
41
  const verificationUrl = `${siteDomain}/api/Auth/verify-email?token=${verificationToken}`;
42
42
 
43
43
  // Send verification email
@@ -86,7 +86,7 @@ export async function GET(request: Request) {
86
86
  });
87
87
 
88
88
  // Redirect to home page or dashboard with success message
89
- const siteDomain = process.env.SITE_DOMAIN || 'https://sitepaige.com';
89
+ const siteDomain = process.env.NODE_ENV === 'production' ? process.env.DOMAIN : process.env.LOCAL_DOMAIN;
90
90
  const redirectUrl = new URL('/', siteDomain);
91
91
  redirectUrl.searchParams.set('verified', 'true');
92
92
 
@@ -96,7 +96,7 @@ export async function GET(request: Request) {
96
96
  console.error('Email verification error:', error);
97
97
 
98
98
  // Redirect to home with error
99
- const siteDomain = process.env.SITE_DOMAIN || 'https://sitepaige.com';
99
+ const siteDomain = process.env.NODE_ENV === 'production' ? process.env.DOMAIN : process.env.LOCAL_DOMAIN;
100
100
  const redirectUrl = new URL('/', siteDomain);
101
101
  redirectUrl.searchParams.set('error', 'verification_failed');
102
102
 
@@ -0,0 +1,154 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { cookies } from 'next/headers';
3
+ import { validateSession } from '../../db-users';
4
+ import { updatePassword, getPasswordAuthByEmail } from '../../db-password-auth';
5
+ import { deleteUser } from '../../db-users';
6
+
7
+ // Handle password change
8
+ export async function PUT(request: NextRequest) {
9
+ try {
10
+ // Get session token from cookies
11
+ const sessionCookie = await cookies();
12
+ const sessionToken = sessionCookie.get('session_id')?.value;
13
+
14
+ if (!sessionToken) {
15
+ return NextResponse.json(
16
+ { error: 'Not authenticated' },
17
+ { status: 401 }
18
+ );
19
+ }
20
+
21
+ // Validate session and get user details
22
+ const sessionData = await validateSession(sessionToken);
23
+
24
+ if (!sessionData.valid || !sessionData.user) {
25
+ return NextResponse.json(
26
+ { error: 'Invalid session' },
27
+ { status: 401 }
28
+ );
29
+ }
30
+
31
+ const { currentPassword, newPassword } = await request.json();
32
+
33
+ if (!currentPassword || !newPassword) {
34
+ return NextResponse.json(
35
+ { error: 'Current password and new password are required' },
36
+ { status: 400 }
37
+ );
38
+ }
39
+
40
+ // Check if user is using password authentication
41
+ if (sessionData.user.source !== 'userpass') {
42
+ return NextResponse.json(
43
+ { error: 'Password changes are only available for email/password accounts' },
44
+ { status: 400 }
45
+ );
46
+ }
47
+
48
+ // Validate password requirements
49
+ if (newPassword.length < 8) {
50
+ return NextResponse.json(
51
+ { error: 'Password must be at least 8 characters long' },
52
+ { status: 400 }
53
+ );
54
+ }
55
+
56
+ // Check if user has an email (required for password auth)
57
+ if (!sessionData.user.email) {
58
+ return NextResponse.json(
59
+ { error: 'No email associated with this account' },
60
+ { status: 400 }
61
+ );
62
+ }
63
+
64
+ // Update the password
65
+ const success = await updatePassword(
66
+ sessionData.user.email,
67
+ currentPassword,
68
+ newPassword
69
+ );
70
+
71
+ if (!success) {
72
+ return NextResponse.json(
73
+ { error: 'Current password is incorrect' },
74
+ { status: 401 }
75
+ );
76
+ }
77
+
78
+ return NextResponse.json({
79
+ success: true,
80
+ message: 'Password updated successfully'
81
+ });
82
+
83
+ } catch (error) {
84
+ console.error('Password update error:', error);
85
+ return NextResponse.json(
86
+ { error: error instanceof Error ? error.message : 'Failed to update password' },
87
+ { status: 500 }
88
+ );
89
+ }
90
+ }
91
+
92
+ // Handle account deletion
93
+ export async function DELETE(request: NextRequest) {
94
+ try {
95
+ // Get session token from cookies
96
+ const sessionCookie = await cookies();
97
+ const sessionToken = sessionCookie.get('session_id')?.value;
98
+
99
+ if (!sessionToken) {
100
+ return NextResponse.json(
101
+ { error: 'Not authenticated' },
102
+ { status: 401 }
103
+ );
104
+ }
105
+
106
+ // Validate session and get user details
107
+ const sessionData = await validateSession(sessionToken);
108
+
109
+ if (!sessionData.valid || !sessionData.user) {
110
+ return NextResponse.json(
111
+ { error: 'Invalid session' },
112
+ { status: 401 }
113
+ );
114
+ }
115
+
116
+ const { confirmDelete } = await request.json();
117
+
118
+ if (confirmDelete !== true) {
119
+ return NextResponse.json(
120
+ { error: 'Please confirm account deletion' },
121
+ { status: 400 }
122
+ );
123
+ }
124
+
125
+ // Delete the user account
126
+ try {
127
+ await deleteUser(sessionData.user.userid);
128
+ } catch (error: any) {
129
+ if (error.message && error.message.includes('last admin')) {
130
+ return NextResponse.json(
131
+ { error: 'Cannot delete the last admin account' },
132
+ { status: 400 }
133
+ );
134
+ }
135
+ throw error;
136
+ }
137
+
138
+ // Clear the session cookie
139
+ const response = NextResponse.json({
140
+ success: true,
141
+ message: 'Account deleted successfully'
142
+ });
143
+
144
+ response.cookies.delete('session_id');
145
+ return response;
146
+
147
+ } catch (error) {
148
+ console.error('Account deletion error:', error);
149
+ return NextResponse.json(
150
+ { error: error instanceof Error ? error.message : 'Failed to delete account' },
151
+ { status: 500 }
152
+ );
153
+ }
154
+ }
@@ -111,7 +111,7 @@ export async function upsertUser(
111
111
  } else {
112
112
  // Check if this is the first user (should be admin)
113
113
  const allUsers = await db_query(client, "SELECT COUNT(*) as count FROM users");
114
- const isFirstUser = allUsers[0].count === 0;
114
+ const isFirstUser = Number(allUsers[0].count) === 0;
115
115
 
116
116
  // Create new user
117
117
  const userId = crypto.randomUUID();
@@ -0,0 +1,6 @@
1
+ import Profile from '../../components/profile';
2
+ import React from 'react';
3
+
4
+ export default function ProfilePage() {
5
+ return <Profile />;
6
+ }