archicore 0.3.0 → 0.3.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 +2267 -374
- package/dist/cli/commands/interactive.js +83 -23
- package/dist/cli/commands/projects.js +3 -3
- package/dist/cli/ui/prompt.d.ts +4 -0
- package/dist/cli/ui/prompt.js +22 -0
- package/dist/cli/utils/config.js +2 -2
- package/dist/cli/utils/upload-utils.js +65 -18
- package/dist/code-index/ast-parser.d.ts +4 -0
- package/dist/code-index/ast-parser.js +42 -0
- package/dist/code-index/index.d.ts +21 -1
- package/dist/code-index/index.js +45 -1
- package/dist/code-index/source-map-extractor.d.ts +71 -0
- package/dist/code-index/source-map-extractor.js +194 -0
- package/dist/gitlab/gitlab-service.d.ts +162 -0
- package/dist/gitlab/gitlab-service.js +652 -0
- package/dist/gitlab/index.d.ts +8 -0
- package/dist/gitlab/index.js +8 -0
- package/dist/server/config/passport.d.ts +14 -0
- package/dist/server/config/passport.js +86 -0
- package/dist/server/index.js +52 -10
- package/dist/server/middleware/api-auth.d.ts +2 -2
- package/dist/server/middleware/api-auth.js +21 -2
- package/dist/server/middleware/csrf.d.ts +23 -0
- package/dist/server/middleware/csrf.js +96 -0
- package/dist/server/routes/auth.d.ts +2 -2
- package/dist/server/routes/auth.js +204 -5
- package/dist/server/routes/device-auth.js +2 -2
- package/dist/server/routes/gitlab.d.ts +12 -0
- package/dist/server/routes/gitlab.js +528 -0
- package/dist/server/routes/oauth.d.ts +6 -0
- package/dist/server/routes/oauth.js +198 -0
- package/dist/server/services/audit-service.d.ts +1 -1
- package/dist/server/services/auth-service.d.ts +13 -1
- package/dist/server/services/auth-service.js +108 -7
- package/dist/server/services/email-service.d.ts +63 -0
- package/dist/server/services/email-service.js +586 -0
- package/dist/server/utils/disposable-email-domains.d.ts +14 -0
- package/dist/server/utils/disposable-email-domains.js +192 -0
- package/dist/types/api.d.ts +98 -0
- package/dist/types/gitlab.d.ts +245 -0
- package/dist/types/gitlab.js +11 -0
- package/package.json +12 -4
|
@@ -5,16 +5,94 @@ import { Router } from 'express';
|
|
|
5
5
|
import { AuthService } from '../services/auth-service.js';
|
|
6
6
|
import { auditService } from '../services/audit-service.js';
|
|
7
7
|
import { Logger } from '../../utils/logger.js';
|
|
8
|
+
import { emailService } from '../services/email-service.js';
|
|
9
|
+
import { isDisposableEmail, getDisposableEmailError } from '../utils/disposable-email-domains.js';
|
|
10
|
+
import { cache } from '../services/cache.js';
|
|
8
11
|
export const authRouter = Router();
|
|
9
12
|
const authService = AuthService.getInstance();
|
|
10
|
-
//
|
|
13
|
+
// Cookie configuration for auth tokens
|
|
14
|
+
// Note: sameSite 'lax' allows cookies on top-level navigations (redirects from OAuth, links)
|
|
15
|
+
// while still protecting against CSRF on POST requests
|
|
16
|
+
const COOKIE_OPTIONS = {
|
|
17
|
+
httpOnly: true,
|
|
18
|
+
secure: process.env.NODE_ENV === 'production',
|
|
19
|
+
sameSite: 'lax',
|
|
20
|
+
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
21
|
+
path: '/'
|
|
22
|
+
};
|
|
23
|
+
// Helper to set auth cookie
|
|
24
|
+
function setAuthCookie(res, token) {
|
|
25
|
+
res.cookie('archicore_token', token, COOKIE_OPTIONS);
|
|
26
|
+
}
|
|
27
|
+
// Helper to clear auth cookie
|
|
28
|
+
function clearAuthCookie(res) {
|
|
29
|
+
res.clearCookie('archicore_token', { path: '/' });
|
|
30
|
+
}
|
|
31
|
+
// ========== Email Verification Code Storage (Redis with fallback) ==========
|
|
32
|
+
const VERIFICATION_PREFIX = 'verify:';
|
|
33
|
+
const VERIFICATION_TTL = 10 * 60; // 10 minutes in seconds
|
|
34
|
+
// Fallback in-memory storage when Redis is unavailable
|
|
35
|
+
const verificationCodes = new Map();
|
|
36
|
+
// Get verification code from Redis or fallback
|
|
37
|
+
async function getVerificationCode(email) {
|
|
38
|
+
const key = `${VERIFICATION_PREFIX}${email}`;
|
|
39
|
+
// Try Redis first
|
|
40
|
+
const cached = await cache.get(key);
|
|
41
|
+
if (cached)
|
|
42
|
+
return cached;
|
|
43
|
+
// Fallback to memory
|
|
44
|
+
const memCached = verificationCodes.get(email);
|
|
45
|
+
if (memCached && memCached.expiresAt > Date.now()) {
|
|
46
|
+
return memCached;
|
|
47
|
+
}
|
|
48
|
+
// Cleanup expired
|
|
49
|
+
if (memCached)
|
|
50
|
+
verificationCodes.delete(email);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
// Set verification code in Redis or fallback
|
|
54
|
+
async function setVerificationCode(email, data) {
|
|
55
|
+
const key = `${VERIFICATION_PREFIX}${email}`;
|
|
56
|
+
// Store in Redis with TTL
|
|
57
|
+
await cache.set(key, data, { ttl: VERIFICATION_TTL });
|
|
58
|
+
// Also store in memory as fallback
|
|
59
|
+
verificationCodes.set(email, data);
|
|
60
|
+
}
|
|
61
|
+
// Delete verification code
|
|
62
|
+
async function deleteVerificationCode(email) {
|
|
63
|
+
const key = `${VERIFICATION_PREFIX}${email}`;
|
|
64
|
+
await cache.del(key);
|
|
65
|
+
verificationCodes.delete(email);
|
|
66
|
+
}
|
|
67
|
+
// Generate 6-digit verification code
|
|
68
|
+
function generateVerificationCode() {
|
|
69
|
+
return Math.floor(100000 + Math.random() * 900000).toString();
|
|
70
|
+
}
|
|
71
|
+
// Cleanup expired memory codes
|
|
72
|
+
setInterval(() => {
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
for (const [email, data] of verificationCodes.entries()) {
|
|
75
|
+
if (data.expiresAt < now) {
|
|
76
|
+
verificationCodes.delete(email);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}, 60 * 1000); // Clean up every minute
|
|
80
|
+
// Middleware to check authentication (supports both Bearer token and httpOnly cookie)
|
|
11
81
|
export async function authMiddleware(req, res, next) {
|
|
12
82
|
const authHeader = req.headers.authorization;
|
|
13
|
-
|
|
83
|
+
const cookieToken = req.cookies?.archicore_token;
|
|
84
|
+
// Try Bearer token first, then cookie
|
|
85
|
+
let token = null;
|
|
86
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
87
|
+
token = authHeader.substring(7);
|
|
88
|
+
}
|
|
89
|
+
else if (cookieToken) {
|
|
90
|
+
token = cookieToken;
|
|
91
|
+
}
|
|
92
|
+
if (!token) {
|
|
14
93
|
res.status(401).json({ error: 'No token provided' });
|
|
15
94
|
return;
|
|
16
95
|
}
|
|
17
|
-
const token = authHeader.substring(7);
|
|
18
96
|
const user = await authService.validateToken(token);
|
|
19
97
|
if (!user) {
|
|
20
98
|
res.status(401).json({ error: 'Invalid or expired token' });
|
|
@@ -31,9 +109,96 @@ export async function adminMiddleware(req, res, next) {
|
|
|
31
109
|
}
|
|
32
110
|
next();
|
|
33
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* POST /api/auth/send-verification-code
|
|
114
|
+
* Send email verification code
|
|
115
|
+
*/
|
|
116
|
+
authRouter.post('/send-verification-code', async (req, res) => {
|
|
117
|
+
try {
|
|
118
|
+
const { email } = req.body;
|
|
119
|
+
if (!email || typeof email !== 'string') {
|
|
120
|
+
res.status(400).json({ success: false, error: 'Valid email is required' });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Validate email format
|
|
124
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
125
|
+
if (!emailRegex.test(email)) {
|
|
126
|
+
res.status(400).json({ success: false, error: 'Invalid email format' });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Check for disposable/temporary email
|
|
130
|
+
if (isDisposableEmail(email)) {
|
|
131
|
+
res.status(400).json({ success: false, error: getDisposableEmailError() });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// Check if email already registered
|
|
135
|
+
// This prevents enumeration but allows re-verification
|
|
136
|
+
const normalizedEmail = email.toLowerCase().trim();
|
|
137
|
+
// Generate verification code
|
|
138
|
+
const code = generateVerificationCode();
|
|
139
|
+
const expiresAt = Date.now() + 10 * 60 * 1000; // 10 minutes
|
|
140
|
+
// Store code in Redis (with memory fallback)
|
|
141
|
+
await setVerificationCode(normalizedEmail, {
|
|
142
|
+
email: normalizedEmail,
|
|
143
|
+
code,
|
|
144
|
+
expiresAt,
|
|
145
|
+
verified: false
|
|
146
|
+
});
|
|
147
|
+
// Send email
|
|
148
|
+
const emailSent = await emailService.sendVerificationCode(normalizedEmail, code);
|
|
149
|
+
if (!emailSent) {
|
|
150
|
+
Logger.warn(`[Auth] Failed to send verification code to ${normalizedEmail}`);
|
|
151
|
+
res.status(500).json({ success: false, error: 'Failed to send verification code' });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
Logger.info(`[Auth] Verification code sent to ${normalizedEmail}`);
|
|
155
|
+
res.json({ success: true, message: 'Verification code sent to your email' });
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
Logger.error('[Auth] Send verification code error:', error);
|
|
159
|
+
res.status(500).json({ success: false, error: 'Failed to send verification code' });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
/**
|
|
163
|
+
* POST /api/auth/verify-email
|
|
164
|
+
* Verify email with code
|
|
165
|
+
*/
|
|
166
|
+
authRouter.post('/verify-email', async (req, res) => {
|
|
167
|
+
try {
|
|
168
|
+
const { email, code } = req.body;
|
|
169
|
+
if (!email || !code) {
|
|
170
|
+
res.status(400).json({ success: false, error: 'Email and code are required' });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const normalizedEmail = email.toLowerCase().trim();
|
|
174
|
+
const storedData = await getVerificationCode(normalizedEmail);
|
|
175
|
+
if (!storedData) {
|
|
176
|
+
res.status(400).json({ success: false, error: 'No verification code found. Please request a new one.' });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (storedData.expiresAt < Date.now()) {
|
|
180
|
+
await deleteVerificationCode(normalizedEmail);
|
|
181
|
+
res.status(400).json({ success: false, error: 'Verification code expired. Please request a new one.' });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (storedData.code !== code.trim()) {
|
|
185
|
+
res.status(400).json({ success: false, error: 'Invalid verification code' });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// Mark as verified and update in Redis
|
|
189
|
+
storedData.verified = true;
|
|
190
|
+
await setVerificationCode(normalizedEmail, storedData);
|
|
191
|
+
Logger.info(`[Auth] Email verified: ${normalizedEmail}`);
|
|
192
|
+
res.json({ success: true, message: 'Email verified successfully' });
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
Logger.error('[Auth] Verify email error:', error);
|
|
196
|
+
res.status(500).json({ success: false, error: 'Failed to verify email' });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
34
199
|
/**
|
|
35
200
|
* POST /api/auth/register
|
|
36
|
-
* Register new user
|
|
201
|
+
* Register new user (requires verified email)
|
|
37
202
|
*/
|
|
38
203
|
authRouter.post('/register', async (req, res) => {
|
|
39
204
|
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
@@ -44,11 +209,32 @@ authRouter.post('/register', async (req, res) => {
|
|
|
44
209
|
res.status(400).json({ success: false, error: 'Email, username, and password are required' });
|
|
45
210
|
return;
|
|
46
211
|
}
|
|
212
|
+
// Check email verification - MANDATORY
|
|
213
|
+
const normalizedEmail = email.toLowerCase().trim();
|
|
214
|
+
const verification = await getVerificationCode(normalizedEmail);
|
|
215
|
+
if (!verification) {
|
|
216
|
+
res.status(400).json({ success: false, error: 'Please verify your email first. Request a verification code to continue.' });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (!verification.verified) {
|
|
220
|
+
res.status(400).json({ success: false, error: 'Email not verified. Please enter the verification code sent to your email.' });
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (verification.expiresAt < Date.now()) {
|
|
224
|
+
await deleteVerificationCode(normalizedEmail);
|
|
225
|
+
res.status(400).json({ success: false, error: 'Verification expired. Please request a new verification code.' });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
47
228
|
if (password.length < 8) {
|
|
48
229
|
res.status(400).json({ success: false, error: 'Password must be at least 8 characters' });
|
|
49
230
|
return;
|
|
50
231
|
}
|
|
51
232
|
const result = await authService.register(email, username, password);
|
|
233
|
+
// Clear verification code after successful registration
|
|
234
|
+
if (result.success) {
|
|
235
|
+
await deleteVerificationCode(normalizedEmail);
|
|
236
|
+
Logger.info(`[Auth] Verification code cleared for ${normalizedEmail} after successful registration`);
|
|
237
|
+
}
|
|
52
238
|
// Audit log
|
|
53
239
|
if (result.success && result.user) {
|
|
54
240
|
await auditService.log({
|
|
@@ -60,6 +246,10 @@ authRouter.post('/register', async (req, res) => {
|
|
|
60
246
|
details: { email }
|
|
61
247
|
});
|
|
62
248
|
}
|
|
249
|
+
// Set httpOnly cookie if registration successful
|
|
250
|
+
if (result.success && result.token) {
|
|
251
|
+
setAuthCookie(res, result.token);
|
|
252
|
+
}
|
|
63
253
|
res.json(result);
|
|
64
254
|
}
|
|
65
255
|
catch (error) {
|
|
@@ -92,6 +282,10 @@ authRouter.post('/login', async (req, res) => {
|
|
|
92
282
|
success: result.success,
|
|
93
283
|
errorMessage: result.success ? undefined : result.error
|
|
94
284
|
});
|
|
285
|
+
// Set httpOnly cookie if login successful
|
|
286
|
+
if (result.success && result.token) {
|
|
287
|
+
setAuthCookie(res, result.token);
|
|
288
|
+
}
|
|
95
289
|
res.json(result);
|
|
96
290
|
}
|
|
97
291
|
catch (error) {
|
|
@@ -116,7 +310,10 @@ authRouter.post('/logout', authMiddleware, async (req, res) => {
|
|
|
116
310
|
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
117
311
|
const userAgent = req.headers['user-agent'];
|
|
118
312
|
try {
|
|
119
|
-
|
|
313
|
+
// Get token from header or cookie
|
|
314
|
+
const headerToken = req.headers.authorization?.substring(7);
|
|
315
|
+
const cookieToken = req.cookies?.archicore_token;
|
|
316
|
+
const token = headerToken || cookieToken;
|
|
120
317
|
if (token) {
|
|
121
318
|
await authService.logout(token);
|
|
122
319
|
}
|
|
@@ -128,6 +325,8 @@ authRouter.post('/logout', authMiddleware, async (req, res) => {
|
|
|
128
325
|
ip,
|
|
129
326
|
userAgent
|
|
130
327
|
});
|
|
328
|
+
// Clear the auth cookie
|
|
329
|
+
clearAuthCookie(res);
|
|
131
330
|
res.json({ success: true });
|
|
132
331
|
}
|
|
133
332
|
catch (error) {
|
|
@@ -58,8 +58,8 @@ router.post('/code', (_req, res) => {
|
|
|
58
58
|
if (!serverUrl) {
|
|
59
59
|
const host = process.env.HOST || 'localhost';
|
|
60
60
|
const port = process.env.PORT || 3000;
|
|
61
|
-
// If HOST is 0.0.0.0 (bind to all interfaces), use
|
|
62
|
-
const effectiveHost = host === '0.0.0.0' ?
|
|
61
|
+
// If HOST is 0.0.0.0 (bind to all interfaces), use localhost for fallback
|
|
62
|
+
const effectiveHost = host === '0.0.0.0' ? 'localhost' : host;
|
|
63
63
|
serverUrl = `http://${effectiveHost}:${port}`;
|
|
64
64
|
}
|
|
65
65
|
res.json({
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitLab Integration Routes for ArchiCore
|
|
3
|
+
*
|
|
4
|
+
* Provides API endpoints for:
|
|
5
|
+
* - Managing GitLab instances (add, remove, list)
|
|
6
|
+
* - Listing and connecting repositories
|
|
7
|
+
* - Branch selection
|
|
8
|
+
* - Webhooks for auto-analysis
|
|
9
|
+
*/
|
|
10
|
+
export declare const gitlabRouter: import("express-serve-static-core").Router;
|
|
11
|
+
export default gitlabRouter;
|
|
12
|
+
//# sourceMappingURL=gitlab.d.ts.map
|