sitepaige-mcp-server 0.7.14 → 1.0.2

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/README.md +94 -42
  2. package/components/form.tsx +133 -6
  3. package/components/login.tsx +173 -21
  4. package/components/menu.tsx +128 -3
  5. package/components/testimonial.tsx +1 -1
  6. package/defaultapp/api/Auth/route.ts +105 -3
  7. package/defaultapp/api/Auth/signup/route.ts +143 -0
  8. package/defaultapp/api/Auth/verify-email/route.ts +98 -0
  9. package/defaultapp/db-password-auth.ts +325 -0
  10. package/defaultapp/storage/email.ts +162 -0
  11. package/dist/blueprintWriter.js +15 -1
  12. package/dist/blueprintWriter.js.map +1 -1
  13. package/dist/components/form.tsx +133 -6
  14. package/dist/components/login.tsx +173 -21
  15. package/dist/components/menu.tsx +128 -3
  16. package/dist/components/testimonial.tsx +1 -1
  17. package/dist/defaultapp/api/Auth/route.ts +105 -3
  18. package/dist/defaultapp/api/Auth/signup/route.ts +143 -0
  19. package/dist/defaultapp/api/Auth/verify-email/route.ts +98 -0
  20. package/dist/defaultapp/db-password-auth.ts +325 -0
  21. package/dist/defaultapp/storage/email.ts +162 -0
  22. package/dist/generators/apis.js +1 -0
  23. package/dist/generators/apis.js.map +1 -1
  24. package/dist/generators/defaultapp.js +2 -2
  25. package/dist/generators/defaultapp.js.map +1 -1
  26. package/dist/generators/env-example-template.txt +27 -0
  27. package/dist/generators/images.js +38 -13
  28. package/dist/generators/images.js.map +1 -1
  29. package/dist/generators/pages.js +1 -1
  30. package/dist/generators/pages.js.map +1 -1
  31. package/dist/generators/sql.js +19 -0
  32. package/dist/generators/sql.js.map +1 -1
  33. package/dist/generators/views.js +17 -2
  34. package/dist/generators/views.js.map +1 -1
  35. package/dist/index.js +15 -116
  36. package/dist/index.js.map +1 -1
  37. package/dist/sitepaige.js +20 -127
  38. package/dist/sitepaige.js.map +1 -1
  39. package/manifest.json +5 -24
  40. package/package.json +3 -2
