archicore 0.1.9 → 0.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/dist/cli/commands/interactive.js +145 -61
- package/dist/github/github-service.d.ts +5 -1
- package/dist/github/github-service.js +21 -3
- package/dist/semantic-memory/embedding-service.d.ts +8 -1
- package/dist/semantic-memory/embedding-service.js +141 -47
- package/dist/server/index.js +66 -1
- package/dist/server/routes/admin.js +149 -1
- package/dist/server/routes/auth.js +46 -0
- package/dist/server/routes/github.js +17 -4
- package/dist/server/services/audit-service.d.ts +88 -0
- package/dist/server/services/audit-service.js +380 -0
- package/dist/server/services/auth-service.d.ts +11 -5
- package/dist/server/services/auth-service.js +299 -52
- package/dist/server/services/cache.d.ts +77 -0
- package/dist/server/services/cache.js +245 -0
- package/dist/server/services/database.d.ts +43 -0
- package/dist/server/services/database.js +221 -0
- package/package.json +17 -2
|
@@ -1,40 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Authentication Service for ArchiCore
|
|
3
|
+
*
|
|
4
|
+
* Supports PostgreSQL (primary) with JSON file fallback
|
|
3
5
|
*/
|
|
4
6
|
import { randomUUID } from 'crypto';
|
|
5
7
|
import { createHash } from 'crypto';
|
|
6
8
|
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
7
9
|
import { join } from 'path';
|
|
8
10
|
import { TIER_LIMITS } from '../../types/user.js';
|
|
11
|
+
import { db } from './database.js';
|
|
12
|
+
import { Logger } from '../../utils/logger.js';
|
|
9
13
|
const DATA_DIR = '.archicore';
|
|
10
14
|
const USERS_FILE = 'users.json';
|
|
11
15
|
const SESSIONS_FILE = 'sessions.json';
|
|
12
16
|
export class AuthService {
|
|
13
17
|
static instance = null;
|
|
14
18
|
dataDir;
|
|
19
|
+
// JSON fallback storage
|
|
15
20
|
users = [];
|
|
16
21
|
sessions = [];
|
|
17
|
-
|
|
22
|
+
jsonInitialized = false;
|
|
18
23
|
constructor(dataDir = DATA_DIR) {
|
|
19
24
|
this.dataDir = dataDir;
|
|
20
25
|
}
|
|
21
|
-
/**
|
|
22
|
-
* Get singleton instance of AuthService
|
|
23
|
-
*/
|
|
24
26
|
static getInstance() {
|
|
25
27
|
if (!AuthService.instance) {
|
|
26
28
|
AuthService.instance = new AuthService();
|
|
27
29
|
}
|
|
28
30
|
return AuthService.instance;
|
|
29
31
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
useDatabase() {
|
|
33
|
+
return db.isAvailable();
|
|
34
|
+
}
|
|
35
|
+
// ========== JSON FALLBACK METHODS ==========
|
|
36
|
+
async ensureJsonInitialized() {
|
|
37
|
+
if (this.jsonInitialized)
|
|
32
38
|
return;
|
|
33
39
|
try {
|
|
34
40
|
await mkdir(this.dataDir, { recursive: true });
|
|
35
41
|
}
|
|
36
42
|
catch { }
|
|
37
|
-
// Load users
|
|
38
43
|
try {
|
|
39
44
|
const usersPath = join(this.dataDir, USERS_FILE);
|
|
40
45
|
const data = await readFile(usersPath, 'utf-8');
|
|
@@ -43,10 +48,8 @@ export class AuthService {
|
|
|
43
48
|
}
|
|
44
49
|
catch {
|
|
45
50
|
this.users = [];
|
|
46
|
-
// Create default admin user
|
|
47
51
|
await this.createDefaultAdmin();
|
|
48
52
|
}
|
|
49
|
-
// Load sessions
|
|
50
53
|
try {
|
|
51
54
|
const sessionsPath = join(this.dataDir, SESSIONS_FILE);
|
|
52
55
|
const data = await readFile(sessionsPath, 'utf-8');
|
|
@@ -56,7 +59,7 @@ export class AuthService {
|
|
|
56
59
|
catch {
|
|
57
60
|
this.sessions = [];
|
|
58
61
|
}
|
|
59
|
-
this.
|
|
62
|
+
this.jsonInitialized = true;
|
|
60
63
|
}
|
|
61
64
|
async createDefaultAdmin() {
|
|
62
65
|
const admin = {
|
|
@@ -81,6 +84,7 @@ export class AuthService {
|
|
|
81
84
|
const sessionsPath = join(this.dataDir, SESSIONS_FILE);
|
|
82
85
|
await writeFile(sessionsPath, JSON.stringify({ sessions: this.sessions }, null, 2));
|
|
83
86
|
}
|
|
87
|
+
// ========== HELPER METHODS ==========
|
|
84
88
|
hashPassword(password) {
|
|
85
89
|
return createHash('sha256').update(password + 'archicore-salt-2024').digest('hex');
|
|
86
90
|
}
|
|
@@ -99,13 +103,72 @@ export class AuthService {
|
|
|
99
103
|
const { passwordHash, ...sanitized } = user;
|
|
100
104
|
return sanitized;
|
|
101
105
|
}
|
|
106
|
+
rowToUser(row) {
|
|
107
|
+
return {
|
|
108
|
+
id: row.id,
|
|
109
|
+
email: row.email,
|
|
110
|
+
username: row.username,
|
|
111
|
+
passwordHash: row.password_hash || undefined,
|
|
112
|
+
avatar: row.avatar || undefined,
|
|
113
|
+
tier: row.tier,
|
|
114
|
+
provider: row.provider,
|
|
115
|
+
providerId: row.provider_id || undefined,
|
|
116
|
+
createdAt: row.created_at,
|
|
117
|
+
lastLoginAt: row.last_login_at,
|
|
118
|
+
usage: {
|
|
119
|
+
requestsToday: row.requests_today,
|
|
120
|
+
fullAnalysisToday: row.full_analysis_today,
|
|
121
|
+
projectsToday: row.projects_today,
|
|
122
|
+
lastResetDate: row.usage_reset_date
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// ========== DATABASE METHODS ==========
|
|
127
|
+
async dbCreateDefaultAdmin() {
|
|
128
|
+
const result = await db.query('SELECT id FROM users WHERE tier = $1 LIMIT 1', ['admin']);
|
|
129
|
+
if (result.rows.length > 0)
|
|
130
|
+
return;
|
|
131
|
+
const adminId = 'admin-' + randomUUID();
|
|
132
|
+
await db.query(`INSERT INTO users (id, email, username, password_hash, tier, provider)
|
|
133
|
+
VALUES ($1, $2, $3, $4, $5, $6)`, [adminId, 'admin@archicore.io', 'Administrator', this.hashPassword('admin123'), 'admin', 'email']);
|
|
134
|
+
Logger.info('Default admin user created');
|
|
135
|
+
}
|
|
136
|
+
// ========== PUBLIC API ==========
|
|
102
137
|
async register(email, username, password) {
|
|
103
|
-
|
|
104
|
-
|
|
138
|
+
if (this.useDatabase()) {
|
|
139
|
+
try {
|
|
140
|
+
// Check email exists
|
|
141
|
+
const emailCheck = await db.query('SELECT id FROM users WHERE LOWER(email) = LOWER($1)', [email]);
|
|
142
|
+
if (emailCheck.rows.length > 0) {
|
|
143
|
+
return { success: false, error: 'Email already registered' };
|
|
144
|
+
}
|
|
145
|
+
// Check username exists
|
|
146
|
+
const usernameCheck = await db.query('SELECT id FROM users WHERE LOWER(username) = LOWER($1)', [username]);
|
|
147
|
+
if (usernameCheck.rows.length > 0) {
|
|
148
|
+
return { success: false, error: 'Username already taken' };
|
|
149
|
+
}
|
|
150
|
+
const userId = 'user-' + randomUUID();
|
|
151
|
+
const token = this.generateToken();
|
|
152
|
+
await db.transaction(async (client) => {
|
|
153
|
+
await client.query(`INSERT INTO users (id, email, username, password_hash, tier, provider)
|
|
154
|
+
VALUES ($1, $2, $3, $4, $5, $6)`, [userId, email.toLowerCase(), username, this.hashPassword(password), 'free', 'email']);
|
|
155
|
+
await client.query(`INSERT INTO sessions (token, user_id, expires_at)
|
|
156
|
+
VALUES ($1, $2, $3)`, [token, userId, new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)]);
|
|
157
|
+
});
|
|
158
|
+
const userResult = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
|
|
159
|
+
const user = this.rowToUser(userResult.rows[0]);
|
|
160
|
+
return { success: true, token, user: this.sanitizeUser(user) };
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
Logger.error('Register error:', error);
|
|
164
|
+
return { success: false, error: 'Registration failed' };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// JSON fallback
|
|
168
|
+
await this.ensureJsonInitialized();
|
|
105
169
|
if (this.users.find(u => u.email.toLowerCase() === email.toLowerCase())) {
|
|
106
170
|
return { success: false, error: 'Email already registered' };
|
|
107
171
|
}
|
|
108
|
-
// Check if username already exists
|
|
109
172
|
if (this.users.find(u => u.username.toLowerCase() === username.toLowerCase())) {
|
|
110
173
|
return { success: false, error: 'Username already taken' };
|
|
111
174
|
}
|
|
@@ -122,23 +185,40 @@ export class AuthService {
|
|
|
122
185
|
};
|
|
123
186
|
this.users.push(user);
|
|
124
187
|
await this.saveUsers();
|
|
125
|
-
// Create session
|
|
126
188
|
const token = this.generateToken();
|
|
127
189
|
const session = {
|
|
128
190
|
token,
|
|
129
191
|
userId: user.id,
|
|
130
|
-
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
|
|
192
|
+
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
|
|
131
193
|
};
|
|
132
194
|
this.sessions.push(session);
|
|
133
195
|
await this.saveSessions();
|
|
134
|
-
return {
|
|
135
|
-
success: true,
|
|
136
|
-
token,
|
|
137
|
-
user: this.sanitizeUser(user)
|
|
138
|
-
};
|
|
196
|
+
return { success: true, token, user: this.sanitizeUser(user) };
|
|
139
197
|
}
|
|
140
198
|
async login(email, password) {
|
|
141
|
-
|
|
199
|
+
if (this.useDatabase()) {
|
|
200
|
+
try {
|
|
201
|
+
const result = await db.query('SELECT * FROM users WHERE LOWER(email) = LOWER($1)', [email]);
|
|
202
|
+
if (result.rows.length === 0) {
|
|
203
|
+
return { success: false, error: 'Invalid email or password' };
|
|
204
|
+
}
|
|
205
|
+
const row = result.rows[0];
|
|
206
|
+
if (row.password_hash !== this.hashPassword(password)) {
|
|
207
|
+
return { success: false, error: 'Invalid email or password' };
|
|
208
|
+
}
|
|
209
|
+
const token = this.generateToken();
|
|
210
|
+
await db.query('UPDATE users SET last_login_at = NOW() WHERE id = $1', [row.id]);
|
|
211
|
+
await db.query('INSERT INTO sessions (token, user_id, expires_at) VALUES ($1, $2, $3)', [token, row.id, new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)]);
|
|
212
|
+
const user = this.rowToUser(row);
|
|
213
|
+
return { success: true, token, user: this.sanitizeUser(user) };
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
Logger.error('Login error:', error);
|
|
217
|
+
return { success: false, error: 'Login failed' };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// JSON fallback
|
|
221
|
+
await this.ensureJsonInitialized();
|
|
142
222
|
const user = this.users.find(u => u.email.toLowerCase() === email.toLowerCase());
|
|
143
223
|
if (!user) {
|
|
144
224
|
return { success: false, error: 'Invalid email or password' };
|
|
@@ -146,10 +226,8 @@ export class AuthService {
|
|
|
146
226
|
if (user.passwordHash !== this.hashPassword(password)) {
|
|
147
227
|
return { success: false, error: 'Invalid email or password' };
|
|
148
228
|
}
|
|
149
|
-
// Update last login
|
|
150
229
|
user.lastLoginAt = new Date();
|
|
151
230
|
await this.saveUsers();
|
|
152
|
-
// Create session
|
|
153
231
|
const token = this.generateToken();
|
|
154
232
|
const session = {
|
|
155
233
|
token,
|
|
@@ -158,19 +236,44 @@ export class AuthService {
|
|
|
158
236
|
};
|
|
159
237
|
this.sessions.push(session);
|
|
160
238
|
await this.saveSessions();
|
|
161
|
-
return {
|
|
162
|
-
success: true,
|
|
163
|
-
token,
|
|
164
|
-
user: this.sanitizeUser(user)
|
|
165
|
-
};
|
|
239
|
+
return { success: true, token, user: this.sanitizeUser(user) };
|
|
166
240
|
}
|
|
167
241
|
async oauthLogin(provider, profile) {
|
|
168
|
-
|
|
169
|
-
|
|
242
|
+
if (this.useDatabase()) {
|
|
243
|
+
try {
|
|
244
|
+
// Find by provider ID or email
|
|
245
|
+
let result = await db.query(`SELECT * FROM users WHERE (provider = $1 AND provider_id = $2) OR LOWER(email) = LOWER($3) LIMIT 1`, [provider, profile.id, profile.email]);
|
|
246
|
+
let userId;
|
|
247
|
+
if (result.rows.length === 0) {
|
|
248
|
+
// Create new user
|
|
249
|
+
userId = 'user-' + randomUUID();
|
|
250
|
+
await db.query(`INSERT INTO users (id, email, username, avatar, tier, provider, provider_id)
|
|
251
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)`, [userId, profile.email.toLowerCase(), profile.name || profile.email.split('@')[0], profile.avatar, 'free', provider, profile.id]);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
// Update existing
|
|
255
|
+
userId = result.rows[0].id;
|
|
256
|
+
await db.query(`UPDATE users SET last_login_at = NOW(), avatar = COALESCE($2, avatar),
|
|
257
|
+
provider = CASE WHEN provider_id IS NULL THEN $3 ELSE provider END,
|
|
258
|
+
provider_id = CASE WHEN provider_id IS NULL THEN $4 ELSE provider_id END
|
|
259
|
+
WHERE id = $1`, [userId, profile.avatar, provider, profile.id]);
|
|
260
|
+
}
|
|
261
|
+
const token = this.generateToken();
|
|
262
|
+
await db.query('INSERT INTO sessions (token, user_id, expires_at) VALUES ($1, $2, $3)', [token, userId, new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)]);
|
|
263
|
+
result = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
|
|
264
|
+
const user = this.rowToUser(result.rows[0]);
|
|
265
|
+
return { success: true, token, user: this.sanitizeUser(user) };
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
Logger.error('OAuth login error:', error);
|
|
269
|
+
return { success: false, error: 'OAuth login failed' };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// JSON fallback
|
|
273
|
+
await this.ensureJsonInitialized();
|
|
170
274
|
let user = this.users.find(u => (u.provider === provider && u.providerId === profile.id) ||
|
|
171
275
|
u.email.toLowerCase() === profile.email.toLowerCase());
|
|
172
276
|
if (!user) {
|
|
173
|
-
// Create new user
|
|
174
277
|
user = {
|
|
175
278
|
id: 'user-' + randomUUID(),
|
|
176
279
|
email: profile.email.toLowerCase(),
|
|
@@ -186,7 +289,6 @@ export class AuthService {
|
|
|
186
289
|
this.users.push(user);
|
|
187
290
|
}
|
|
188
291
|
else {
|
|
189
|
-
// Update existing user
|
|
190
292
|
user.lastLoginAt = new Date();
|
|
191
293
|
if (profile.avatar)
|
|
192
294
|
user.avatar = profile.avatar;
|
|
@@ -196,7 +298,6 @@ export class AuthService {
|
|
|
196
298
|
}
|
|
197
299
|
}
|
|
198
300
|
await this.saveUsers();
|
|
199
|
-
// Create session
|
|
200
301
|
const token = this.generateToken();
|
|
201
302
|
const session = {
|
|
202
303
|
token,
|
|
@@ -205,37 +306,78 @@ export class AuthService {
|
|
|
205
306
|
};
|
|
206
307
|
this.sessions.push(session);
|
|
207
308
|
await this.saveSessions();
|
|
208
|
-
return {
|
|
209
|
-
success: true,
|
|
210
|
-
token,
|
|
211
|
-
user: this.sanitizeUser(user)
|
|
212
|
-
};
|
|
309
|
+
return { success: true, token, user: this.sanitizeUser(user) };
|
|
213
310
|
}
|
|
214
311
|
async validateToken(token) {
|
|
215
|
-
|
|
312
|
+
if (this.useDatabase()) {
|
|
313
|
+
try {
|
|
314
|
+
const result = await db.query('SELECT * FROM sessions WHERE token = $1', [token]);
|
|
315
|
+
if (result.rows.length === 0)
|
|
316
|
+
return null;
|
|
317
|
+
const session = result.rows[0];
|
|
318
|
+
if (new Date(session.expires_at) < new Date()) {
|
|
319
|
+
await db.query('DELETE FROM sessions WHERE token = $1', [token]);
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
const userResult = await db.query('SELECT * FROM users WHERE id = $1', [session.user_id]);
|
|
323
|
+
if (userResult.rows.length === 0)
|
|
324
|
+
return null;
|
|
325
|
+
return this.rowToUser(userResult.rows[0]);
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
Logger.error('Token validation error:', error);
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// JSON fallback
|
|
333
|
+
await this.ensureJsonInitialized();
|
|
216
334
|
const session = this.sessions.find(s => s.token === token);
|
|
217
335
|
if (!session)
|
|
218
336
|
return null;
|
|
219
337
|
if (new Date(session.expiresAt) < new Date()) {
|
|
220
|
-
// Session expired, remove it
|
|
221
338
|
this.sessions = this.sessions.filter(s => s.token !== token);
|
|
222
339
|
await this.saveSessions();
|
|
223
340
|
return null;
|
|
224
341
|
}
|
|
225
|
-
|
|
226
|
-
return user || null;
|
|
342
|
+
return this.users.find(u => u.id === session.userId) || null;
|
|
227
343
|
}
|
|
228
344
|
async logout(token) {
|
|
229
|
-
|
|
345
|
+
if (this.useDatabase()) {
|
|
346
|
+
await db.query('DELETE FROM sessions WHERE token = $1', [token]);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
await this.ensureJsonInitialized();
|
|
230
350
|
this.sessions = this.sessions.filter(s => s.token !== token);
|
|
231
351
|
await this.saveSessions();
|
|
232
352
|
}
|
|
233
353
|
async getUser(userId) {
|
|
234
|
-
|
|
354
|
+
if (this.useDatabase()) {
|
|
355
|
+
try {
|
|
356
|
+
const result = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
|
|
357
|
+
if (result.rows.length === 0)
|
|
358
|
+
return null;
|
|
359
|
+
return this.rowToUser(result.rows[0]);
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
Logger.error('Get user error:', error);
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
await this.ensureJsonInitialized();
|
|
235
367
|
return this.users.find(u => u.id === userId) || null;
|
|
236
368
|
}
|
|
237
369
|
async updateUserTier(userId, tier) {
|
|
238
|
-
|
|
370
|
+
if (this.useDatabase()) {
|
|
371
|
+
try {
|
|
372
|
+
const result = await db.query('UPDATE users SET tier = $1 WHERE id = $2', [tier, userId]);
|
|
373
|
+
return (result.rowCount ?? 0) > 0;
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
Logger.error('Update tier error:', error);
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
await this.ensureJsonInitialized();
|
|
239
381
|
const user = this.users.find(u => u.id === userId);
|
|
240
382
|
if (!user)
|
|
241
383
|
return false;
|
|
@@ -244,12 +386,59 @@ export class AuthService {
|
|
|
244
386
|
return true;
|
|
245
387
|
}
|
|
246
388
|
async checkAndUpdateUsage(userId, type) {
|
|
247
|
-
|
|
389
|
+
if (this.useDatabase()) {
|
|
390
|
+
try {
|
|
391
|
+
const result = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
|
|
392
|
+
if (result.rows.length === 0)
|
|
393
|
+
return { allowed: false, remaining: 0, limit: 0 };
|
|
394
|
+
const row = result.rows[0];
|
|
395
|
+
const today = new Date().toISOString().split('T')[0];
|
|
396
|
+
// Reset usage if new day
|
|
397
|
+
if (row.usage_reset_date !== today) {
|
|
398
|
+
await db.query(`UPDATE users SET requests_today = 0, full_analysis_today = 0, projects_today = 0, usage_reset_date = $1 WHERE id = $2`, [today, userId]);
|
|
399
|
+
row.requests_today = 0;
|
|
400
|
+
row.full_analysis_today = 0;
|
|
401
|
+
row.projects_today = 0;
|
|
402
|
+
}
|
|
403
|
+
const limits = TIER_LIMITS[row.tier];
|
|
404
|
+
let current;
|
|
405
|
+
let limit;
|
|
406
|
+
let column;
|
|
407
|
+
switch (type) {
|
|
408
|
+
case 'request':
|
|
409
|
+
current = row.requests_today;
|
|
410
|
+
limit = limits.requestsPerDay;
|
|
411
|
+
column = 'requests_today';
|
|
412
|
+
break;
|
|
413
|
+
case 'analysis':
|
|
414
|
+
current = row.full_analysis_today;
|
|
415
|
+
limit = limits.fullAnalysisPerDay;
|
|
416
|
+
column = 'full_analysis_today';
|
|
417
|
+
break;
|
|
418
|
+
case 'project':
|
|
419
|
+
current = row.projects_today;
|
|
420
|
+
limit = limits.projectsPerDay;
|
|
421
|
+
column = 'projects_today';
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
if (current >= limit && limit !== Infinity) {
|
|
425
|
+
return { allowed: false, remaining: 0, limit };
|
|
426
|
+
}
|
|
427
|
+
await db.query(`UPDATE users SET ${column} = ${column} + 1 WHERE id = $1`, [userId]);
|
|
428
|
+
const remaining = limit === Infinity ? Infinity : limit - current - 1;
|
|
429
|
+
return { allowed: true, remaining, limit };
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
Logger.error('Usage check error:', error);
|
|
433
|
+
return { allowed: false, remaining: 0, limit: 0 };
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// JSON fallback
|
|
437
|
+
await this.ensureJsonInitialized();
|
|
248
438
|
const user = this.users.find(u => u.id === userId);
|
|
249
439
|
if (!user)
|
|
250
440
|
return { allowed: false, remaining: 0, limit: 0 };
|
|
251
441
|
const today = new Date().toISOString().split('T')[0];
|
|
252
|
-
// Reset usage if new day
|
|
253
442
|
if (user.usage.lastResetDate !== today) {
|
|
254
443
|
user.usage = this.createEmptyUsage();
|
|
255
444
|
}
|
|
@@ -273,7 +462,6 @@ export class AuthService {
|
|
|
273
462
|
if (current >= limit && limit !== Infinity) {
|
|
274
463
|
return { allowed: false, remaining: 0, limit };
|
|
275
464
|
}
|
|
276
|
-
// Increment usage
|
|
277
465
|
switch (type) {
|
|
278
466
|
case 'request':
|
|
279
467
|
user.usage.requestsToday++;
|
|
@@ -290,15 +478,38 @@ export class AuthService {
|
|
|
290
478
|
return { allowed: true, remaining, limit };
|
|
291
479
|
}
|
|
292
480
|
async getAllUsers() {
|
|
293
|
-
|
|
481
|
+
if (this.useDatabase()) {
|
|
482
|
+
try {
|
|
483
|
+
const result = await db.query('SELECT * FROM users ORDER BY created_at DESC');
|
|
484
|
+
return result.rows.map(row => this.sanitizeUser(this.rowToUser(row)));
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
Logger.error('Get all users error:', error);
|
|
488
|
+
return [];
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
await this.ensureJsonInitialized();
|
|
294
492
|
return this.users.map(u => this.sanitizeUser(u));
|
|
295
493
|
}
|
|
296
494
|
async deleteUser(userId) {
|
|
297
|
-
|
|
495
|
+
if (this.useDatabase()) {
|
|
496
|
+
try {
|
|
497
|
+
// Don't delete admin
|
|
498
|
+
const check = await db.query('SELECT tier FROM users WHERE id = $1', [userId]);
|
|
499
|
+
if (check.rows.length === 0 || check.rows[0].tier === 'admin')
|
|
500
|
+
return false;
|
|
501
|
+
await db.query('DELETE FROM users WHERE id = $1', [userId]);
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
Logger.error('Delete user error:', error);
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
await this.ensureJsonInitialized();
|
|
298
510
|
const index = this.users.findIndex(u => u.id === userId);
|
|
299
511
|
if (index === -1)
|
|
300
512
|
return false;
|
|
301
|
-
// Don't delete admin
|
|
302
513
|
if (this.users[index].tier === 'admin')
|
|
303
514
|
return false;
|
|
304
515
|
this.users.splice(index, 1);
|
|
@@ -308,7 +519,35 @@ export class AuthService {
|
|
|
308
519
|
return true;
|
|
309
520
|
}
|
|
310
521
|
async updateUser(userId, updates) {
|
|
311
|
-
|
|
522
|
+
if (this.useDatabase()) {
|
|
523
|
+
try {
|
|
524
|
+
const sets = [];
|
|
525
|
+
const values = [];
|
|
526
|
+
let paramIndex = 1;
|
|
527
|
+
if (updates.username) {
|
|
528
|
+
sets.push(`username = $${paramIndex++}`);
|
|
529
|
+
values.push(updates.username);
|
|
530
|
+
}
|
|
531
|
+
if (updates.email) {
|
|
532
|
+
sets.push(`email = $${paramIndex++}`);
|
|
533
|
+
values.push(updates.email);
|
|
534
|
+
}
|
|
535
|
+
if (updates.avatar) {
|
|
536
|
+
sets.push(`avatar = $${paramIndex++}`);
|
|
537
|
+
values.push(updates.avatar);
|
|
538
|
+
}
|
|
539
|
+
if (sets.length === 0)
|
|
540
|
+
return true;
|
|
541
|
+
values.push(userId);
|
|
542
|
+
const result = await db.query(`UPDATE users SET ${sets.join(', ')} WHERE id = $${paramIndex}`, values);
|
|
543
|
+
return (result.rowCount ?? 0) > 0;
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
Logger.error('Update user error:', error);
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
await this.ensureJsonInitialized();
|
|
312
551
|
const user = this.users.find(u => u.id === userId);
|
|
313
552
|
if (!user)
|
|
314
553
|
return false;
|
|
@@ -321,5 +560,13 @@ export class AuthService {
|
|
|
321
560
|
await this.saveUsers();
|
|
322
561
|
return true;
|
|
323
562
|
}
|
|
563
|
+
/**
|
|
564
|
+
* Initialize database and create default admin if needed
|
|
565
|
+
*/
|
|
566
|
+
async initDatabase() {
|
|
567
|
+
if (this.useDatabase()) {
|
|
568
|
+
await this.dbCreateDefaultAdmin();
|
|
569
|
+
}
|
|
570
|
+
}
|
|
324
571
|
}
|
|
325
572
|
//# sourceMappingURL=auth-service.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ArchiCore Redis Cache Service
|
|
3
|
+
*
|
|
4
|
+
* Provides caching for API responses to improve performance
|
|
5
|
+
*/
|
|
6
|
+
export interface CacheOptions {
|
|
7
|
+
ttl?: number;
|
|
8
|
+
prefix?: string;
|
|
9
|
+
}
|
|
10
|
+
declare class CacheService {
|
|
11
|
+
private client;
|
|
12
|
+
private enabled;
|
|
13
|
+
private memoryCache;
|
|
14
|
+
connect(): Promise<void>;
|
|
15
|
+
private getKey;
|
|
16
|
+
/**
|
|
17
|
+
* Get value from cache
|
|
18
|
+
*/
|
|
19
|
+
get<T>(key: string, prefix?: string): Promise<T | null>;
|
|
20
|
+
/**
|
|
21
|
+
* Set value in cache
|
|
22
|
+
*/
|
|
23
|
+
set(key: string, value: any, options?: CacheOptions): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Delete value from cache
|
|
26
|
+
*/
|
|
27
|
+
del(key: string, prefix?: string): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Delete all keys matching pattern
|
|
30
|
+
*/
|
|
31
|
+
delPattern(pattern: string): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Clear all cache
|
|
34
|
+
*/
|
|
35
|
+
flush(): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Get or set cache with callback
|
|
38
|
+
*/
|
|
39
|
+
getOrSet<T>(key: string, callback: () => Promise<T>, options?: CacheOptions): Promise<T>;
|
|
40
|
+
/**
|
|
41
|
+
* Check if cache is available
|
|
42
|
+
*/
|
|
43
|
+
isEnabled(): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Get cache stats
|
|
46
|
+
*/
|
|
47
|
+
getStats(): Promise<{
|
|
48
|
+
enabled: boolean;
|
|
49
|
+
type: string;
|
|
50
|
+
keys: number;
|
|
51
|
+
}>;
|
|
52
|
+
/**
|
|
53
|
+
* Cleanup expired memory cache entries
|
|
54
|
+
*/
|
|
55
|
+
private cleanupMemoryCache;
|
|
56
|
+
/**
|
|
57
|
+
* Disconnect from Redis
|
|
58
|
+
*/
|
|
59
|
+
disconnect(): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
export declare const cache: CacheService;
|
|
62
|
+
export declare const cacheKeys: {
|
|
63
|
+
project: (projectId: string) => string;
|
|
64
|
+
projectAnalysis: (projectId: string) => string;
|
|
65
|
+
projectMetrics: (projectId: string) => string;
|
|
66
|
+
projectSearch: (projectId: string, query: string) => string;
|
|
67
|
+
user: (userId: string) => string;
|
|
68
|
+
userProjects: (userId: string) => string;
|
|
69
|
+
};
|
|
70
|
+
export declare const cacheTTL: {
|
|
71
|
+
short: number;
|
|
72
|
+
medium: number;
|
|
73
|
+
long: number;
|
|
74
|
+
day: number;
|
|
75
|
+
};
|
|
76
|
+
export {};
|
|
77
|
+
//# sourceMappingURL=cache.d.ts.map
|