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.
- package/README.md +94 -42
- package/components/form.tsx +133 -6
- package/components/login.tsx +173 -21
- package/components/menu.tsx +128 -3
- package/components/testimonial.tsx +1 -1
- package/defaultapp/api/Auth/route.ts +105 -3
- package/defaultapp/api/Auth/signup/route.ts +143 -0
- package/defaultapp/api/Auth/verify-email/route.ts +98 -0
- package/defaultapp/db-password-auth.ts +325 -0
- package/defaultapp/storage/email.ts +162 -0
- package/dist/blueprintWriter.js +15 -1
- package/dist/blueprintWriter.js.map +1 -1
- package/dist/components/form.tsx +133 -6
- package/dist/components/login.tsx +173 -21
- package/dist/components/menu.tsx +128 -3
- package/dist/components/testimonial.tsx +1 -1
- package/dist/defaultapp/api/Auth/route.ts +105 -3
- package/dist/defaultapp/api/Auth/signup/route.ts +143 -0
- package/dist/defaultapp/api/Auth/verify-email/route.ts +98 -0
- package/dist/defaultapp/db-password-auth.ts +325 -0
- package/dist/defaultapp/storage/email.ts +162 -0
- package/dist/generators/apis.js +1 -0
- package/dist/generators/apis.js.map +1 -1
- package/dist/generators/defaultapp.js +2 -2
- package/dist/generators/defaultapp.js.map +1 -1
- package/dist/generators/env-example-template.txt +27 -0
- package/dist/generators/images.js +38 -13
- package/dist/generators/images.js.map +1 -1
- package/dist/generators/pages.js +1 -1
- package/dist/generators/pages.js.map +1 -1
- package/dist/generators/sql.js +19 -0
- package/dist/generators/sql.js.map +1 -1
- package/dist/generators/views.js +17 -2
- package/dist/generators/views.js.map +1 -1
- package/dist/index.js +15 -116
- package/dist/index.js.map +1 -1
- package/dist/sitepaige.js +20 -127
- package/dist/sitepaige.js.map +1 -1
- package/manifest.json +5 -24
- 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 (!
|
|
37
|
+
if (!provider) {
|
|
38
38
|
return NextResponse.json(
|
|
39
|
-
{ error: 'No
|
|
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
|
+
}
|