@@ -32,11 +32,113 @@ export async function POST(request: Request) {
32
32
  const db = await db_init();
33
33
 
34
34
  try {
35
- const { code, provider } = await request.json();
35
+ const { code, provider, email, password } = await request.json();
36
36
 
37
- if (!code || !provider) {
37
+ if (!provider) {
38
38
  return NextResponse.json(
39
- { error: 'No authorization code or provider specified' },
39
+ { error: 'No provider specified' },
40
+ { status: 400 }
41
+ );
42
+ }
43
+
44
+ // Handle username/password authentication
45
+ if (provider === 'username') {
46
+ if (!email || !password) {
47
+ return NextResponse.json(
48
+ { error: 'Email and password are required' },
49
+ { status: 400 }
50
+ );
51
+ }
52
+
53
+ const { authenticateUser, createPasswordAuthTable } = await import('../../db-password-auth');
54
+ const { upsertUser } = await import('../../db-users');
55
+
56
+ // Ensure password auth table exists
57
+ await createPasswordAuthTable();
58
+
59
+ try {
60
+ // Authenticate the user
61
+ const authRecord = await authenticateUser(email, password);
62
+
63
+ if (!authRecord) {
64
+ return NextResponse.json(
65
+ { error: 'Invalid email or password' },
66
+ { status: 401 }
67
+ );
68
+ }
69
+
70
+ // Create or update user in the main Users table
71
+ const user = await upsertUser(
72
+ `password_${authRecord.id}`, // Unique OAuth ID for password users
73
+ 'username' as any, // Source type
74
+ email.split('@')[0], // Username from email
75
+ email,
76
+ undefined // No avatar for password auth
77
+ );
78
+
79
+ // Delete existing sessions for this user
80
+ const existingSessions = await db_query(db,
81
+ "SELECT ID FROM usersession WHERE userid = ?",
82
+ [user.userid]
83
+ );
84
+
85
+ if (existingSessions && existingSessions.length > 0) {
86
+ const sessionIds = existingSessions.map(session => session.ID);
87
+ const placeholders = sessionIds.map(() => '?').join(',');
88
+ await db_query(db, `DELETE FROM usersession WHERE ID IN (${placeholders})`, sessionIds);
89
+ }
90
+
91
+ // Generate secure session token and ID
92
+ const sessionId = crypto.randomUUID();
93
+ const sessionToken = crypto.randomBytes(32).toString('base64url');
94
+
95
+ // Create new session with secure token
96
+ await db_query(db,
97
+ "INSERT INTO usersession (ID, SessionToken, userid, ExpirationDate) VALUES (?, ?, ?, ?)",
98
+ [sessionId, sessionToken, user.userid, new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()]
99
+ );
100
+
101
+ // Set session cookie with secure token
102
+ const sessionCookie = await cookies();
103
+ sessionCookie.set({
104
+ name: 'session_id',
105
+ value: sessionToken,
106
+ httpOnly: true,
107
+ secure: process.env.NODE_ENV === 'production',
108
+ sameSite: process.env.NODE_ENV === 'production' ? 'strict' : 'lax',
109
+ maxAge: 30 * 24 * 60 * 60, // 30 days
110
+ path: '/',
111
+ });
112
+
113
+ // Create a completely clean object to avoid any database result object issues
114
+ const cleanUserData = {
115
+ userid: String(user.userid),
116
+ userName: String(user.UserName),
117
+ avatarURL: String(user.AvatarURL || ''),
118
+ userLevel: Number(user.UserLevel),
119
+ isAdmin: Number(user.UserLevel) === 2
120
+ };
121
+
122
+ return NextResponse.json({
123
+ success: true,
124
+ user: cleanUserData
125
+ });
126
+
127
+ } catch (error: any) {
128
+ if (error.message === 'Email not verified') {
129
+ return NextResponse.json(
130
+ { error: 'Please verify your email before logging in' },
131
+ { status: 403 }
132
+ );
133
+ }
134
+ throw error;
135
+ }
136
+ }
137
+
138
+ // Handle OAuth authentication
139
+ if (!code) {
140
+ return NextResponse.json(
141
+ { error: 'No authorization code specified' },
40
142
  { status: 400 }
41
143
  );
42
144
  }
@@ -0,0 +1,143 @@
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 { validateCsrfToken } from '../../../csrf';
8
+ import { createPasswordAuth } from '../../../db-password-auth';
9
+ import { send_email } from '../../../storage/email';
10
+
11
+ // Email validation regex
12
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
13
+
14
+ // Password validation - at least 8 characters, one uppercase, one lowercase, one number
15
+ const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
16
+
17
+ export async function POST(request: Request) {
18
+ // Validate CSRF token
19
+ const isValidCsrf = await validateCsrfToken(request);
20
+ if (!isValidCsrf) {
21
+ return NextResponse.json(
22
+ { error: 'Invalid CSRF token' },
23
+ { status: 403 }
24
+ );
25
+ }
26
+
27
+ try {
28
+ const { email, password } = await request.json();
29
+
30
+ // Validate email format
31
+ if (!email || !emailRegex.test(email)) {
32
+ return NextResponse.json(
33
+ { error: 'Invalid email address' },
34
+ { status: 400 }
35
+ );
36
+ }
37
+
38
+ // Validate password strength
39
+ if (!password || !passwordRegex.test(password)) {
40
+ return NextResponse.json(
41
+ { error: 'Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, and one number' },
42
+ { status: 400 }
43
+ );
44
+ }
45
+
46
+ // Create password auth record
47
+ const { passwordAuth, verificationToken } = await createPasswordAuth(email, password);
48
+
49
+ // Get site domain from environment or default
50
+ const siteDomain = process.env.SITE_DOMAIN || 'https://sitepaige.com';
51
+ const verificationUrl = `${siteDomain}/api/Auth/verify-email?token=${verificationToken}`;
52
+
53
+ // Send verification email
54
+ try {
55
+ const emailHtml = `
56
+ <!DOCTYPE html>
57
+ <html>
58
+ <head>
59
+ <style>
60
+ body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
61
+ .container { max-width: 600px; margin: 0 auto; padding: 20px; }
62
+ .button {
63
+ display: inline-block;
64
+ padding: 12px 24px;
65
+ background-color: #4F46E5;
66
+ color: white;
67
+ text-decoration: none;
68
+ border-radius: 6px;
69
+ font-weight: bold;
70
+ }
71
+ .footer { margin-top: 30px; font-size: 12px; color: #666; }
72
+ </style>
73
+ </head>
74
+ <body>
75
+ <div class="container">
76
+ <h2>Welcome to ${siteDomain}!</h2>
77
+ <p>Thank you for signing up. Please verify your email address by clicking the button below:</p>
78
+ <p style="margin: 30px 0;">
79
+ <a href="${verificationUrl}" class="button">Verify Email Address</a>
80
+ </p>
81
+ <p>Or copy and paste this link into your browser:</p>
82
+ <p style="word-break: break-all; color: #4F46E5;">${verificationUrl}</p>
83
+ <p>This link will expire in 24 hours.</p>
84
+ <div class="footer">
85
+ <p>If you didn't create an account, you can safely ignore this email.</p>
86
+ </div>
87
+ </div>
88
+ </body>
89
+ </html>
90
+ `;
91
+
92
+ const emailText = `
93
+ Welcome to ${siteDomain}!
94
+
95
+ Thank you for signing up. Please verify your email address by clicking the link below:
96
+
97
+ ${verificationUrl}
98
+
99
+ This link will expire in 24 hours.
100
+
101
+ If you didn't create an account, you can safely ignore this email.
102
+ `;
103
+
104
+ await send_email({
105
+ to: email,
106
+ from: process.env.EMAIL_FROM || 'noreply@sitepaige.com',
107
+ subject: 'Verify your email address',
108
+ html: emailHtml,
109
+ text: emailText
110
+ });
111
+
112
+ return NextResponse.json({
113
+ success: true,
114
+ message: 'Registration successful! Please check your email to verify your account.'
115
+ });
116
+
117
+ } catch (emailError) {
118
+ console.error('Failed to send verification email:', emailError);
119
+ // Still return success but with a warning
120
+ return NextResponse.json({
121
+ success: true,
122
+ message: 'Registration successful! However, we could not send the verification email. Please contact support.',
123
+ warning: 'Email sending failed'
124
+ });
125
+ }
126
+
127
+ } catch (error: any) {
128
+ console.error('Signup error:', error);
129
+
130
+ // Handle specific errors
131
+ if (error.message === 'Email already registered') {
132
+ return NextResponse.json(
133
+ { error: 'This email is already registered. Please sign in instead.' },
134
+ { status: 409 }
135
+ );
136
+ }
137
+
138
+ return NextResponse.json(
139
+ { error: error.message || 'Signup failed' },
140
+ { status: 500 }
141
+ );
142
+ }
143
+ }
@@ -0,0 +1,98 @@
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
+ 'username' 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
+ // Auto-login the user after verification
45
+ const db = await db_init();
46
+
47
+ // Delete existing sessions for this user
48
+ const existingSessions = await db_query(db,
49
+ "SELECT ID FROM usersession WHERE userid = ?",
50
+ [user.userid]
51
+ );
52
+
53
+ if (existingSessions && existingSessions.length > 0) {
54
+ const sessionIds = existingSessions.map(session => session.ID);
55
+ const placeholders = sessionIds.map(() => '?').join(',');
56
+ await db_query(db, `DELETE FROM usersession WHERE ID IN (${placeholders})`, sessionIds);
57
+ }
58
+
59
+ // Generate secure session token and ID
60
+ const sessionId = crypto.randomUUID();
61
+ const sessionToken = crypto.randomBytes(32).toString('base64url');
62
+
63
+ // Create new session with secure token
64
+ await db_query(db,
65
+ "INSERT INTO usersession (ID, SessionToken, userid, ExpirationDate) VALUES (?, ?, ?, ?)",
66
+ [sessionId, sessionToken, user.userid, new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()]
67
+ );
68
+
69
+ // Set session cookie with secure token
70
+ const sessionCookie = await cookies();
71
+ sessionCookie.set({
72
+ name: 'session_id',
73
+ value: sessionToken,
74
+ httpOnly: true,
75
+ secure: process.env.NODE_ENV === 'production',
76
+ sameSite: process.env.NODE_ENV === 'production' ? 'strict' : 'lax',
77
+ maxAge: 30 * 24 * 60 * 60, // 30 days
78
+ path: '/',
79
+ });
80
+
81
+ // Redirect to home page or dashboard with success message
82
+ const siteDomain = process.env.SITE_DOMAIN || 'https://sitepaige.com';
83
+ const redirectUrl = new URL('/', siteDomain);
84
+ redirectUrl.searchParams.set('verified', 'true');
85
+
86
+ return NextResponse.redirect(redirectUrl);
87
+
88
+ } catch (error: any) {
89
+ console.error('Email verification error:', error);
90
+
91
+ // Redirect to home with error
92
+ const siteDomain = process.env.SITE_DOMAIN || 'https://sitepaige.com';
93
+ const redirectUrl = new URL('/', siteDomain);
94
+ redirectUrl.searchParams.set('error', 'verification_failed');
95
+
96
+ return NextResponse.redirect(redirectUrl);
97
+ }
98
+ }
@@ -0,0 +1,325 @@
1
+ /*
2
+ * Username/Password Authentication Database Utilities
3
+ * Handles all database operations related to username/password authentication
4
+ */
5
+
6
+ import { db_init, db_query } from './db';
7
+ import type { DatabaseClient } from './db';
8
+ import * as crypto from 'node:crypto';
9
+ import { scrypt, randomBytes } from 'node:crypto';
10
+ import { promisify } from 'node:util';
11
+
12
+ const scryptAsync = promisify(scrypt);
13
+
14
+ export interface PasswordAuth {
15
+ id: string;
16
+ email: string; // Email serves as username
17
+ passwordhash: string;
18
+ salt: string;
19
+ verificationtoken?: string;
20
+ verificationtokenexpires?: string;
21
+ emailverified: boolean;
22
+ resettoken?: string;
23
+ resettokenexpires?: string;
24
+ createdat: string;
25
+ updatedat: string;
26
+ }
27
+
28
+ /**
29
+ * Hash a password using scrypt
30
+ */
31
+ export async function hashPassword(password: string): Promise<{ hash: string; salt: string }> {
32
+ const salt = randomBytes(32).toString('base64');
33
+ const hash = (await scryptAsync(password, salt, 64)) as Buffer;
34
+ return {
35
+ hash: hash.toString('base64'),
36
+ salt
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Verify a password against its hash
42
+ */
43
+ export async function verifyPassword(password: string, hash: string, salt: string): Promise<boolean> {
44
+ const derivedKey = (await scryptAsync(password, salt, 64)) as Buffer;
45
+ return derivedKey.toString('base64') === hash;
46
+ }
47
+
48
+ /**
49
+ * Create the password authentication table
50
+ */
51
+ export async function createPasswordAuthTable(): Promise<void> {
52
+ const client = await db_init();
53
+
54
+ const createTableQuery = `
55
+ CREATE TABLE IF NOT EXISTS passwordauth (
56
+ id TEXT PRIMARY KEY,
57
+ email TEXT NOT NULL UNIQUE,
58
+ passwordhash TEXT NOT NULL,
59
+ salt TEXT NOT NULL,
60
+ verificationtoken TEXT,
61
+ verificationtokenexpires TIMESTAMP,
62
+ emailverified BOOLEAN DEFAULT FALSE,
63
+ resettoken TEXT,
64
+ resettokenexpires TIMESTAMP,
65
+ createdat TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
66
+ updatedat TIMESTAMP DEFAULT CURRENT_TIMESTAMP
67
+ )
68
+ `;
69
+
70
+ await db_query(client, createTableQuery);
71
+
72
+ // Create index on email for faster lookups
73
+ await db_query(client, 'CREATE INDEX IF NOT EXISTS idx_passwordauth_email ON passwordauth(email)');
74
+ await db_query(client, 'CREATE INDEX IF NOT EXISTS idx_passwordauth_verification_token ON passwordauth(verificationtoken)');
75
+ await db_query(client, 'CREATE INDEX IF NOT EXISTS idx_passwordauth_reset_token ON passwordauth(resettoken)');
76
+ }
77
+
78
+ /**
79
+ * Get password auth record by email
80
+ */
81
+ export async function getPasswordAuthByEmail(email: string): Promise<PasswordAuth | null> {
82
+ const client = await db_init();
83
+
84
+ const results = await db_query(client,
85
+ "SELECT * FROM passwordauth WHERE email = ?",
86
+ [email.toLowerCase()]
87
+ );
88
+
89
+ return results.length > 0 ? results[0] as PasswordAuth : null;
90
+ }
91
+
92
+ /**
93
+ * Create a new password auth record (for signup)
94
+ */
95
+ export async function createPasswordAuth(
96
+ email: string,
97
+ password: string
98
+ ): Promise<{ passwordAuth: PasswordAuth; verificationToken: string }> {
99
+ const client = await db_init();
100
+
101
+ // Check if email already exists
102
+ const existing = await getPasswordAuthByEmail(email);
103
+ if (existing) {
104
+ throw new Error('Email already registered');
105
+ }
106
+
107
+ // Hash the password
108
+ const { hash, salt } = await hashPassword(password);
109
+
110
+ // Generate verification token
111
+ const verificationToken = randomBytes(32).toString('base64url');
112
+ const verificationTokenExpires = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours
113
+
114
+ const id = crypto.randomUUID();
115
+ const now = new Date().toISOString();
116
+
117
+ await db_query(client,
118
+ `INSERT INTO passwordauth
119
+ (id, email, passwordhash, salt, verificationtoken, verificationtokenexpires, emailverified, createdat, updatedat)
120
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
121
+ [id, email.toLowerCase(), hash, salt, verificationToken, verificationTokenExpires.toISOString(), false, now, now]
122
+ );
123
+
124
+ const passwordAuth = await getPasswordAuthByEmail(email);
125
+ if (!passwordAuth) {
126
+ throw new Error('Failed to create password auth record');
127
+ }
128
+
129
+ return { passwordAuth, verificationToken };
130
+ }
131
+
132
+ /**
133
+ * Verify email with token
134
+ */
135
+ export async function verifyEmailWithToken(token: string): Promise<PasswordAuth | null> {
136
+ const client = await db_init();
137
+
138
+ // Find the auth record with this token
139
+ const results = await db_query(client,
140
+ `SELECT * FROM passwordauth
141
+ WHERE verificationtoken = ?
142
+ AND verificationtokenexpires > CURRENT_TIMESTAMP
143
+ AND emailverified = false`,
144
+ [token]
145
+ );
146
+
147
+ if (results.length === 0) {
148
+ return null;
149
+ }
150
+
151
+ const authRecord = results[0];
152
+
153
+ // Mark email as verified and clear token
154
+ await db_query(client,
155
+ `UPDATE passwordauth
156
+ SET emailverified = true,
157
+ verificationtoken = NULL,
158
+ verificationtokenexpires = NULL,
159
+ updatedat = CURRENT_TIMESTAMP
160
+ WHERE id = ?`,
161
+ [authRecord.id]
162
+ );
163
+
164
+ return await getPasswordAuthByEmail(authRecord.email);
165
+ }
166
+
167
+ /**
168
+ * Authenticate user with email and password
169
+ */
170
+ export async function authenticateUser(email: string, password: string): Promise<PasswordAuth | null> {
171
+ const authRecord = await getPasswordAuthByEmail(email);
172
+
173
+ if (!authRecord) {
174
+ return null;
175
+ }
176
+
177
+ // Check if email is verified
178
+ if (!authRecord.emailverified) {
179
+ throw new Error('Email not verified');
180
+ }
181
+
182
+ // Verify password
183
+ const isValid = await verifyPassword(password, authRecord.passwordhash, authRecord.salt);
184
+
185
+ if (!isValid) {
186
+ return null;
187
+ }
188
+
189
+ return authRecord;
190
+ }
191
+
192
+ /**
193
+ * Generate password reset token
194
+ */
195
+ export async function generatePasswordResetToken(email: string): Promise<string | null> {
196
+ const client = await db_init();
197
+
198
+ const authRecord = await getPasswordAuthByEmail(email);
199
+ if (!authRecord) {
200
+ return null;
201
+ }
202
+
203
+ const resetToken = randomBytes(32).toString('base64url');
204
+ const resetTokenExpires = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
205
+
206
+ await db_query(client,
207
+ `UPDATE passwordauth
208
+ SET resettoken = ?,
209
+ resettokenexpires = ?,
210
+ updatedat = CURRENT_TIMESTAMP
211
+ WHERE id = ?`,
212
+ [resetToken, resetTokenExpires.toISOString(), authRecord.id]
213
+ );
214
+
215
+ return resetToken;
216
+ }
217
+
218
+ /**
219
+ * Reset password with token
220
+ */
221
+ export async function resetPasswordWithToken(token: string, newPassword: string): Promise<boolean> {
222
+ const client = await db_init();
223
+
224
+ // Find the auth record with this token
225
+ const results = await db_query(client,
226
+ `SELECT * FROM passwordauth
227
+ WHERE resettoken = ?
228
+ AND resettokenexpires > CURRENT_TIMESTAMP`,
229
+ [token]
230
+ );
231
+
232
+ if (results.length === 0) {
233
+ return false;
234
+ }
235
+
236
+ const authRecord = results[0];
237
+
238
+ // Hash the new password
239
+ const { hash, salt } = await hashPassword(newPassword);
240
+
241
+ // Update password and clear reset token
242
+ await db_query(client,
243
+ `UPDATE passwordauth
244
+ SET passwordhash = ?,
245
+ salt = ?,
246
+ resettoken = NULL,
247
+ resettokenexpires = NULL,
248
+ updatedat = CURRENT_TIMESTAMP
249
+ WHERE id = ?`,
250
+ [hash, salt, authRecord.id]
251
+ );
252
+
253
+ return true;
254
+ }
255
+
256
+ /**
257
+ * Delete unverified accounts older than 7 days
258
+ */
259
+ export async function cleanupUnverifiedAccounts(): Promise<number> {
260
+ const client = await db_init();
261
+
262
+ const result = await db_query(client,
263
+ `DELETE FROM passwordauth
264
+ WHERE emailverified = false
265
+ AND createdat < datetime('now', '-7 days')`
266
+ );
267
+
268
+ return result[0]?.changes || 0;
269
+ }
270
+
271
+ /**
272
+ * Update password for authenticated user
273
+ */
274
+ export async function updatePassword(email: string, currentPassword: string, newPassword: string): Promise<boolean> {
275
+ // First verify the current password
276
+ const authRecord = await authenticateUser(email, currentPassword);
277
+ if (!authRecord) {
278
+ return false;
279
+ }
280
+
281
+ // Hash the new password
282
+ const { hash, salt } = await hashPassword(newPassword);
283
+ const client = await db_init();
284
+
285
+ // Update password
286
+ await db_query(client,
287
+ `UPDATE passwordauth
288
+ SET passwordhash = ?,
289
+ salt = ?,
290
+ updatedat = CURRENT_TIMESTAMP
291
+ WHERE id = ?`,
292
+ [hash, salt, authRecord.id]
293
+ );
294
+
295
+ return true;
296
+ }
297
+
298
+ /**
299
+ * Check if an email is already registered
300
+ */
301
+ export async function isEmailRegistered(email: string): Promise<boolean> {
302
+ const authRecord = await getPasswordAuthByEmail(email);
303
+ return authRecord !== null;
304
+ }
305
+
306
+ /**
307
+ * Get statistics about password auth users
308
+ */
309
+ export async function getPasswordAuthStats(): Promise<{
310
+ totalUsers: number;
311
+ verifiedUsers: number;
312
+ unverifiedUsers: number;
313
+ }> {
314
+ const client = await db_init();
315
+
316
+ const stats = await db_query(client, `
317
+ SELECT
318
+ COUNT(*) as totalUsers,
319
+ SUM(CASE WHEN emailverified = true THEN 1 ELSE 0 END) as verifiedUsers,
320
+ SUM(CASE WHEN emailverified = false THEN 1 ELSE 0 END) as unverifiedUsers
321
+ FROM passwordauth
322
+ `);
323
+
324
+ return stats[0];
325
+ }