sitepaige-mcp-server 1.0.0 → 1.0.3

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.
Files changed (40) hide show
  1. package/components/form.tsx +133 -6
  2. package/components/login.tsx +249 -21
  3. package/components/menu.tsx +128 -3
  4. package/components/testimonial.tsx +1 -1
  5. package/defaultapp/api/Auth/resend-verification/route.ts +130 -0
  6. package/defaultapp/api/Auth/route.ts +105 -13
  7. package/defaultapp/api/Auth/signup/route.ts +133 -0
  8. package/defaultapp/api/Auth/verify-email/route.ts +105 -0
  9. package/defaultapp/db-password-auth.ts +362 -0
  10. package/defaultapp/db-users.ts +11 -11
  11. package/defaultapp/middleware.ts +15 -17
  12. package/defaultapp/storage/email.ts +162 -0
  13. package/dist/blueprintWriter.js +15 -1
  14. package/dist/blueprintWriter.js.map +1 -1
  15. package/dist/components/form.tsx +133 -6
  16. package/dist/components/login.tsx +249 -21
  17. package/dist/components/menu.tsx +128 -3
  18. package/dist/components/testimonial.tsx +1 -1
  19. package/dist/defaultapp/api/Auth/resend-verification/route.ts +130 -0
  20. package/dist/defaultapp/api/Auth/route.ts +105 -13
  21. package/dist/defaultapp/api/Auth/signup/route.ts +133 -0
  22. package/dist/defaultapp/api/Auth/verify-email/route.ts +105 -0
  23. package/dist/defaultapp/db-password-auth.ts +362 -0
  24. package/dist/defaultapp/db-users.ts +11 -11
  25. package/dist/defaultapp/middleware.ts +15 -17
  26. package/dist/defaultapp/storage/email.ts +162 -0
  27. package/dist/generators/apis.js +1 -0
  28. package/dist/generators/apis.js.map +1 -1
  29. package/dist/generators/defaultapp.js +2 -2
  30. package/dist/generators/defaultapp.js.map +1 -1
  31. package/dist/generators/env-example-template.txt +27 -0
  32. package/dist/generators/images.js +38 -13
  33. package/dist/generators/images.js.map +1 -1
  34. package/dist/generators/pages.js +1 -1
  35. package/dist/generators/pages.js.map +1 -1
  36. package/dist/generators/sql.js +19 -0
  37. package/dist/generators/sql.js.map +1 -1
  38. package/dist/generators/views.js +29 -14
  39. package/dist/generators/views.js.map +1 -1
  40. package/package.json +3 -2
@@ -26,8 +26,10 @@ interface MenuItem {
26
26
  page: string | null;
27
27
  menu: string | null;
28
28
  untouchable: boolean;
29
- link_type?: 'page' | 'external';
29
+ link_type?: 'page' | 'external' | 'file';
30
30
  external_url?: string | null;
31
+ file_id?: string | null;
32
+ file_name?: string | null;
31
33
  hiddenOnDesktop?: boolean; // New field to hide item on desktop (shows in icon bar instead)
32
34
  }
33
35
 
