archicore 0.2.0 → 0.2.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/dist/cli/commands/init.js +37 -14
- package/dist/cli/commands/interactive.js +174 -21
- package/dist/github/github-service.d.ts +5 -1
- package/dist/github/github-service.js +21 -3
- package/dist/orchestrator/index.js +19 -0
- package/dist/semantic-memory/embedding-service.d.ts +8 -1
- package/dist/semantic-memory/embedding-service.js +141 -47
- package/dist/server/index.js +163 -1
- package/dist/server/routes/admin.js +439 -1
- package/dist/server/routes/api.js +17 -2
- package/dist/server/routes/auth.js +46 -0
- package/dist/server/routes/developer.js +1 -1
- package/dist/server/routes/device-auth.js +10 -1
- package/dist/server/routes/github.js +17 -4
- package/dist/server/routes/report-issue.d.ts +7 -0
- package/dist/server/routes/report-issue.js +307 -0
- 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 +32 -5
- package/dist/server/services/auth-service.js +347 -54
- 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/dist/server/services/encryption.d.ts +48 -0
- package/dist/server/services/encryption.js +148 -0
- package/package.json +17 -2
|
@@ -1,40 +1,46 @@
|
|
|
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';
|
|
9
|
+
import bcrypt from 'bcrypt';
|
|
7
10
|
import { join } from 'path';
|
|
8
11
|
import { TIER_LIMITS } from '../../types/user.js';
|
|
12
|
+
import { db } from './database.js';
|
|
13
|
+
import { Logger } from '../../utils/logger.js';
|
|
9
14
|
const DATA_DIR = '.archicore';
|
|
10
15
|
const USERS_FILE = 'users.json';
|
|
11
16
|
const SESSIONS_FILE = 'sessions.json';
|
|
12
17
|
export class AuthService {
|
|
13
18
|
static instance = null;
|
|
14
19
|
dataDir;
|
|
20
|
+
// JSON fallback storage
|
|
15
21
|
users = [];
|
|
16
22
|
sessions = [];
|
|
17
|
-
|
|
23
|
+
jsonInitialized = false;
|
|
18
24
|
constructor(dataDir = DATA_DIR) {
|
|
19
25
|
this.dataDir = dataDir;
|
|
20
26
|
}
|
|
21
|
-
/**
|
|
22
|
-
* Get singleton instance of AuthService
|
|
23
|
-
*/
|
|
24
27
|
static getInstance() {
|
|
25
28
|
if (!AuthService.instance) {
|
|
26
29
|
AuthService.instance = new AuthService();
|
|
27
30
|
}
|
|
28
31
|
return AuthService.instance;
|
|
29
32
|
}
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
useDatabase() {
|
|
34
|
+
return db.isAvailable();
|
|
35
|
+
}
|
|
36
|
+
// ========== JSON FALLBACK METHODS ==========
|
|
37
|
+
async ensureJsonInitialized() {
|
|
38
|
+
if (this.jsonInitialized)
|
|
32
39
|
return;
|
|
33
40
|
try {
|
|
34
41
|
await mkdir(this.dataDir, { recursive: true });
|
|
35
42
|
}
|
|
36
43
|
catch { }
|
|
37
|
-
// Load users
|
|
38
44
|
try {
|
|
39
45
|
const usersPath = join(this.dataDir, USERS_FILE);
|
|
40
46
|
const data = await readFile(usersPath, 'utf-8');
|
|
@@ -43,10 +49,8 @@ export class AuthService {
|
|
|
43
49
|
}
|
|
44
50
|
catch {
|
|
45
51
|
this.users = [];
|
|
46
|
-
// Create default admin user
|
|
47
52
|
await this.createDefaultAdmin();
|
|
48
53
|
}
|
|
49
|
-
// Load sessions
|
|
50
54
|
try {
|
|
51
55
|
const sessionsPath = join(this.dataDir, SESSIONS_FILE);
|
|
52
56
|
const data = await readFile(sessionsPath, 'utf-8');
|
|
@@ -56,13 +60,13 @@ export class AuthService {
|
|
|
56
60
|
catch {
|
|
57
61
|
this.sessions = [];
|
|
58
62
|
}
|
|
59
|
-
this.
|
|
63
|
+
this.jsonInitialized = true;
|
|
60
64
|
}
|
|
61
65
|
async createDefaultAdmin() {
|
|
62
66
|
const admin = {
|
|
63
67
|
id: 'admin-' + randomUUID(),
|
|
64
68
|
email: 'admin@archicore.io',
|
|
65
|
-
username: '
|
|
69
|
+
username: 'ArchiCore Team',
|
|
66
70
|
passwordHash: this.hashPassword('admin123'),
|
|
67
71
|
tier: 'admin',
|
|
68
72
|
provider: 'email',
|
|
@@ -81,9 +85,46 @@ export class AuthService {
|
|
|
81
85
|
const sessionsPath = join(this.dataDir, SESSIONS_FILE);
|
|
82
86
|
await writeFile(sessionsPath, JSON.stringify({ sessions: this.sessions }, null, 2));
|
|
83
87
|
}
|
|
84
|
-
|
|
88
|
+
// ========== HELPER METHODS ==========
|
|
89
|
+
BCRYPT_ROUNDS = 12;
|
|
90
|
+
/**
|
|
91
|
+
* Hash password with bcrypt (async)
|
|
92
|
+
*/
|
|
93
|
+
async hashPasswordAsync(password) {
|
|
94
|
+
return bcrypt.hash(password, this.BCRYPT_ROUNDS);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Legacy SHA256 hash for backward compatibility
|
|
98
|
+
*/
|
|
99
|
+
hashPasswordLegacy(password) {
|
|
85
100
|
return createHash('sha256').update(password + 'archicore-salt-2024').digest('hex');
|
|
86
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Verify password against hash (supports both bcrypt and legacy SHA256)
|
|
104
|
+
*/
|
|
105
|
+
async verifyPassword(password, hash) {
|
|
106
|
+
// Check if it's a bcrypt hash (starts with $2b$ or $2a$)
|
|
107
|
+
if (hash.startsWith('$2')) {
|
|
108
|
+
return bcrypt.compare(password, hash);
|
|
109
|
+
}
|
|
110
|
+
// Legacy SHA256 hash
|
|
111
|
+
return this.hashPasswordLegacy(password) === hash;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Check if password needs rehashing (from legacy to bcrypt)
|
|
115
|
+
*/
|
|
116
|
+
needsRehash(hash) {
|
|
117
|
+
return !hash.startsWith('$2');
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Sync hash for JSON fallback (still uses legacy for simplicity)
|
|
121
|
+
* @deprecated Use hashPasswordAsync for new passwords
|
|
122
|
+
*/
|
|
123
|
+
hashPassword(password) {
|
|
124
|
+
// For new registrations, we should use async version
|
|
125
|
+
// This is kept for backward compatibility with JSON storage
|
|
126
|
+
return this.hashPasswordLegacy(password);
|
|
127
|
+
}
|
|
87
128
|
createEmptyUsage() {
|
|
88
129
|
return {
|
|
89
130
|
requestsToday: 0,
|
|
@@ -99,13 +140,73 @@ export class AuthService {
|
|
|
99
140
|
const { passwordHash, ...sanitized } = user;
|
|
100
141
|
return sanitized;
|
|
101
142
|
}
|
|
143
|
+
rowToUser(row) {
|
|
144
|
+
return {
|
|
145
|
+
id: row.id,
|
|
146
|
+
email: row.email,
|
|
147
|
+
username: row.username,
|
|
148
|
+
passwordHash: row.password_hash || undefined,
|
|
149
|
+
avatar: row.avatar || undefined,
|
|
150
|
+
tier: row.tier,
|
|
151
|
+
provider: row.provider,
|
|
152
|
+
providerId: row.provider_id || undefined,
|
|
153
|
+
createdAt: row.created_at,
|
|
154
|
+
lastLoginAt: row.last_login_at,
|
|
155
|
+
usage: {
|
|
156
|
+
requestsToday: row.requests_today,
|
|
157
|
+
fullAnalysisToday: row.full_analysis_today,
|
|
158
|
+
projectsToday: row.projects_today,
|
|
159
|
+
lastResetDate: row.usage_reset_date
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// ========== DATABASE METHODS ==========
|
|
164
|
+
async dbCreateDefaultAdmin() {
|
|
165
|
+
const result = await db.query('SELECT id FROM users WHERE tier = $1 LIMIT 1', ['admin']);
|
|
166
|
+
if (result.rows.length > 0)
|
|
167
|
+
return;
|
|
168
|
+
const adminId = 'admin-' + randomUUID();
|
|
169
|
+
await db.query(`INSERT INTO users (id, email, username, password_hash, tier, provider)
|
|
170
|
+
VALUES ($1, $2, $3, $4, $5, $6)`, [adminId, 'admin@archicore.io', 'ArchiCore Team', this.hashPassword('admin123'), 'admin', 'email']);
|
|
171
|
+
Logger.info('Default admin user created');
|
|
172
|
+
}
|
|
173
|
+
// ========== PUBLIC API ==========
|
|
102
174
|
async register(email, username, password) {
|
|
103
|
-
|
|
104
|
-
|
|
175
|
+
if (this.useDatabase()) {
|
|
176
|
+
try {
|
|
177
|
+
// Check email exists
|
|
178
|
+
const emailCheck = await db.query('SELECT id FROM users WHERE LOWER(email) = LOWER($1)', [email]);
|
|
179
|
+
if (emailCheck.rows.length > 0) {
|
|
180
|
+
return { success: false, error: 'Email already registered' };
|
|
181
|
+
}
|
|
182
|
+
// Check username exists
|
|
183
|
+
const usernameCheck = await db.query('SELECT id FROM users WHERE LOWER(username) = LOWER($1)', [username]);
|
|
184
|
+
if (usernameCheck.rows.length > 0) {
|
|
185
|
+
return { success: false, error: 'Username already taken' };
|
|
186
|
+
}
|
|
187
|
+
const userId = 'user-' + randomUUID();
|
|
188
|
+
const token = this.generateToken();
|
|
189
|
+
const passwordHash = await this.hashPasswordAsync(password);
|
|
190
|
+
await db.transaction(async (client) => {
|
|
191
|
+
await client.query(`INSERT INTO users (id, email, username, password_hash, tier, provider)
|
|
192
|
+
VALUES ($1, $2, $3, $4, $5, $6)`, [userId, email.toLowerCase(), username, passwordHash, 'free', 'email']);
|
|
193
|
+
await client.query(`INSERT INTO sessions (token, user_id, expires_at)
|
|
194
|
+
VALUES ($1, $2, $3)`, [token, userId, new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)]);
|
|
195
|
+
});
|
|
196
|
+
const userResult = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
|
|
197
|
+
const user = this.rowToUser(userResult.rows[0]);
|
|
198
|
+
return { success: true, token, user: this.sanitizeUser(user) };
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
Logger.error('Register error:', error);
|
|
202
|
+
return { success: false, error: 'Registration failed' };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// JSON fallback
|
|
206
|
+
await this.ensureJsonInitialized();
|
|
105
207
|
if (this.users.find(u => u.email.toLowerCase() === email.toLowerCase())) {
|
|
106
208
|
return { success: false, error: 'Email already registered' };
|
|
107
209
|
}
|
|
108
|
-
// Check if username already exists
|
|
109
210
|
if (this.users.find(u => u.username.toLowerCase() === username.toLowerCase())) {
|
|
110
211
|
return { success: false, error: 'Username already taken' };
|
|
111
212
|
}
|
|
@@ -122,23 +223,48 @@ export class AuthService {
|
|
|
122
223
|
};
|
|
123
224
|
this.users.push(user);
|
|
124
225
|
await this.saveUsers();
|
|
125
|
-
// Create session
|
|
126
226
|
const token = this.generateToken();
|
|
127
227
|
const session = {
|
|
128
228
|
token,
|
|
129
229
|
userId: user.id,
|
|
130
|
-
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
|
|
230
|
+
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
|
|
131
231
|
};
|
|
132
232
|
this.sessions.push(session);
|
|
133
233
|
await this.saveSessions();
|
|
134
|
-
return {
|
|
135
|
-
success: true,
|
|
136
|
-
token,
|
|
137
|
-
user: this.sanitizeUser(user)
|
|
138
|
-
};
|
|
234
|
+
return { success: true, token, user: this.sanitizeUser(user) };
|
|
139
235
|
}
|
|
140
236
|
async login(email, password) {
|
|
141
|
-
|
|
237
|
+
if (this.useDatabase()) {
|
|
238
|
+
try {
|
|
239
|
+
const result = await db.query('SELECT * FROM users WHERE LOWER(email) = LOWER($1)', [email]);
|
|
240
|
+
if (result.rows.length === 0) {
|
|
241
|
+
return { success: false, error: 'Invalid email or password' };
|
|
242
|
+
}
|
|
243
|
+
const row = result.rows[0];
|
|
244
|
+
// Verify password (supports both bcrypt and legacy SHA256)
|
|
245
|
+
const isValid = await this.verifyPassword(password, row.password_hash || '');
|
|
246
|
+
if (!isValid) {
|
|
247
|
+
return { success: false, error: 'Invalid email or password' };
|
|
248
|
+
}
|
|
249
|
+
const token = this.generateToken();
|
|
250
|
+
// Rehash legacy passwords to bcrypt
|
|
251
|
+
if (row.password_hash && this.needsRehash(row.password_hash)) {
|
|
252
|
+
const newHash = await this.hashPasswordAsync(password);
|
|
253
|
+
await db.query('UPDATE users SET password_hash = $1 WHERE id = $2', [newHash, row.id]);
|
|
254
|
+
Logger.info(`Upgraded password hash for user ${row.id}`);
|
|
255
|
+
}
|
|
256
|
+
await db.query('UPDATE users SET last_login_at = NOW() WHERE id = $1', [row.id]);
|
|
257
|
+
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)]);
|
|
258
|
+
const user = this.rowToUser(row);
|
|
259
|
+
return { success: true, token, user: this.sanitizeUser(user) };
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
Logger.error('Login error:', error);
|
|
263
|
+
return { success: false, error: 'Login failed' };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// JSON fallback
|
|
267
|
+
await this.ensureJsonInitialized();
|
|
142
268
|
const user = this.users.find(u => u.email.toLowerCase() === email.toLowerCase());
|
|
143
269
|
if (!user) {
|
|
144
270
|
return { success: false, error: 'Invalid email or password' };
|
|
@@ -146,10 +272,8 @@ export class AuthService {
|
|
|
146
272
|
if (user.passwordHash !== this.hashPassword(password)) {
|
|
147
273
|
return { success: false, error: 'Invalid email or password' };
|
|
148
274
|
}
|
|
149
|
-
// Update last login
|
|
150
275
|
user.lastLoginAt = new Date();
|
|
151
276
|
await this.saveUsers();
|
|
152
|
-
// Create session
|
|
153
277
|
const token = this.generateToken();
|
|
154
278
|
const session = {
|
|
155
279
|
token,
|
|
@@ -158,19 +282,44 @@ export class AuthService {
|
|
|
158
282
|
};
|
|
159
283
|
this.sessions.push(session);
|
|
160
284
|
await this.saveSessions();
|
|
161
|
-
return {
|
|
162
|
-
success: true,
|
|
163
|
-
token,
|
|
164
|
-
user: this.sanitizeUser(user)
|
|
165
|
-
};
|
|
285
|
+
return { success: true, token, user: this.sanitizeUser(user) };
|
|
166
286
|
}
|
|
167
287
|
async oauthLogin(provider, profile) {
|
|
168
|
-
|
|
169
|
-
|
|
288
|
+
if (this.useDatabase()) {
|
|
289
|
+
try {
|
|
290
|
+
// Find by provider ID or email
|
|
291
|
+
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]);
|
|
292
|
+
let userId;
|
|
293
|
+
if (result.rows.length === 0) {
|
|
294
|
+
// Create new user
|
|
295
|
+
userId = 'user-' + randomUUID();
|
|
296
|
+
await db.query(`INSERT INTO users (id, email, username, avatar, tier, provider, provider_id)
|
|
297
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)`, [userId, profile.email.toLowerCase(), profile.name || profile.email.split('@')[0], profile.avatar, 'free', provider, profile.id]);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
// Update existing
|
|
301
|
+
userId = result.rows[0].id;
|
|
302
|
+
await db.query(`UPDATE users SET last_login_at = NOW(), avatar = COALESCE($2, avatar),
|
|
303
|
+
provider = CASE WHEN provider_id IS NULL THEN $3 ELSE provider END,
|
|
304
|
+
provider_id = CASE WHEN provider_id IS NULL THEN $4 ELSE provider_id END
|
|
305
|
+
WHERE id = $1`, [userId, profile.avatar, provider, profile.id]);
|
|
306
|
+
}
|
|
307
|
+
const token = this.generateToken();
|
|
308
|
+
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)]);
|
|
309
|
+
result = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
|
|
310
|
+
const user = this.rowToUser(result.rows[0]);
|
|
311
|
+
return { success: true, token, user: this.sanitizeUser(user) };
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
Logger.error('OAuth login error:', error);
|
|
315
|
+
return { success: false, error: 'OAuth login failed' };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// JSON fallback
|
|
319
|
+
await this.ensureJsonInitialized();
|
|
170
320
|
let user = this.users.find(u => (u.provider === provider && u.providerId === profile.id) ||
|
|
171
321
|
u.email.toLowerCase() === profile.email.toLowerCase());
|
|
172
322
|
if (!user) {
|
|
173
|
-
// Create new user
|
|
174
323
|
user = {
|
|
175
324
|
id: 'user-' + randomUUID(),
|
|
176
325
|
email: profile.email.toLowerCase(),
|
|
@@ -186,7 +335,6 @@ export class AuthService {
|
|
|
186
335
|
this.users.push(user);
|
|
187
336
|
}
|
|
188
337
|
else {
|
|
189
|
-
// Update existing user
|
|
190
338
|
user.lastLoginAt = new Date();
|
|
191
339
|
if (profile.avatar)
|
|
192
340
|
user.avatar = profile.avatar;
|
|
@@ -196,7 +344,6 @@ export class AuthService {
|
|
|
196
344
|
}
|
|
197
345
|
}
|
|
198
346
|
await this.saveUsers();
|
|
199
|
-
// Create session
|
|
200
347
|
const token = this.generateToken();
|
|
201
348
|
const session = {
|
|
202
349
|
token,
|
|
@@ -205,37 +352,78 @@ export class AuthService {
|
|
|
205
352
|
};
|
|
206
353
|
this.sessions.push(session);
|
|
207
354
|
await this.saveSessions();
|
|
208
|
-
return {
|
|
209
|
-
success: true,
|
|
210
|
-
token,
|
|
211
|
-
user: this.sanitizeUser(user)
|
|
212
|
-
};
|
|
355
|
+
return { success: true, token, user: this.sanitizeUser(user) };
|
|
213
356
|
}
|
|
214
357
|
async validateToken(token) {
|
|
215
|
-
|
|
358
|
+
if (this.useDatabase()) {
|
|
359
|
+
try {
|
|
360
|
+
const result = await db.query('SELECT * FROM sessions WHERE token = $1', [token]);
|
|
361
|
+
if (result.rows.length === 0)
|
|
362
|
+
return null;
|
|
363
|
+
const session = result.rows[0];
|
|
364
|
+
if (new Date(session.expires_at) < new Date()) {
|
|
365
|
+
await db.query('DELETE FROM sessions WHERE token = $1', [token]);
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
const userResult = await db.query('SELECT * FROM users WHERE id = $1', [session.user_id]);
|
|
369
|
+
if (userResult.rows.length === 0)
|
|
370
|
+
return null;
|
|
371
|
+
return this.rowToUser(userResult.rows[0]);
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
Logger.error('Token validation error:', error);
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// JSON fallback
|
|
379
|
+
await this.ensureJsonInitialized();
|
|
216
380
|
const session = this.sessions.find(s => s.token === token);
|
|
217
381
|
if (!session)
|
|
218
382
|
return null;
|
|
219
383
|
if (new Date(session.expiresAt) < new Date()) {
|
|
220
|
-
// Session expired, remove it
|
|
221
384
|
this.sessions = this.sessions.filter(s => s.token !== token);
|
|
222
385
|
await this.saveSessions();
|
|
223
386
|
return null;
|
|
224
387
|
}
|
|
225
|
-
|
|
226
|
-
return user || null;
|
|
388
|
+
return this.users.find(u => u.id === session.userId) || null;
|
|
227
389
|
}
|
|
228
390
|
async logout(token) {
|
|
229
|
-
|
|
391
|
+
if (this.useDatabase()) {
|
|
392
|
+
await db.query('DELETE FROM sessions WHERE token = $1', [token]);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
await this.ensureJsonInitialized();
|
|
230
396
|
this.sessions = this.sessions.filter(s => s.token !== token);
|
|
231
397
|
await this.saveSessions();
|
|
232
398
|
}
|
|
233
399
|
async getUser(userId) {
|
|
234
|
-
|
|
400
|
+
if (this.useDatabase()) {
|
|
401
|
+
try {
|
|
402
|
+
const result = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
|
|
403
|
+
if (result.rows.length === 0)
|
|
404
|
+
return null;
|
|
405
|
+
return this.rowToUser(result.rows[0]);
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
Logger.error('Get user error:', error);
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
await this.ensureJsonInitialized();
|
|
235
413
|
return this.users.find(u => u.id === userId) || null;
|
|
236
414
|
}
|
|
237
415
|
async updateUserTier(userId, tier) {
|
|
238
|
-
|
|
416
|
+
if (this.useDatabase()) {
|
|
417
|
+
try {
|
|
418
|
+
const result = await db.query('UPDATE users SET tier = $1 WHERE id = $2', [tier, userId]);
|
|
419
|
+
return (result.rowCount ?? 0) > 0;
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
Logger.error('Update tier error:', error);
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
await this.ensureJsonInitialized();
|
|
239
427
|
const user = this.users.find(u => u.id === userId);
|
|
240
428
|
if (!user)
|
|
241
429
|
return false;
|
|
@@ -244,12 +432,59 @@ export class AuthService {
|
|
|
244
432
|
return true;
|
|
245
433
|
}
|
|
246
434
|
async checkAndUpdateUsage(userId, type) {
|
|
247
|
-
|
|
435
|
+
if (this.useDatabase()) {
|
|
436
|
+
try {
|
|
437
|
+
const result = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
|
|
438
|
+
if (result.rows.length === 0)
|
|
439
|
+
return { allowed: false, remaining: 0, limit: 0 };
|
|
440
|
+
const row = result.rows[0];
|
|
441
|
+
const today = new Date().toISOString().split('T')[0];
|
|
442
|
+
// Reset usage if new day
|
|
443
|
+
if (row.usage_reset_date !== today) {
|
|
444
|
+
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]);
|
|
445
|
+
row.requests_today = 0;
|
|
446
|
+
row.full_analysis_today = 0;
|
|
447
|
+
row.projects_today = 0;
|
|
448
|
+
}
|
|
449
|
+
const limits = TIER_LIMITS[row.tier];
|
|
450
|
+
let current;
|
|
451
|
+
let limit;
|
|
452
|
+
let column;
|
|
453
|
+
switch (type) {
|
|
454
|
+
case 'request':
|
|
455
|
+
current = row.requests_today;
|
|
456
|
+
limit = limits.requestsPerDay;
|
|
457
|
+
column = 'requests_today';
|
|
458
|
+
break;
|
|
459
|
+
case 'analysis':
|
|
460
|
+
current = row.full_analysis_today;
|
|
461
|
+
limit = limits.fullAnalysisPerDay;
|
|
462
|
+
column = 'full_analysis_today';
|
|
463
|
+
break;
|
|
464
|
+
case 'project':
|
|
465
|
+
current = row.projects_today;
|
|
466
|
+
limit = limits.projectsPerDay;
|
|
467
|
+
column = 'projects_today';
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
if (current >= limit && limit !== Infinity) {
|
|
471
|
+
return { allowed: false, remaining: 0, limit };
|
|
472
|
+
}
|
|
473
|
+
await db.query(`UPDATE users SET ${column} = ${column} + 1 WHERE id = $1`, [userId]);
|
|
474
|
+
const remaining = limit === Infinity ? Infinity : limit - current - 1;
|
|
475
|
+
return { allowed: true, remaining, limit };
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
Logger.error('Usage check error:', error);
|
|
479
|
+
return { allowed: false, remaining: 0, limit: 0 };
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
// JSON fallback
|
|
483
|
+
await this.ensureJsonInitialized();
|
|
248
484
|
const user = this.users.find(u => u.id === userId);
|
|
249
485
|
if (!user)
|
|
250
486
|
return { allowed: false, remaining: 0, limit: 0 };
|
|
251
487
|
const today = new Date().toISOString().split('T')[0];
|
|
252
|
-
// Reset usage if new day
|
|
253
488
|
if (user.usage.lastResetDate !== today) {
|
|
254
489
|
user.usage = this.createEmptyUsage();
|
|
255
490
|
}
|
|
@@ -273,7 +508,6 @@ export class AuthService {
|
|
|
273
508
|
if (current >= limit && limit !== Infinity) {
|
|
274
509
|
return { allowed: false, remaining: 0, limit };
|
|
275
510
|
}
|
|
276
|
-
// Increment usage
|
|
277
511
|
switch (type) {
|
|
278
512
|
case 'request':
|
|
279
513
|
user.usage.requestsToday++;
|
|
@@ -290,15 +524,38 @@ export class AuthService {
|
|
|
290
524
|
return { allowed: true, remaining, limit };
|
|
291
525
|
}
|
|
292
526
|
async getAllUsers() {
|
|
293
|
-
|
|
527
|
+
if (this.useDatabase()) {
|
|
528
|
+
try {
|
|
529
|
+
const result = await db.query('SELECT * FROM users ORDER BY created_at DESC');
|
|
530
|
+
return result.rows.map(row => this.sanitizeUser(this.rowToUser(row)));
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
Logger.error('Get all users error:', error);
|
|
534
|
+
return [];
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
await this.ensureJsonInitialized();
|
|
294
538
|
return this.users.map(u => this.sanitizeUser(u));
|
|
295
539
|
}
|
|
296
540
|
async deleteUser(userId) {
|
|
297
|
-
|
|
541
|
+
if (this.useDatabase()) {
|
|
542
|
+
try {
|
|
543
|
+
// Don't delete admin
|
|
544
|
+
const check = await db.query('SELECT tier FROM users WHERE id = $1', [userId]);
|
|
545
|
+
if (check.rows.length === 0 || check.rows[0].tier === 'admin')
|
|
546
|
+
return false;
|
|
547
|
+
await db.query('DELETE FROM users WHERE id = $1', [userId]);
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
Logger.error('Delete user error:', error);
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
await this.ensureJsonInitialized();
|
|
298
556
|
const index = this.users.findIndex(u => u.id === userId);
|
|
299
557
|
if (index === -1)
|
|
300
558
|
return false;
|
|
301
|
-
// Don't delete admin
|
|
302
559
|
if (this.users[index].tier === 'admin')
|
|
303
560
|
return false;
|
|
304
561
|
this.users.splice(index, 1);
|
|
@@ -308,7 +565,35 @@ export class AuthService {
|
|
|
308
565
|
return true;
|
|
309
566
|
}
|
|
310
567
|
async updateUser(userId, updates) {
|
|
311
|
-
|
|
568
|
+
if (this.useDatabase()) {
|
|
569
|
+
try {
|
|
570
|
+
const sets = [];
|
|
571
|
+
const values = [];
|
|
572
|
+
let paramIndex = 1;
|
|
573
|
+
if (updates.username) {
|
|
574
|
+
sets.push(`username = $${paramIndex++}`);
|
|
575
|
+
values.push(updates.username);
|
|
576
|
+
}
|
|
577
|
+
if (updates.email) {
|
|
578
|
+
sets.push(`email = $${paramIndex++}`);
|
|
579
|
+
values.push(updates.email);
|
|
580
|
+
}
|
|
581
|
+
if (updates.avatar) {
|
|
582
|
+
sets.push(`avatar = $${paramIndex++}`);
|
|
583
|
+
values.push(updates.avatar);
|
|
584
|
+
}
|
|
585
|
+
if (sets.length === 0)
|
|
586
|
+
return true;
|
|
587
|
+
values.push(userId);
|
|
588
|
+
const result = await db.query(`UPDATE users SET ${sets.join(', ')} WHERE id = $${paramIndex}`, values);
|
|
589
|
+
return (result.rowCount ?? 0) > 0;
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
Logger.error('Update user error:', error);
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
await this.ensureJsonInitialized();
|
|
312
597
|
const user = this.users.find(u => u.id === userId);
|
|
313
598
|
if (!user)
|
|
314
599
|
return false;
|
|
@@ -321,5 +606,13 @@ export class AuthService {
|
|
|
321
606
|
await this.saveUsers();
|
|
322
607
|
return true;
|
|
323
608
|
}
|
|
609
|
+
/**
|
|
610
|
+
* Initialize database and create default admin if needed
|
|
611
|
+
*/
|
|
612
|
+
async initDatabase() {
|
|
613
|
+
if (this.useDatabase()) {
|
|
614
|
+
await this.dbCreateDefaultAdmin();
|
|
615
|
+
}
|
|
616
|
+
}
|
|
324
617
|
}
|
|
325
618
|
//# 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
|