sitepaige-mcp-server 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/headerlogin.tsx +10 -28
- package/components/profile.tsx +302 -46
- package/components/slideshow.tsx +1 -9
- package/defaultapp/api/Auth/resend-verification/route.ts +3 -3
- package/defaultapp/api/Auth/route.ts +2 -2
- package/defaultapp/api/Auth/signup/route.ts +3 -3
- package/defaultapp/api/Auth/verify-email/route.ts +5 -5
- package/defaultapp/api/admin/users/route.ts +3 -3
- package/defaultapp/{db-users.ts → api/db-users.ts} +1 -1
- package/defaultapp/api/profile/route.ts +154 -0
- package/defaultapp/profile/page.tsx +6 -0
- package/dist/blueprintWriter.js.map +1 -1
- package/dist/components/headerlogin.tsx +10 -28
- package/dist/components/profile.tsx +302 -46
- package/dist/components/slideshow.tsx +1 -9
- package/dist/defaultapp/api/Auth/resend-verification/route.ts +3 -3
- package/dist/defaultapp/api/Auth/route.ts +2 -2
- package/dist/defaultapp/api/Auth/signup/route.ts +3 -3
- package/dist/defaultapp/api/Auth/verify-email/route.ts +5 -5
- package/dist/defaultapp/api/admin/users/route.ts +3 -3
- package/dist/defaultapp/api/csrf.ts +111 -0
- package/dist/defaultapp/api/db-mysql.ts +183 -0
- package/dist/defaultapp/api/db-password-auth.ts +362 -0
- package/dist/defaultapp/api/db-postgres.ts +189 -0
- package/dist/defaultapp/api/db-sqlite.ts +335 -0
- package/dist/defaultapp/api/db-users.ts +520 -0
- package/dist/defaultapp/api/db.ts +149 -0
- package/dist/defaultapp/api/profile/route.ts +154 -0
- package/dist/defaultapp/api/storage/email.ts +162 -0
- package/dist/defaultapp/api/storage/files.ts +160 -0
- package/dist/defaultapp/db-users.ts +1 -1
- package/dist/defaultapp/profile/page.tsx +6 -0
- package/dist/generators/env-example-template.txt +4 -3
- package/dist/generators/skeleton.js +3 -5
- package/dist/generators/skeleton.js.map +1 -1
- package/dist/generators/sql.js +60 -0
- package/dist/generators/sql.js.map +1 -1
- package/dist/sitepaige.js +2 -1
- package/dist/sitepaige.js.map +1 -1
- package/package.json +1 -1
- package/defaultapp/admin/page.tsx +0 -6
- package/defaultapp/api/example-secure/route.ts +0 -100
- package/defaultapp/migrate.ts +0 -142
- /package/defaultapp/{csrf.ts → api/csrf.ts} +0 -0
- /package/defaultapp/{db-mysql.ts → api/db-mysql.ts} +0 -0
- /package/defaultapp/{db-password-auth.ts → api/db-password-auth.ts} +0 -0
- /package/defaultapp/{db-postgres.ts → api/db-postgres.ts} +0 -0
- /package/defaultapp/{db-sqlite.ts → api/db-sqlite.ts} +0 -0
- /package/defaultapp/{db.ts → api/db.ts} +0 -0
- /package/defaultapp/{storage → api/storage}/email.ts +0 -0
- /package/defaultapp/{storage → api/storage}/files.ts +0 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* User Management Database Utilities
|
|
3
|
+
* Handles all database operations related to user management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { db_init, db_query, db_migrate } from './db';
|
|
7
|
+
import type { DatabaseClient } from './db';
|
|
8
|
+
import * as crypto from 'node:crypto';
|
|
9
|
+
|
|
10
|
+
export interface User {
|
|
11
|
+
userid: string;
|
|
12
|
+
oauthid: string;
|
|
13
|
+
source: 'google' | 'facebook' | 'apple' | 'github' | 'userpass';
|
|
14
|
+
username: string;
|
|
15
|
+
email?: string;
|
|
16
|
+
avatarurl?: string;
|
|
17
|
+
userlevel: number; // 0: everyone, 1: registered user, 2: admin
|
|
18
|
+
lastlogindate: string;
|
|
19
|
+
createddate: string;
|
|
20
|
+
isactive: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UserSession {
|
|
24
|
+
id: string;
|
|
25
|
+
sessiontoken: string;
|
|
26
|
+
userid: string;
|
|
27
|
+
expirationdate: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface OAuthToken {
|
|
31
|
+
id: string;
|
|
32
|
+
userid: string;
|
|
33
|
+
provider: 'google' | 'facebook' | 'apple' | 'github';
|
|
34
|
+
accesstoken: string;
|
|
35
|
+
refreshtoken?: string;
|
|
36
|
+
expiresat?: string;
|
|
37
|
+
createdat: string;
|
|
38
|
+
updatedat: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get all users from the database
|
|
43
|
+
*/
|
|
44
|
+
export async function getAllUsers(): Promise<User[]> {
|
|
45
|
+
const client = await db_init();
|
|
46
|
+
|
|
47
|
+
const users = await db_query(client,
|
|
48
|
+
`SELECT * FROM users
|
|
49
|
+
WHERE isactive = ?
|
|
50
|
+
ORDER BY userlevel DESC, username ASC`,
|
|
51
|
+
[1] // Use 1 instead of true for PostgreSQL compatibility
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return users as User[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get a user by their OAuth ID
|
|
59
|
+
*/
|
|
60
|
+
export async function getUserByOAuthID(oauthId: string): Promise<User | null> {
|
|
61
|
+
const client = await db_init();
|
|
62
|
+
|
|
63
|
+
const users = await db_query(client,
|
|
64
|
+
"SELECT * FROM users WHERE oauthid = ? AND isactive = ?",
|
|
65
|
+
[oauthId, 1] // Use 1 instead of true for PostgreSQL compatibility
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return users.length > 0 ? users[0] as User : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get a user by their userid
|
|
73
|
+
*/
|
|
74
|
+
export async function getUserByID(userId: string): Promise<User | null> {
|
|
75
|
+
const client = await db_init();
|
|
76
|
+
|
|
77
|
+
const users = await db_query(client,
|
|
78
|
+
"SELECT * FROM users WHERE userid = ? AND isactive = ?",
|
|
79
|
+
[userId, 1] // Use 1 instead of true for PostgreSQL compatibility
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return users.length > 0 ? users[0] as User : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create or update a user
|
|
87
|
+
*/
|
|
88
|
+
export async function upsertUser(
|
|
89
|
+
oauthId: string,
|
|
90
|
+
source: 'google' | 'facebook' | 'apple' | 'github' | 'userpass',
|
|
91
|
+
userName: string,
|
|
92
|
+
email?: string,
|
|
93
|
+
avatarUrl?: string
|
|
94
|
+
): Promise<User> {
|
|
95
|
+
const client = await db_init();
|
|
96
|
+
|
|
97
|
+
// Check if user exists
|
|
98
|
+
const existingUser = await getUserByOAuthID(oauthId);
|
|
99
|
+
|
|
100
|
+
if (existingUser) {
|
|
101
|
+
// Update existing user
|
|
102
|
+
await db_query(client,
|
|
103
|
+
`UPDATE users
|
|
104
|
+
SET username = ?, email = COALESCE(?, email), avatarurl = ?,
|
|
105
|
+
lastlogindate = CURRENT_TIMESTAMP, source = ?
|
|
106
|
+
WHERE oauthid = ?`,
|
|
107
|
+
[userName, email, avatarUrl || '', source, oauthId]
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return (await getUserByOAuthID(oauthId))!;
|
|
111
|
+
} else {
|
|
112
|
+
// Check if this is the first user (should be admin)
|
|
113
|
+
const allUsers = await db_query(client, "SELECT COUNT(*) as count FROM users");
|
|
114
|
+
const isFirstUser = Number(allUsers[0].count) === 0;
|
|
115
|
+
|
|
116
|
+
// Create new user
|
|
117
|
+
const userId = crypto.randomUUID();
|
|
118
|
+
const permissionLevel = isFirstUser ? 2 : 1; // First user is admin
|
|
119
|
+
|
|
120
|
+
await db_query(client,
|
|
121
|
+
`INSERT INTO users
|
|
122
|
+
(userid, oauthid, source, username, email, avatarurl, userlevel, usertier,
|
|
123
|
+
lastlogindate, createddate, isactive)
|
|
124
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, ?)`,
|
|
125
|
+
[userId, oauthId, source, userName, email || null, avatarUrl || '', permissionLevel, 1] // Use 1 instead of true for PostgreSQL compatibility
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return (await getUserByOAuthID(oauthId))!;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Update a user's permission level
|
|
134
|
+
*/
|
|
135
|
+
export async function updateUserPermission(
|
|
136
|
+
userId: string,
|
|
137
|
+
permissionLevel: number
|
|
138
|
+
): Promise<boolean> {
|
|
139
|
+
const client = await db_init();
|
|
140
|
+
|
|
141
|
+
// Ensure there's always at least one admin
|
|
142
|
+
if (permissionLevel < 2) {
|
|
143
|
+
const admins = await db_query(client,
|
|
144
|
+
"SELECT COUNT(*) as count FROM users WHERE userlevel = ? AND userid != ? AND isactive = ?",
|
|
145
|
+
[2, userId, 1] // Use 1 instead of true for PostgreSQL compatibility
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
if (admins[0].count === 0) {
|
|
149
|
+
throw new Error('Cannot demote the last admin user');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const result = await db_query(client,
|
|
154
|
+
"UPDATE users SET userlevel = ? WHERE userid = ?",
|
|
155
|
+
[permissionLevel, userId]
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
return result[0].changes > 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Delete (soft delete) a user
|
|
163
|
+
*/
|
|
164
|
+
export async function deleteUser(userId: string): Promise<boolean> {
|
|
165
|
+
const client = await db_init();
|
|
166
|
+
|
|
167
|
+
// Ensure there's always at least one admin
|
|
168
|
+
const user = await getUserByID(userId);
|
|
169
|
+
if (user && user.userlevel === 2) {
|
|
170
|
+
const admins = await db_query(client,
|
|
171
|
+
"SELECT COUNT(*) as count FROM users WHERE userlevel = ? AND userid != ? AND isactive = ?",
|
|
172
|
+
[2, userId, 1] // Use 1 instead of true for PostgreSQL compatibility
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (admins[0].count === 0) {
|
|
176
|
+
throw new Error('Cannot delete the last admin user');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Soft delete the user
|
|
181
|
+
const result = await db_query(client,
|
|
182
|
+
"UPDATE users SET isactive = ? WHERE userid = ?",
|
|
183
|
+
[0, userId] // Use 0 instead of false for PostgreSQL compatibility
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
return result[0].changes > 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get user statistics
|
|
191
|
+
*/
|
|
192
|
+
export async function getUserStats(): Promise<{
|
|
193
|
+
totalUsers: number;
|
|
194
|
+
admins: number;
|
|
195
|
+
registeredUsers: number;
|
|
196
|
+
guestUsers: number;
|
|
197
|
+
}> {
|
|
198
|
+
const client = await db_init();
|
|
199
|
+
|
|
200
|
+
const stats = await db_query(client, `
|
|
201
|
+
SELECT
|
|
202
|
+
COUNT(*) as totalUsers,
|
|
203
|
+
SUM(CASE WHEN userlevel = 2 THEN 1 ELSE 0 END) as admins,
|
|
204
|
+
SUM(CASE WHEN userlevel = 1 THEN 1 ELSE 0 END) as registeredUsers,
|
|
205
|
+
SUM(CASE WHEN userlevel = 0 THEN 1 ELSE 0 END) as guestUsers
|
|
206
|
+
FROM users
|
|
207
|
+
WHERE isactive = ?
|
|
208
|
+
`, [1]); // Use 1 instead of true for PostgreSQL compatibility
|
|
209
|
+
|
|
210
|
+
return stats[0];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Clean up expired sessions
|
|
215
|
+
*/
|
|
216
|
+
export async function cleanupExpiredSessions(): Promise<number> {
|
|
217
|
+
const client = await db_init();
|
|
218
|
+
|
|
219
|
+
const result = await db_query(client,
|
|
220
|
+
"DELETE FROM usersession WHERE expirationdate::TIMESTAMP < CURRENT_TIMESTAMP"
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
return result[0].changes;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Store OAuth tokens securely
|
|
228
|
+
*/
|
|
229
|
+
export async function storeOAuthToken(
|
|
230
|
+
userId: string,
|
|
231
|
+
provider: 'google' | 'facebook' | 'apple' | 'github',
|
|
232
|
+
accessToken: string,
|
|
233
|
+
refreshToken?: string,
|
|
234
|
+
expiresIn?: number
|
|
235
|
+
): Promise<void> {
|
|
236
|
+
const client = await db_init();
|
|
237
|
+
|
|
238
|
+
const tokenId = crypto.randomUUID();
|
|
239
|
+
const now = new Date().toISOString();
|
|
240
|
+
const expiresAt = expiresIn ? new Date(Date.now() + expiresIn * 1000).toISOString() : null;
|
|
241
|
+
|
|
242
|
+
// Delete existing tokens for this user/provider combo
|
|
243
|
+
await db_query(client,
|
|
244
|
+
"DELETE FROM oauthtokens WHERE userid = ? AND provider = ?",
|
|
245
|
+
[userId, provider]
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
// Insert new token
|
|
249
|
+
await db_query(client,
|
|
250
|
+
`INSERT INTO oauthtokens
|
|
251
|
+
(id, userid, provider, accesstoken, refreshtoken, expiresat, createdat, updatedat)
|
|
252
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
253
|
+
[tokenId, userId, provider, accessToken, refreshToken || null, expiresAt, now, now]
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get OAuth token for a user
|
|
259
|
+
*/
|
|
260
|
+
export async function getOAuthToken(
|
|
261
|
+
userId: string,
|
|
262
|
+
provider: 'google' | 'facebook' | 'apple' | 'github'
|
|
263
|
+
): Promise<OAuthToken | null> {
|
|
264
|
+
const client = await db_init();
|
|
265
|
+
|
|
266
|
+
const tokens = await db_query(client,
|
|
267
|
+
"SELECT * FROM oauthtokens WHERE userid = ? AND provider = ?",
|
|
268
|
+
[userId, provider]
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
return tokens.length > 0 ? tokens[0] as OAuthToken : null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Validate session and check for suspicious activity
|
|
276
|
+
*/
|
|
277
|
+
export async function validateSession(sessionToken: string): Promise<{
|
|
278
|
+
valid: boolean;
|
|
279
|
+
user?: User;
|
|
280
|
+
needsRotation?: boolean;
|
|
281
|
+
}> {
|
|
282
|
+
const client = await db_init();
|
|
283
|
+
|
|
284
|
+
// Get session details
|
|
285
|
+
const sessions = await db_query(client,
|
|
286
|
+
`SELECT s.*, u.* FROM usersession s
|
|
287
|
+
JOIN users u ON s.userid = u.userid
|
|
288
|
+
WHERE s.sessiontoken = ? AND s.expirationdate::TIMESTAMP > CURRENT_TIMESTAMP AND u.isactive = ?`,
|
|
289
|
+
[sessionToken, 1] // Use 1 instead of true for PostgreSQL compatibility
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
if (!sessions || sessions.length === 0) {
|
|
293
|
+
return { valid: false };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const session = sessions[0];
|
|
297
|
+
|
|
298
|
+
// Check if session needs rotation (older than 24 hours)
|
|
299
|
+
const sessionAge = Date.now() - new Date(session.id).getTime();
|
|
300
|
+
const needsRotation = sessionAge > 24 * 60 * 60 * 1000; // 24 hours
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
valid: true,
|
|
304
|
+
user: {
|
|
305
|
+
userid: session.userid,
|
|
306
|
+
oauthid: session.oauthid,
|
|
307
|
+
source: session.source,
|
|
308
|
+
username: session.username,
|
|
309
|
+
email: session.email,
|
|
310
|
+
avatarurl: session.avatarurl,
|
|
311
|
+
userlevel: session.userlevel,
|
|
312
|
+
lastlogindate: session.lastlogindate,
|
|
313
|
+
createddate: session.createddate,
|
|
314
|
+
isactive: session.isactive
|
|
315
|
+
} as User,
|
|
316
|
+
needsRotation
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Rotate session token for security
|
|
322
|
+
*/
|
|
323
|
+
export async function rotateSession(oldSessionToken: string): Promise<string | null> {
|
|
324
|
+
const client = await db_init();
|
|
325
|
+
|
|
326
|
+
// Get existing session
|
|
327
|
+
const sessions = await db_query(client,
|
|
328
|
+
"SELECT * FROM usersession WHERE sessiontoken = ? AND expirationdate::TIMESTAMP > CURRENT_TIMESTAMP",
|
|
329
|
+
[oldSessionToken]
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
if (!sessions || sessions.length === 0) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const session = sessions[0];
|
|
337
|
+
const newSessionToken = crypto.randomBytes(32).toString('base64url');
|
|
338
|
+
|
|
339
|
+
// Update session with new token
|
|
340
|
+
await db_query(client,
|
|
341
|
+
"UPDATE usersession SET sessiontoken = ?, expirationdate = ? WHERE id = ?",
|
|
342
|
+
[newSessionToken, new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), session.id]
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
return newSessionToken;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* OAuth token refresh endpoints
|
|
350
|
+
*/
|
|
351
|
+
const OAUTH_REFRESH_ENDPOINTS = {
|
|
352
|
+
google: 'https://oauth2.googleapis.com/token',
|
|
353
|
+
facebook: 'https://graph.facebook.com/v12.0/oauth/access_token',
|
|
354
|
+
apple: 'https://appleid.apple.com/auth/token',
|
|
355
|
+
github: 'https://github.com/login/oauth/access_token'
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Validate OAuth token with provider
|
|
360
|
+
*/
|
|
361
|
+
export async function validateOAuthToken(
|
|
362
|
+
userId: string,
|
|
363
|
+
provider: 'google' | 'facebook' | 'apple' | 'github'
|
|
364
|
+
): Promise<boolean> {
|
|
365
|
+
const client = await db_init();
|
|
366
|
+
|
|
367
|
+
// Get stored OAuth token
|
|
368
|
+
const token = await getOAuthToken(userId, provider);
|
|
369
|
+
if (!token) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Check if token is expired based on stored expiry
|
|
374
|
+
if (token.expiresat && new Date(token.expiresat) < new Date()) {
|
|
375
|
+
// Try to refresh the token
|
|
376
|
+
if (token.refreshtoken) {
|
|
377
|
+
return await refreshOAuthToken(userId, provider);
|
|
378
|
+
}
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// For providers that don't provide expiry, validate with a test request
|
|
383
|
+
try {
|
|
384
|
+
let validationUrl: string;
|
|
385
|
+
switch (provider) {
|
|
386
|
+
case 'google':
|
|
387
|
+
validationUrl = `https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=${token.accesstoken}`;
|
|
388
|
+
break;
|
|
389
|
+
case 'facebook':
|
|
390
|
+
validationUrl = `https://graph.facebook.com/me?access_token=${token.accesstoken}`;
|
|
391
|
+
break;
|
|
392
|
+
case 'github':
|
|
393
|
+
validationUrl = 'https://api.github.com/user';
|
|
394
|
+
break;
|
|
395
|
+
default:
|
|
396
|
+
return true; // Skip validation for providers without easy validation endpoints
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const response = await fetch(validationUrl, {
|
|
400
|
+
headers: provider === 'github' ? {
|
|
401
|
+
Authorization: `Bearer ${token.accesstoken}`
|
|
402
|
+
} : {}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
return response.ok;
|
|
406
|
+
} catch (error) {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Refresh OAuth token
|
|
413
|
+
*/
|
|
414
|
+
export async function refreshOAuthToken(
|
|
415
|
+
userId: string,
|
|
416
|
+
provider: 'google' | 'facebook' | 'apple' | 'github'
|
|
417
|
+
): Promise<boolean> {
|
|
418
|
+
const client = await db_init();
|
|
419
|
+
|
|
420
|
+
// Get stored OAuth token with refresh token
|
|
421
|
+
const token = await getOAuthToken(userId, provider);
|
|
422
|
+
if (!token || !token.refreshtoken) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const refreshEndpoint = OAUTH_REFRESH_ENDPOINTS[provider];
|
|
428
|
+
|
|
429
|
+
const params: Record<string, string> = {
|
|
430
|
+
grant_type: 'refresh_token',
|
|
431
|
+
refresh_token: token.refreshtoken,
|
|
432
|
+
client_id: process.env[`NEXT_PUBLIC_${provider.toUpperCase()}_CLIENT_ID`]!,
|
|
433
|
+
client_secret: process.env[`${provider.toUpperCase()}_CLIENT_SECRET`]!
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
const response = await fetch(refreshEndpoint, {
|
|
437
|
+
method: 'POST',
|
|
438
|
+
headers: {
|
|
439
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
440
|
+
Accept: 'application/json'
|
|
441
|
+
},
|
|
442
|
+
body: new URLSearchParams(params).toString()
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
if (!response.ok) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const data = await response.json();
|
|
450
|
+
|
|
451
|
+
// Update stored token
|
|
452
|
+
await storeOAuthToken(
|
|
453
|
+
userId,
|
|
454
|
+
provider,
|
|
455
|
+
data.access_token,
|
|
456
|
+
data.refresh_token || token.refreshtoken, // Some providers don't return new refresh token
|
|
457
|
+
data.expires_in
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
return true;
|
|
461
|
+
} catch (error) {
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Create a secure authentication middleware function
|
|
468
|
+
*/
|
|
469
|
+
export async function createAuthMiddleware(): Promise<{
|
|
470
|
+
validateRequest: (sessionToken: string, userAgent?: string, ipAddress?: string) => Promise<{
|
|
471
|
+
valid: boolean;
|
|
472
|
+
user?: User;
|
|
473
|
+
newSessionToken?: string;
|
|
474
|
+
}>;
|
|
475
|
+
}> {
|
|
476
|
+
// Track suspicious activity
|
|
477
|
+
const suspiciousActivity = new Map<string, number>();
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
validateRequest: async (sessionToken: string, userAgent?: string, ipAddress?: string) => {
|
|
481
|
+
// Check for suspicious activity (too many requests from same IP)
|
|
482
|
+
if (ipAddress) {
|
|
483
|
+
const requestCount = suspiciousActivity.get(ipAddress) || 0;
|
|
484
|
+
if (requestCount > 100) { // 100 requests per minute threshold
|
|
485
|
+
return { valid: false };
|
|
486
|
+
}
|
|
487
|
+
suspiciousActivity.set(ipAddress, requestCount + 1);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Validate session
|
|
491
|
+
const sessionData = await validateSession(sessionToken);
|
|
492
|
+
|
|
493
|
+
if (!sessionData.valid || !sessionData.user) {
|
|
494
|
+
return { valid: false };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Return validated session with potential rotation
|
|
498
|
+
if (sessionData.needsRotation) {
|
|
499
|
+
const newToken = await rotateSession(sessionToken);
|
|
500
|
+
return {
|
|
501
|
+
valid: true,
|
|
502
|
+
user: sessionData.user,
|
|
503
|
+
newSessionToken: newToken || undefined
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
valid: true,
|
|
509
|
+
user: sessionData.user
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Clear suspicious activity counter every minute
|
|
516
|
+
if (typeof setInterval !== 'undefined') {
|
|
517
|
+
setInterval(() => {
|
|
518
|
+
const suspiciousActivity = new Map<string, number>();
|
|
519
|
+
}, 60 * 1000);
|
|
520
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Sitepaige v1.0.0
|
|
3
|
+
Database abstraction layer for multiple database support
|
|
4
|
+
WARNING: This file is automatically generated and should not be modified.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
|
|
10
|
+
interface ModelField {
|
|
11
|
+
name: string;
|
|
12
|
+
datatype: string;
|
|
13
|
+
datatypesize: string | null;
|
|
14
|
+
required: string;
|
|
15
|
+
key: string | null;
|
|
16
|
+
default?: any;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Model {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
fields: ModelField[];
|
|
23
|
+
data_is_user_specific?: string; // Added for user-specific data
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Database client type that's compatible across all database types
|
|
27
|
+
export type DatabaseClient = any;
|
|
28
|
+
|
|
29
|
+
// Database type enum
|
|
30
|
+
export type DatabaseType = 'sqlite' | 'postgres' | 'mysql';
|
|
31
|
+
|
|
32
|
+
// Database configuration
|
|
33
|
+
export interface DatabaseConfig {
|
|
34
|
+
type: DatabaseType;
|
|
35
|
+
connectionString?: string; // For postgres/mysql
|
|
36
|
+
host?: string;
|
|
37
|
+
port?: number;
|
|
38
|
+
user?: string;
|
|
39
|
+
password?: string;
|
|
40
|
+
database?: string;
|
|
41
|
+
sqliteDir?: string; // For sqlite
|
|
42
|
+
efsMountPath?: string; // For production sqlite
|
|
43
|
+
ssl?: {
|
|
44
|
+
rejectUnauthorized?: boolean;
|
|
45
|
+
require?: boolean;
|
|
46
|
+
ca?: string;
|
|
47
|
+
cert?: string;
|
|
48
|
+
key?: string;
|
|
49
|
+
}; // SSL configuration for postgres/mysql
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Get database configuration from environment
|
|
53
|
+
export function getDatabaseConfig(): DatabaseConfig {
|
|
54
|
+
const dbType = (process.env.DATABASE_TYPE || 'postgres').toLowerCase() as DatabaseType;
|
|
55
|
+
|
|
56
|
+
switch (dbType) {
|
|
57
|
+
case 'postgres':
|
|
58
|
+
return {
|
|
59
|
+
type: 'postgres',
|
|
60
|
+
connectionString: process.env.DATABASE_URL || process.env.POSTGRES_URL,
|
|
61
|
+
host: process.env.DB_HOST || process.env.POSTGRES_HOST || 'localhost',
|
|
62
|
+
port: parseInt(process.env.DB_PORT || process.env.POSTGRES_PORT || '5432'),
|
|
63
|
+
user: process.env.DB_USER || process.env.POSTGRES_USER || 'postgres',
|
|
64
|
+
password: process.env.DB_PASSWORD || process.env.POSTGRES_PASSWORD,
|
|
65
|
+
database: process.env.DB_NAME || process.env.POSTGRES_DB || 'app',
|
|
66
|
+
ssl: {
|
|
67
|
+
rejectUnauthorized: true,
|
|
68
|
+
require: true // Always require SSL for PostgreSQL
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
case 'mysql':
|
|
73
|
+
return {
|
|
74
|
+
type: 'mysql',
|
|
75
|
+
connectionString: process.env.DATABASE_URL || process.env.MYSQL_URL,
|
|
76
|
+
host: process.env.DB_HOST || process.env.MYSQL_HOST || 'localhost',
|
|
77
|
+
port: parseInt(process.env.DB_PORT || process.env.MYSQL_PORT || '3306'),
|
|
78
|
+
user: process.env.DB_USER || process.env.MYSQL_USER || 'root',
|
|
79
|
+
password: process.env.DB_PASSWORD || process.env.MYSQL_PASSWORD,
|
|
80
|
+
database: process.env.DB_NAME || process.env.MYSQL_DATABASE || 'app'
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
default: // sqlite
|
|
84
|
+
return {
|
|
85
|
+
type: 'sqlite',
|
|
86
|
+
connectionString: process.env.DATABASE_URL, // Add support for DATABASE_URL
|
|
87
|
+
sqliteDir: process.env.SQLITE_DIR || '.',
|
|
88
|
+
efsMountPath: process.env.EFS_MOUNT_PATH
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Dynamic imports based on database type
|
|
94
|
+
let dbImplementation: any;
|
|
95
|
+
|
|
96
|
+
async function loadDatabaseImplementation(dbType: DatabaseType) {
|
|
97
|
+
switch (dbType) {
|
|
98
|
+
case 'postgres':
|
|
99
|
+
dbImplementation = await import('./db-postgres');
|
|
100
|
+
break;
|
|
101
|
+
case 'mysql':
|
|
102
|
+
dbImplementation = await import('./db-mysql');
|
|
103
|
+
break;
|
|
104
|
+
default: // sqlite
|
|
105
|
+
dbImplementation = await import('./db-sqlite');
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Initialize database connection
|
|
112
|
+
* @returns Database client
|
|
113
|
+
*/
|
|
114
|
+
export async function db_init(): Promise<DatabaseClient> {
|
|
115
|
+
const dbType = (process.env.DATABASE_TYPE || process.env.DB_TYPE || 'postgres').toLowerCase() as DatabaseType;
|
|
116
|
+
|
|
117
|
+
if (!dbImplementation) {
|
|
118
|
+
await loadDatabaseImplementation(dbType);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return dbImplementation.db_init();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Execute a SQL query using the provided database client
|
|
126
|
+
* @param client Database client from db_init()
|
|
127
|
+
* @param query SQL query string
|
|
128
|
+
* @param params Optional array of parameters for the query
|
|
129
|
+
* @returns Array of selected rows or execution results
|
|
130
|
+
*/
|
|
131
|
+
export async function db_query(
|
|
132
|
+
client: DatabaseClient,
|
|
133
|
+
query: string,
|
|
134
|
+
params?: any[]
|
|
135
|
+
): Promise<any[]> {
|
|
136
|
+
return dbImplementation.db_query(client, query, params);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Generates a CREATE TABLE SQL string for the specified table and fields
|
|
141
|
+
* @param model The model definition
|
|
142
|
+
* @returns SQL string for creating the table
|
|
143
|
+
*/
|
|
144
|
+
export function db_migrate(model: Model): string {
|
|
145
|
+
const dbType = (process.env.DATABASE_TYPE || process.env.DB_TYPE || 'postgres').toLowerCase() as DatabaseType;
|
|
146
|
+
return dbImplementation.db_migrate(model, dbType);
|
|
147
|
+
}
|
|
148
|
+
// Export types
|
|
149
|
+
export type { Model, ModelField };
|