@@ -57,6 +59,8 @@ export default function Menu({ menu, onClick, pages = [] }: MenuProps) {
57
59
  const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
58
60
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
59
61
  const [isMobile, setIsMobile] = useState(false);
62
+ const [selectedPage, setSelectedPage] = useState<string | null>(null);
63
+ const [isPaigeLoading, setIsPaigeLoading] = useState(false);
60
64
 
61
65
  // Handle case where menu is undefined/null
62
66
  if (!menu) {
@@ -88,7 +92,8 @@ export default function Menu({ menu, onClick, pages = [] }: MenuProps) {
88
92
  return () => window.removeEventListener('resize', checkMobile);
89
93
  }, []);
90
94
 
91
- const direction = menu.direction === 'vertical' ? 'vertical' : 'horizontal';
95
+ const direction = menu.direction === 'vertical' ? 'vertical' :
96
+ menu.direction === 'tiled' ? 'tiled' : 'horizontal';
92
97
 
93
98
  const renderMenuItem = (item: MenuItem, index: number) => {
94
99
  const handleClick = () => {
@@ -187,8 +192,128 @@ export default function Menu({ menu, onClick, pages = [] }: MenuProps) {
187
192
  );
188
193
  };
189
194
 
195
+ // Render a tiled menu item
196
+ const renderTiledMenuItem = (item: MenuItem, index: number) => {
197
+ const isSelected = item.page === selectedPage;
198
+
199
+ // Helper function to get font size value
200
+ const getFontSizeValue = (sizeClass: string) => {
201
+ const sizeMap: Record<string, string> = {
202
+ 'text-xs': '0.75rem',
203
+ 'text-sm': '0.875rem',
204
+ 'text-base': '1rem',
205
+ 'text-lg': '1.125rem',
206
+ 'text-xl': '1.25rem',
207
+ 'text-2xl': '1.5rem',
208
+ 'text-3xl': '1.875rem',
209
+ };
210
+ return sizeMap[sizeClass] || '1.25rem';
211
+ };
212
+
213
+ const tileContent = (
214
+ <div className={`
215
+ p-6
216
+ bg-white
217
+ border-2
218
+ border-gray-300
219
+ rounded-lg
220
+ shadow-md
221
+ hover:shadow-lg
222
+ hover:border-blue-500
223
+ transition-all
224
+ duration-200
225
+ cursor-pointer
226
+ text-center
227
+ h-full
228
+ flex
229
+ flex-col
230
+ items-center
231
+ justify-center
232
+ ${isSelected ? 'border-blue-600 bg-blue-50' : ''}
233
+ ${isPaigeLoading ? 'opacity-50 cursor-not-allowed' : ''}
234
+ `}>
235
+ <h3
236
+ className={`${isSelected ? 'font-bold' : 'font-medium'} text-gray-800`}
237
+ style={{ fontFamily: menu.font, fontSize: getFontSizeValue(menu.fontSize || 'text-xl') }}
238
+ >
239
+ {item.name}
240
+ </h3>
241
+ </div>
242
+ );
243
+
244
+ if (item.link_type === 'external' && item.external_url) {
245
+ return (
246
+ <a
247
+ key={item.name}
248
+ href={item.external_url}
249
+ target="_blank"
250
+ rel="noopener noreferrer"
251
+ className="block h-full"
252
+ >
253
+ {tileContent}
254
+ </a>
255
+ );
256
+ } else if (item.link_type === 'file' && item.file_name) {
257
+ return (
258
+ <a
259
+ key={item.name}
260
+ href={`/library/files/${item.file_name}`}
261
+ target="_blank"
262
+ rel="noopener noreferrer"
263
+ className="block h-full"
264
+ >
265
+ {tileContent}
266
+ </a>
267
+ );
268
+ } else if (item.page) {
269
+ const page = pages.find(p => p.id === item.page);
270
+ let linkUrl = '#';
271
+
272
+ if (page) {
273
+ let urlPath = page.name
274
+ .replace(/[^a-zA-Z0-9\s]/g, '')
275
+ .trim()
276
+ .replace(/\s+/g, '_')
277
+ .toLowerCase();
278
+ linkUrl = urlPath === 'home' || urlPath === 'index' ? '/' : `/${urlPath}`;
279
+ }
280
+
281
+ return (
282
+ <Link
283
+ key={item.name}
284
+ href={linkUrl}
285
+ onClick={(e) => {
286
+ e.preventDefault();
287
+ if (isPaigeLoading) {
288
+ console.log('Navigation blocked: Paige is currently processing a request');
289
+ return;
290
+ }
291
+ setSelectedPage(item.page);
292
+ onClick?.();
293
+ }}
294
+ className="block h-full"
295
+ >
296
+ {tileContent}
297
+ </Link>
298
+ );
299
+ } else {
300
+ return (
301
+ <div key={item.name} className="block h-full">
302
+ {tileContent}
303
+ </div>
304
+ );
305
+ }
306
+ };
307
+
190
308
  return (
191
309
  <>
310
+ {/* Tiled menu layout */}
311
+ {direction === 'tiled' && (
312
+ <div className={`grid gap-4 ${isMobile ? 'grid-cols-1' : 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4'}`}>
313
+ {menu.items?.map((item, index) => renderTiledMenuItem(item, index)) || []}
314
+ </div>
315
+ )}
316
+
192
317
  {/* Hamburger menu for mobile horizontal menus */}
193
318
  {isMobile && direction === 'horizontal' && (
194
319
  <div className="relative">
@@ -211,7 +336,7 @@ export default function Menu({ menu, onClick, pages = [] }: MenuProps) {
211
336
  )}
212
337
 
213
338
  {/* Regular menu for desktop or vertical menus */}
214
- {(!isMobile || direction === 'vertical') && (
339
+ {(!isMobile || direction === 'vertical') && direction !== 'tiled' && (
215
340
  <nav className={`${direction === 'horizontal' ? 'space-x-4' : 'flex flex-col'} ${menu.align === 'Left' ? 'justify-start' : menu.align === 'Center' ? 'justify-center' : menu.align === 'Right' ? 'justify-end' : ''}`}>
216
341
  {menu.items?.filter(item => !item.hiddenOnDesktop).map((item, index) => renderMenuItem(item, index)) || []}
217
342
  </nav>
@@ -112,7 +112,7 @@ export default function RTestimonial({ name, custom_view_description, design }:
112
112
  <div className="flex items-center">
113
113
  {testimonial.photoId && (
114
114
  <img
115
- src={`/api/image?imageid=${testimonial.photoId}`}
115
+ src={`/images/${testimonial.photoId}.jpg`}
116
116
  alt={testimonial.attribution}
117
117
  className="w-12 h-12 rounded-full object-cover mr-4"
118
118
  onError={(e) => {
@@ -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
+ }
@@ -10,7 +10,6 @@ import * as crypto from 'node:crypto';
10
10
 
11
11
  import { db_init, db_query } from '../../db';
12
12
  import { upsertUser, storeOAuthToken, validateSession, rotateSession } from '../../db-users';
13
- import { validateCsrfToken } from '../../csrf';
14
13
 
15
14
  type OAuthProvider = 'google' | 'facebook' | 'apple' | 'github';
16
15
 
@@ -32,11 +31,113 @@ export async function POST(request: Request) {
32
31
  const db = await db_init();
33
32
 
34
33
  try {
35
- const { code, provider } = await request.json();
34
+ const { code, provider, email, password } = await request.json();
36
35
 
37
- if (!code || !provider) {
36
+ if (!provider) {
38
37
  return NextResponse.json(
39
- { error: 'No authorization code or provider specified' },
38
+ { error: 'No provider specified' },
39
+ { status: 400 }
40
+ );
41
+ }
42
+
43
+ // Handle username/password authentication
44
+ if (provider === 'username') {
45
+ if (!email || !password) {
46
+ return NextResponse.json(
47
+ { error: 'Email and password are required' },
48
+ { status: 400 }
49
+ );
50
+ }
51
+
52
+ const { authenticateUser, createPasswordAuthTable } = await import('../../db-password-auth');
53
+ const { upsertUser } = await import('../../db-users');
54
+
55
+ // Ensure password auth table exists
56
+ await createPasswordAuthTable();
57
+
58
+ try {
59
+ // Authenticate the user
60
+ const authRecord = await authenticateUser(email, password);
61
+
62
+ if (!authRecord) {
63
+ return NextResponse.json(
64
+ { error: 'Invalid email or password' },
65
+ { status: 401 }
66
+ );
67
+ }
68
+
69
+ // Create or update user in the main Users table
70
+ const user = await upsertUser(
71
+ `password_${authRecord.id}`, // Unique OAuth ID for password users
72
+ 'username' as any, // Source type
73
+ email.split('@')[0], // Username from email
74
+ email,
75
+ undefined // No avatar for password auth
76
+ );
77
+
78
+ // Delete existing sessions for this user
79
+ const existingSessions = await db_query(db,
80
+ "SELECT ID FROM usersession WHERE userid = ?",
81
+ [user.userid]
82
+ );
83
+
84
+ if (existingSessions && existingSessions.length > 0) {
85
+ const sessionIds = existingSessions.map(session => session.ID);
86
+ const placeholders = sessionIds.map(() => '?').join(',');
87
+ await db_query(db, `DELETE FROM usersession WHERE ID IN (${placeholders})`, sessionIds);
88
+ }
89
+
90
+ // Generate secure session token and ID
91
+ const sessionId = crypto.randomUUID();
92
+ const sessionToken = crypto.randomBytes(32).toString('base64url');
93
+
94
+ // Create new session with secure token
95
+ await db_query(db,
96
+ "INSERT INTO usersession (ID, SessionToken, userid, ExpirationDate) VALUES (?, ?, ?, ?)",
97
+ [sessionId, sessionToken, user.userid, new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()]
98
+ );
99
+
100
+ // Set session cookie with secure token
101
+ const sessionCookie = await cookies();
102
+ sessionCookie.set({
103
+ name: 'session_id',
104
+ value: sessionToken,
105
+ httpOnly: true,
106
+ secure: process.env.NODE_ENV === 'production',
107
+ sameSite: process.env.NODE_ENV === 'production' ? 'strict' : 'lax',
108
+ maxAge: 30 * 24 * 60 * 60, // 30 days
109
+ path: '/',
110
+ });
111
+
112
+ // Create a completely clean object to avoid any database result object issues
113
+ const cleanUserData = {
114
+ userid: String(user.userid),
115
+ userName: String(user.UserName),
116
+ avatarURL: String(user.AvatarURL || ''),
117
+ userLevel: Number(user.UserLevel),
118
+ isAdmin: Number(user.UserLevel) === 2
119
+ };
120
+
121
+ return NextResponse.json({
122
+ success: true,
123
+ user: cleanUserData
124
+ });
125
+
126
+ } catch (error: any) {
127
+ if (error.message === 'Email not verified') {
128
+ return NextResponse.json(
129
+ { error: 'Please verify your email before logging in' },
130
+ { status: 403 }
131
+ );
132
+ }
133
+ throw error;
134
+ }
135
+ }
136
+
137
+ // Handle OAuth authentication
138
+ if (!code) {
139
+ return NextResponse.json(
140
+ { error: 'No authorization code specified' },
40
141
  { status: 400 }
41
142
  );
42
143
  }
@@ -293,15 +394,6 @@ export async function GET() {
293
394
  }
294
395
 
295
396
  export async function DELETE(request: Request) {
296
- // Validate CSRF token for logout
297
- const isValidCsrf = await validateCsrfToken(request);
298
- if (!isValidCsrf) {
299
- return NextResponse.json(
300
- { error: 'Invalid CSRF token' },
301
- { status: 403 }
302
- );
303
- }
304
-
305
397
  const db = await db_init();
306
398
 
307
399
  try {
@@ -0,0 +1,133 @@
1
+ /*
2
+ * Signup endpoint for username/password authentication
3
+ * Handles user registration with email verification
4
+ */
5
+
6
+ import { NextResponse } from 'next/server';
7
+ import { createPasswordAuth } from '../../../db-password-auth';
8
+ import { send_email } from '../../../storage/email';
9
+
10
+ // Email validation regex
11
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
12
+
13
+ // Password validation - at least 8 characters, one uppercase, one lowercase, one number
14
+ const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
15
+
16
+ export async function POST(request: Request) {
17
+ try {
18
+ const { email, password } = await request.json();
19
+
20
+ // Validate email format
21
+ if (!email || !emailRegex.test(email)) {
22
+ return NextResponse.json(
23
+ { error: 'Invalid email address' },
24
+ { status: 400 }
25
+ );
26
+ }
27
+
28
+ // Validate password strength
29
+ if (!password || !passwordRegex.test(password)) {
30
+ return NextResponse.json(
31
+ { error: 'Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, and one number' },
32
+ { status: 400 }
33
+ );
34
+ }
35
+
36
+ // Create password auth record
37
+ const { passwordAuth, verificationToken } = await createPasswordAuth(email, password);
38
+
39
+ // Get site domain from environment or default
40
+ const siteDomain = process.env.SITE_DOMAIN || 'https://sitepaige.com';
41
+ const verificationUrl = `${siteDomain}/api/Auth/verify-email?token=${verificationToken}`;
42
+
43
+ // Send verification email
44
+ try {
45
+ const emailHtml = `
46
+ <!DOCTYPE html>
47
+ <html>
48
+ <head>
49
+ <style>
50
+ body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
51
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
52
+ .button {
53
+ display: inline-block;
54
+ padding: 12px 24px;
55
+ background-color: #f0f0f0;
56
+ color: #000000;
57
+ text-decoration: none;
58
+ border: 1px solid #cccccc;
59
+ border-radius: 4px;
60
+ }
61
+ .footer { margin-top: 30px; font-size: 12px; color: #666; }
62
+ </style>
63
+ </head>
64
+ <body>
65
+ <div class="container">
66
+ <h2>Welcome to ${siteDomain}!</h2>
67
+ <p>Thank you for signing up. Please verify your email address by clicking the button below:</p>
68
+ <p style="margin: 30px 0;">
69
+ <a href="${verificationUrl}" class="button">Verify Email Address</a>
70
+ </p>
71
+ <p>Or copy and paste this link into your browser:</p>
72
+ <p style="word-break: break-all; color: #0066cc;">${verificationUrl}</p>
73
+ <p>This link will expire in 24 hours.</p>
74
+ <div class="footer">
75
+ <p>If you didn't create an account, you can safely ignore this email.</p>
76
+ </div>
77
+ </div>
78
+ </body>
79
+ </html>
80
+ `;
81
+
82
+ const emailText = `
83
+ Welcome to ${siteDomain}!
84
+
85
+ Thank you for signing up. Please verify your email address by clicking the link below:
86
+
87
+ ${verificationUrl}
88
+
89
+ This link will expire in 24 hours.
90
+
91
+ If you didn't create an account, you can safely ignore this email.
92
+ `;
93
+
94
+ await send_email({
95
+ to: email,
96
+ from: process.env.EMAIL_FROM || 'noreply@sitepaige.com',
97
+ subject: 'Verify your email address',
98
+ html: emailHtml,
99
+ text: emailText
100
+ });
101
+
102
+ return NextResponse.json({
103
+ success: true,
104
+ message: 'Registration successful! Please check your email to verify your account.'
105
+ });
106
+
107
+ } catch (emailError) {
108
+ console.error('Failed to send verification email:', emailError);
109
+ // Still return success but with a warning
110
+ return NextResponse.json({
111
+ success: true,
112
+ message: 'Registration successful! However, we could not send the verification email. Please contact support.',
113
+ warning: 'Email sending failed'
114
+ });
115
+ }
116
+
117
+ } catch (error: any) {
118
+ console.error('Signup error:', error);
119
+
120
+ // Handle specific errors
121
+ if (error.message === 'Email already registered') {
122
+ return NextResponse.json(
123
+ { error: 'This email is already registered. Please sign in instead.' },
124
+ { status: 409 }
125
+ );
126
+ }
127
+
128
+ return NextResponse.json(
129
+ { error: error.message || 'Signup failed' },
130
+ { status: 500 }
131
+ );
132
+ }
133
+ }
@@ -0,0 +1,105 @@
1
+ /*
2
+ * Email verification endpoint
3
+ * Handles email verification tokens from signup emails
4
+ */
5
+
6
+ import { NextResponse } from 'next/server';
7
+ import { verifyEmailWithToken } from '../../../db-password-auth';
8
+ import { upsertUser } from '../../../db-users';
9
+ import { db_init, db_query } from '../../../db';
10
+ import { cookies } from 'next/headers';
11
+ import * as crypto from 'node:crypto';
12
+
13
+ export async function GET(request: Request) {
14
+ try {
15
+ const { searchParams } = new URL(request.url);
16
+ const token = searchParams.get('token');
17
+
18
+ if (!token) {
19
+ return NextResponse.json(
20
+ { error: 'Verification token is required' },
21
+ { status: 400 }
22
+ );
23
+ }
24
+
25
+ // Verify the email with token
26
+ const authRecord = await verifyEmailWithToken(token);
27
+
28
+ if (!authRecord) {
29
+ return NextResponse.json(
30
+ { error: 'Invalid or expired verification token' },
31
+ { status: 400 }
32
+ );
33
+ }
34
+
35
+ // Create or update user in the main Users table
36
+ const user = await upsertUser(
37
+ `password_${authRecord.id}`, // Unique OAuth ID for password users
38
+ 'userpass' as any, // Source type
39
+ authRecord.email.split('@')[0], // Username from email
40
+ authRecord.email,
41
+ undefined // No avatar for password auth
42
+ );
43
+
44
+ if (!user) {
45
+ return NextResponse.json(
46
+ { error: 'Failed to create user account' },
47
+ { status: 500 }
48
+ );
49
+ }
50
+
51
+ // Auto-login the user after verification
52
+ const db = await db_init();
53
+
54
+ // Delete existing sessions for this user
55
+ const existingSessions = await db_query(db,
56
+ "SELECT ID FROM usersession WHERE userid = ?",
57
+ [user.userid]
58
+ );
59
+
60
+ if (existingSessions && existingSessions.length > 0) {
61
+ const sessionIds = existingSessions.map(session => session.ID);
62
+ const placeholders = sessionIds.map(() => '?').join(',');
63
+ await db_query(db, `DELETE FROM usersession WHERE ID IN (${placeholders})`, sessionIds);
64
+ }
65
+
66
+ // Generate secure session token and ID
67
+ const sessionId = crypto.randomUUID();
68
+ const sessionToken = crypto.randomBytes(32).toString('base64url');
69
+
70
+ // Create new session with secure token
71
+ await db_query(db,
72
+ "INSERT INTO usersession (ID, SessionToken, userid, ExpirationDate) VALUES (?, ?, ?, ?)",
73
+ [sessionId, sessionToken, user.userid, new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()]
74
+ );
75
+
76
+ // Set session cookie with secure token
77
+ const sessionCookie = await cookies();
78
+ sessionCookie.set({
79
+ name: 'session_id',
80
+ value: sessionToken,
81
+ httpOnly: true,
82
+ secure: process.env.NODE_ENV === 'production',
83
+ sameSite: process.env.NODE_ENV === 'production' ? 'strict' : 'lax',
84
+ maxAge: 30 * 24 * 60 * 60, // 30 days
85
+ path: '/',
86
+ });
87
+
88
+ // Redirect to home page or dashboard with success message
89
+ const siteDomain = process.env.SITE_DOMAIN || 'https://sitepaige.com';
90
+ const redirectUrl = new URL('/', siteDomain);
91
+ redirectUrl.searchParams.set('verified', 'true');
92
+
93
+ return NextResponse.redirect(redirectUrl);
94
+
95
+ } catch (error: any) {
96
+ console.error('Email verification error:', error);
97
+
98
+ // Redirect to home with error
99
+ const siteDomain = process.env.SITE_DOMAIN || 'https://sitepaige.com';
100
+ const redirectUrl = new URL('/', siteDomain);
101
+ redirectUrl.searchParams.set('error', 'verification_failed');
102
+
103
+ return NextResponse.redirect(redirectUrl);
104
+ }
105
+ }