archicore 0.1.0
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 +530 -0
- package/dist/analyzers/dead-code.d.ts +95 -0
- package/dist/analyzers/dead-code.js +327 -0
- package/dist/analyzers/duplication.d.ts +90 -0
- package/dist/analyzers/duplication.js +344 -0
- package/dist/analyzers/security.d.ts +79 -0
- package/dist/analyzers/security.js +484 -0
- package/dist/architecture/index.d.ts +35 -0
- package/dist/architecture/index.js +249 -0
- package/dist/cli/commands/analyzers.d.ts +6 -0
- package/dist/cli/commands/analyzers.js +431 -0
- package/dist/cli/commands/export.d.ts +6 -0
- package/dist/cli/commands/export.js +78 -0
- package/dist/cli/commands/index.d.ts +8 -0
- package/dist/cli/commands/index.js +8 -0
- package/dist/cli/commands/init.d.ts +26 -0
- package/dist/cli/commands/init.js +140 -0
- package/dist/cli/commands/interactive.d.ts +7 -0
- package/dist/cli/commands/interactive.js +522 -0
- package/dist/cli/commands/projects.d.ts +6 -0
- package/dist/cli/commands/projects.js +249 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +7 -0
- package/dist/cli/ui/box.d.ts +17 -0
- package/dist/cli/ui/box.js +62 -0
- package/dist/cli/ui/colors.d.ts +49 -0
- package/dist/cli/ui/colors.js +86 -0
- package/dist/cli/ui/index.d.ts +9 -0
- package/dist/cli/ui/index.js +9 -0
- package/dist/cli/ui/prompt.d.ts +34 -0
- package/dist/cli/ui/prompt.js +122 -0
- package/dist/cli/ui/spinner.d.ts +29 -0
- package/dist/cli/ui/spinner.js +80 -0
- package/dist/cli/ui/table.d.ts +33 -0
- package/dist/cli/ui/table.js +84 -0
- package/dist/cli/utils/config.d.ts +23 -0
- package/dist/cli/utils/config.js +73 -0
- package/dist/cli/utils/index.d.ts +6 -0
- package/dist/cli/utils/index.js +6 -0
- package/dist/cli/utils/session.d.ts +27 -0
- package/dist/cli/utils/session.js +117 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +295 -0
- package/dist/code-index/ast-parser.d.ts +16 -0
- package/dist/code-index/ast-parser.js +330 -0
- package/dist/code-index/dependency-graph.d.ts +16 -0
- package/dist/code-index/dependency-graph.js +161 -0
- package/dist/code-index/index.d.ts +44 -0
- package/dist/code-index/index.js +124 -0
- package/dist/code-index/symbol-extractor.d.ts +13 -0
- package/dist/code-index/symbol-extractor.js +150 -0
- package/dist/export/index.d.ts +92 -0
- package/dist/export/index.js +676 -0
- package/dist/github/github-service.d.ts +146 -0
- package/dist/github/github-service.js +609 -0
- package/dist/impact-engine/index.d.ts +25 -0
- package/dist/impact-engine/index.js +284 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +149 -0
- package/dist/metrics/index.d.ts +136 -0
- package/dist/metrics/index.js +525 -0
- package/dist/orchestrator/deepseek-optimizer.d.ts +67 -0
- package/dist/orchestrator/deepseek-optimizer.js +320 -0
- package/dist/orchestrator/index.d.ts +34 -0
- package/dist/orchestrator/index.js +305 -0
- package/dist/pr-guardian/index.d.ts +143 -0
- package/dist/pr-guardian/index.js +553 -0
- package/dist/refactoring/index.d.ts +108 -0
- package/dist/refactoring/index.js +580 -0
- package/dist/rules-engine/index.d.ts +129 -0
- package/dist/rules-engine/index.js +482 -0
- package/dist/semantic-memory/embedding-service.d.ts +24 -0
- package/dist/semantic-memory/embedding-service.js +120 -0
- package/dist/semantic-memory/index.d.ts +45 -0
- package/dist/semantic-memory/index.js +206 -0
- package/dist/semantic-memory/vector-store.d.ts +27 -0
- package/dist/semantic-memory/vector-store.js +166 -0
- package/dist/server/index.d.ts +28 -0
- package/dist/server/index.js +141 -0
- package/dist/server/middleware/api-auth.d.ts +43 -0
- package/dist/server/middleware/api-auth.js +256 -0
- package/dist/server/routes/admin.d.ts +5 -0
- package/dist/server/routes/admin.js +123 -0
- package/dist/server/routes/api.d.ts +7 -0
- package/dist/server/routes/api.js +362 -0
- package/dist/server/routes/auth.d.ts +16 -0
- package/dist/server/routes/auth.js +191 -0
- package/dist/server/routes/developer.d.ts +8 -0
- package/dist/server/routes/developer.js +439 -0
- package/dist/server/routes/github.d.ts +7 -0
- package/dist/server/routes/github.js +495 -0
- package/dist/server/routes/upload.d.ts +7 -0
- package/dist/server/routes/upload.js +196 -0
- package/dist/server/services/api-key-service.d.ts +81 -0
- package/dist/server/services/api-key-service.js +281 -0
- package/dist/server/services/auth-service.d.ts +40 -0
- package/dist/server/services/auth-service.js +315 -0
- package/dist/server/services/project-service.d.ts +123 -0
- package/dist/server/services/project-service.js +533 -0
- package/dist/server/services/token-service.d.ts +107 -0
- package/dist/server/services/token-service.js +416 -0
- package/dist/server/services/upload-service.d.ts +93 -0
- package/dist/server/services/upload-service.js +464 -0
- package/dist/types/api.d.ts +188 -0
- package/dist/types/api.js +86 -0
- package/dist/types/github.d.ts +335 -0
- package/dist/types/github.js +5 -0
- package/dist/types/index.d.ts +265 -0
- package/dist/types/index.js +32 -0
- package/dist/types/user.d.ts +69 -0
- package/dist/types/user.js +42 -0
- package/dist/utils/file-utils.d.ts +20 -0
- package/dist/utils/file-utils.js +163 -0
- package/dist/utils/logger.d.ts +17 -0
- package/dist/utils/logger.js +41 -0
- package/dist/watcher/index.d.ts +125 -0
- package/dist/watcher/index.js +397 -0
- package/package.json +71 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ArchiCore Web Server
|
|
3
|
+
*
|
|
4
|
+
* HTTP API для Architecture Digital Twin:
|
|
5
|
+
* - Индексация проектов
|
|
6
|
+
* - Визуализация архитектуры
|
|
7
|
+
* - Анализ влияния изменений
|
|
8
|
+
* - Симуляция изменений
|
|
9
|
+
*/
|
|
10
|
+
import 'dotenv/config';
|
|
11
|
+
import express from 'express';
|
|
12
|
+
import cors from 'cors';
|
|
13
|
+
import { createServer } from 'http';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
import { Logger } from '../utils/logger.js';
|
|
17
|
+
import { apiRouter } from './routes/api.js';
|
|
18
|
+
import { uploadRouter } from './routes/upload.js';
|
|
19
|
+
import { authRouter } from './routes/auth.js';
|
|
20
|
+
import { adminRouter } from './routes/admin.js';
|
|
21
|
+
import { developerRouter } from './routes/developer.js';
|
|
22
|
+
import { githubRouter } from './routes/github.js';
|
|
23
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
+
const __dirname = path.dirname(__filename);
|
|
25
|
+
export class ArchiCoreServer {
|
|
26
|
+
app;
|
|
27
|
+
server = null;
|
|
28
|
+
config;
|
|
29
|
+
constructor(config) {
|
|
30
|
+
this.config = config;
|
|
31
|
+
this.app = express();
|
|
32
|
+
this.setupMiddleware();
|
|
33
|
+
this.setupRoutes();
|
|
34
|
+
this.setupErrorHandling();
|
|
35
|
+
}
|
|
36
|
+
setupMiddleware() {
|
|
37
|
+
// CORS для фронтенда
|
|
38
|
+
this.app.use(cors({
|
|
39
|
+
origin: '*',
|
|
40
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
41
|
+
allowedHeaders: ['Content-Type', 'Authorization']
|
|
42
|
+
}));
|
|
43
|
+
// JSON парсинг
|
|
44
|
+
this.app.use(express.json({ limit: '50mb' }));
|
|
45
|
+
this.app.use(express.urlencoded({ extended: true }));
|
|
46
|
+
// Статические файлы (фронтенд)
|
|
47
|
+
const publicPath = path.join(__dirname, '../../public');
|
|
48
|
+
this.app.use(express.static(publicPath));
|
|
49
|
+
// Логирование запросов
|
|
50
|
+
this.app.use((req, _res, next) => {
|
|
51
|
+
Logger.debug(`${req.method} ${req.path}`);
|
|
52
|
+
next();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
setupRoutes() {
|
|
56
|
+
// Auth routes
|
|
57
|
+
this.app.use('/api/auth', authRouter);
|
|
58
|
+
// Admin routes
|
|
59
|
+
this.app.use('/api/admin', adminRouter);
|
|
60
|
+
// Developer API routes (for API key management and public API)
|
|
61
|
+
this.app.use('/api/developer', developerRouter);
|
|
62
|
+
// GitHub integration routes
|
|
63
|
+
this.app.use('/api/github', githubRouter);
|
|
64
|
+
// API маршруты
|
|
65
|
+
this.app.use('/api', apiRouter);
|
|
66
|
+
// Upload маршруты
|
|
67
|
+
this.app.use('/api/upload', uploadRouter);
|
|
68
|
+
// Health check
|
|
69
|
+
this.app.get('/health', (_req, res) => {
|
|
70
|
+
res.json({
|
|
71
|
+
status: 'ok',
|
|
72
|
+
version: '0.1.0',
|
|
73
|
+
name: 'ArchiCore',
|
|
74
|
+
timestamp: new Date().toISOString()
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
// SPA fallback - все остальные маршруты отдают index.html
|
|
78
|
+
this.app.get('/*splat', (_req, res) => {
|
|
79
|
+
const indexPath = path.join(__dirname, '../../public/index.html');
|
|
80
|
+
res.sendFile(indexPath);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
setupErrorHandling() {
|
|
84
|
+
// 404 handler
|
|
85
|
+
this.app.use((_req, res) => {
|
|
86
|
+
res.status(404).json({ error: 'Not found' });
|
|
87
|
+
});
|
|
88
|
+
// Error handler
|
|
89
|
+
this.app.use((err, _req, res, _next) => {
|
|
90
|
+
Logger.error('Server error:', err);
|
|
91
|
+
res.status(500).json({
|
|
92
|
+
error: 'Internal server error',
|
|
93
|
+
message: err.message
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async start() {
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
this.server = createServer(this.app);
|
|
100
|
+
this.server.listen(this.config.port, this.config.host || '0.0.0.0', () => {
|
|
101
|
+
Logger.success(`ArchiCore server running at http://localhost:${this.config.port}`);
|
|
102
|
+
Logger.info(`API available at http://localhost:${this.config.port}/api`);
|
|
103
|
+
Logger.info(`Dashboard at http://localhost:${this.config.port}`);
|
|
104
|
+
resolve();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
async stop() {
|
|
109
|
+
return new Promise((resolve) => {
|
|
110
|
+
if (this.server) {
|
|
111
|
+
this.server.close(() => {
|
|
112
|
+
Logger.info('Server stopped');
|
|
113
|
+
resolve();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
resolve();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
getApp() {
|
|
122
|
+
return this.app;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Запуск сервера при прямом вызове
|
|
126
|
+
const isMainModule = process.argv[1]?.includes('server');
|
|
127
|
+
if (isMainModule) {
|
|
128
|
+
const port = parseInt(process.env.PORT || '3000', 10);
|
|
129
|
+
const server = new ArchiCoreServer({ port });
|
|
130
|
+
server.start().catch((err) => {
|
|
131
|
+
Logger.error('Failed to start server:', err);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
});
|
|
134
|
+
// Graceful shutdown
|
|
135
|
+
process.on('SIGINT', async () => {
|
|
136
|
+
Logger.info('Shutting down...');
|
|
137
|
+
await server.stop();
|
|
138
|
+
process.exit(0);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Authentication Middleware for ArchiCore Developer API
|
|
3
|
+
*
|
|
4
|
+
* Аутентификация через API ключи, rate limiting, проверка permissions
|
|
5
|
+
*/
|
|
6
|
+
import { Request, Response, NextFunction } from 'express';
|
|
7
|
+
import { ApiPermission, ApiRequestContext } from '../../types/api.js';
|
|
8
|
+
declare global {
|
|
9
|
+
namespace Express {
|
|
10
|
+
interface Request {
|
|
11
|
+
apiContext?: ApiRequestContext;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Middleware для аутентификации через API ключ
|
|
17
|
+
* Поддерживает:
|
|
18
|
+
* - Header: Authorization: Bearer arc_xxxxx
|
|
19
|
+
* - Header: X-API-Key: arc_xxxxx
|
|
20
|
+
* - Query: ?api_key=arc_xxxxx
|
|
21
|
+
*/
|
|
22
|
+
export declare function apiKeyAuth(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Middleware для проверки permission
|
|
25
|
+
*/
|
|
26
|
+
export declare function requirePermission(permission: ApiPermission): (req: Request, res: Response, next: NextFunction) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Middleware для проверки баланса перед операцией
|
|
29
|
+
*/
|
|
30
|
+
export declare function checkBalance(estimatedTokens: number): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Wrapper для автоматического трекинга использования токенов
|
|
33
|
+
*/
|
|
34
|
+
export declare function trackUsage(operation: ApiPermission extends `${infer Op}:${string}` ? Op : never): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Error handler для API ошибок
|
|
37
|
+
*/
|
|
38
|
+
export declare function apiErrorHandler(err: Error, _req: Request, res: Response, _next: NextFunction): void;
|
|
39
|
+
/**
|
|
40
|
+
* Optional API key auth (не требует ключ, но использует если есть)
|
|
41
|
+
*/
|
|
42
|
+
export declare function optionalApiKeyAuth(req: Request, _res: Response, next: NextFunction): Promise<void>;
|
|
43
|
+
//# sourceMappingURL=api-auth.d.ts.map
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Authentication Middleware for ArchiCore Developer API
|
|
3
|
+
*
|
|
4
|
+
* Аутентификация через API ключи, rate limiting, проверка permissions
|
|
5
|
+
*/
|
|
6
|
+
import { ApiKeyService } from '../services/api-key-service.js';
|
|
7
|
+
import { TokenService } from '../services/token-service.js';
|
|
8
|
+
import { Logger } from '../../utils/logger.js';
|
|
9
|
+
// Singleton instances
|
|
10
|
+
const apiKeyService = new ApiKeyService();
|
|
11
|
+
const tokenService = new TokenService();
|
|
12
|
+
/**
|
|
13
|
+
* Middleware для аутентификации через API ключ
|
|
14
|
+
* Поддерживает:
|
|
15
|
+
* - Header: Authorization: Bearer arc_xxxxx
|
|
16
|
+
* - Header: X-API-Key: arc_xxxxx
|
|
17
|
+
* - Query: ?api_key=arc_xxxxx
|
|
18
|
+
*/
|
|
19
|
+
export async function apiKeyAuth(req, res, next) {
|
|
20
|
+
try {
|
|
21
|
+
// Извлекаем API ключ из разных источников
|
|
22
|
+
let apiKey;
|
|
23
|
+
// 1. Authorization header (Bearer token)
|
|
24
|
+
const authHeader = req.headers.authorization;
|
|
25
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
26
|
+
apiKey = authHeader.substring(7);
|
|
27
|
+
}
|
|
28
|
+
// 2. X-API-Key header
|
|
29
|
+
if (!apiKey && req.headers['x-api-key']) {
|
|
30
|
+
apiKey = req.headers['x-api-key'];
|
|
31
|
+
}
|
|
32
|
+
// 3. Query parameter (не рекомендуется, но поддерживаем)
|
|
33
|
+
if (!apiKey && req.query.api_key) {
|
|
34
|
+
apiKey = req.query.api_key;
|
|
35
|
+
}
|
|
36
|
+
if (!apiKey) {
|
|
37
|
+
res.status(401).json({
|
|
38
|
+
error: {
|
|
39
|
+
type: 'authentication_error',
|
|
40
|
+
message: 'API key is required. Provide it via Authorization header, X-API-Key header, or api_key query parameter.'
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Валидация ключа
|
|
46
|
+
const validatedKey = await apiKeyService.validateKey(apiKey);
|
|
47
|
+
if (!validatedKey) {
|
|
48
|
+
res.status(401).json({
|
|
49
|
+
error: {
|
|
50
|
+
type: 'authentication_error',
|
|
51
|
+
message: 'Invalid or expired API key.'
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// Проверка rate limit
|
|
57
|
+
const rateLimitResult = await tokenService.checkRateLimit(validatedKey.id, validatedKey.rateLimit);
|
|
58
|
+
if (!rateLimitResult.allowed) {
|
|
59
|
+
res.status(429).json({
|
|
60
|
+
error: {
|
|
61
|
+
type: 'rate_limit_error',
|
|
62
|
+
message: 'Rate limit exceeded. Please retry later.',
|
|
63
|
+
retryAfter: rateLimitResult.retryAfter
|
|
64
|
+
},
|
|
65
|
+
limits: {
|
|
66
|
+
remaining: rateLimitResult.remaining,
|
|
67
|
+
resetAt: rateLimitResult.resetAt
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Получаем billing account
|
|
73
|
+
const billingAccount = await tokenService.getBillingAccount(validatedKey.userId);
|
|
74
|
+
// Добавляем контекст в request
|
|
75
|
+
req.apiContext = {
|
|
76
|
+
apiKey: validatedKey,
|
|
77
|
+
userId: validatedKey.userId,
|
|
78
|
+
rateLimitState: (await tokenService.getRateLimitState(validatedKey.id)),
|
|
79
|
+
billingAccount
|
|
80
|
+
};
|
|
81
|
+
// Добавляем rate limit headers
|
|
82
|
+
res.setHeader('X-RateLimit-Limit-Requests', validatedKey.rateLimit.requestsPerMinute.toString());
|
|
83
|
+
res.setHeader('X-RateLimit-Remaining-Requests', rateLimitResult.remaining.requestsPerMinute.toString());
|
|
84
|
+
res.setHeader('X-RateLimit-Reset', rateLimitResult.resetAt.minute.toISOString());
|
|
85
|
+
next();
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
Logger.error('API auth error:', error);
|
|
89
|
+
res.status(500).json({
|
|
90
|
+
error: {
|
|
91
|
+
type: 'server_error',
|
|
92
|
+
message: 'Authentication failed due to server error.'
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Middleware для проверки permission
|
|
99
|
+
*/
|
|
100
|
+
export function requirePermission(permission) {
|
|
101
|
+
return (req, res, next) => {
|
|
102
|
+
if (!req.apiContext) {
|
|
103
|
+
res.status(401).json({
|
|
104
|
+
error: {
|
|
105
|
+
type: 'authentication_error',
|
|
106
|
+
message: 'Not authenticated.'
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (!apiKeyService.hasPermission(req.apiContext.apiKey, permission)) {
|
|
112
|
+
res.status(403).json({
|
|
113
|
+
error: {
|
|
114
|
+
type: 'permission_error',
|
|
115
|
+
message: `Permission '${permission}' is required for this operation.`,
|
|
116
|
+
required: permission,
|
|
117
|
+
available: req.apiContext.apiKey.permissions
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
next();
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Middleware для проверки баланса перед операцией
|
|
127
|
+
*/
|
|
128
|
+
export function checkBalance(estimatedTokens) {
|
|
129
|
+
return async (req, res, next) => {
|
|
130
|
+
if (!req.apiContext) {
|
|
131
|
+
res.status(401).json({
|
|
132
|
+
error: {
|
|
133
|
+
type: 'authentication_error',
|
|
134
|
+
message: 'Not authenticated.'
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const balanceCheck = await tokenService.checkBalance(req.apiContext.userId, estimatedTokens);
|
|
140
|
+
if (!balanceCheck.allowed) {
|
|
141
|
+
res.status(402).json({
|
|
142
|
+
error: {
|
|
143
|
+
type: 'insufficient_funds',
|
|
144
|
+
message: 'Insufficient balance for this operation.',
|
|
145
|
+
balance: balanceCheck.balance / 100, // в долларах
|
|
146
|
+
estimatedCost: balanceCheck.estimatedCost / 100,
|
|
147
|
+
tokensRemaining: balanceCheck.tokensRemaining
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
next();
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Wrapper для автоматического трекинга использования токенов
|
|
157
|
+
*/
|
|
158
|
+
export function trackUsage(operation) {
|
|
159
|
+
return async (req, res, next) => {
|
|
160
|
+
if (!req.apiContext) {
|
|
161
|
+
next();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// Сохраняем оригинальный json метод
|
|
165
|
+
const originalJson = res.json.bind(res);
|
|
166
|
+
const startTime = Date.now();
|
|
167
|
+
// Перехватываем ответ
|
|
168
|
+
res.json = function (body) {
|
|
169
|
+
// Подсчитываем токены на основе размера ответа
|
|
170
|
+
const responseSize = JSON.stringify(body).length;
|
|
171
|
+
const inputSize = JSON.stringify(req.body || {}).length;
|
|
172
|
+
const tokens = tokenService.calculateTokens(operation, {
|
|
173
|
+
inputText: JSON.stringify(req.body || {}),
|
|
174
|
+
outputText: JSON.stringify(body),
|
|
175
|
+
contentSizeKb: Math.ceil((inputSize + responseSize) / 1024)
|
|
176
|
+
});
|
|
177
|
+
// Записываем использование асинхронно (не блокируем ответ)
|
|
178
|
+
tokenService.recordUsage(req.apiContext.apiKey.id, req.apiContext.userId, operation, tokens, req.params.id, // projectId if present
|
|
179
|
+
{
|
|
180
|
+
endpoint: req.path,
|
|
181
|
+
method: req.method,
|
|
182
|
+
duration: Date.now() - startTime
|
|
183
|
+
}).then(() => {
|
|
184
|
+
// Инкрементим rate limit
|
|
185
|
+
return tokenService.incrementRateLimit(req.apiContext.apiKey.id, tokens.totalTokens);
|
|
186
|
+
}).catch(err => {
|
|
187
|
+
Logger.error('Failed to record usage:', err);
|
|
188
|
+
});
|
|
189
|
+
// Добавляем usage info в ответ
|
|
190
|
+
if (body && typeof body === 'object' && !Array.isArray(body)) {
|
|
191
|
+
body.usage = {
|
|
192
|
+
inputTokens: tokens.inputTokens,
|
|
193
|
+
outputTokens: tokens.outputTokens,
|
|
194
|
+
totalTokens: tokens.totalTokens
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return originalJson(body);
|
|
198
|
+
};
|
|
199
|
+
next();
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Error handler для API ошибок
|
|
204
|
+
*/
|
|
205
|
+
export function apiErrorHandler(err, _req, res, _next) {
|
|
206
|
+
Logger.error('API Error:', err);
|
|
207
|
+
// Определяем тип ошибки
|
|
208
|
+
let statusCode = 500;
|
|
209
|
+
let errorType = 'server_error';
|
|
210
|
+
if (err.message.includes('not found')) {
|
|
211
|
+
statusCode = 404;
|
|
212
|
+
errorType = 'not_found';
|
|
213
|
+
}
|
|
214
|
+
else if (err.message.includes('permission') || err.message.includes('forbidden')) {
|
|
215
|
+
statusCode = 403;
|
|
216
|
+
errorType = 'permission_error';
|
|
217
|
+
}
|
|
218
|
+
else if (err.message.includes('invalid') || err.message.includes('validation')) {
|
|
219
|
+
statusCode = 400;
|
|
220
|
+
errorType = 'validation_error';
|
|
221
|
+
}
|
|
222
|
+
res.status(statusCode).json({
|
|
223
|
+
error: {
|
|
224
|
+
type: errorType,
|
|
225
|
+
message: err.message
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Optional API key auth (не требует ключ, но использует если есть)
|
|
231
|
+
*/
|
|
232
|
+
export async function optionalApiKeyAuth(req, _res, next) {
|
|
233
|
+
// Пытаемся аутентифицировать, но не отклоняем если нет ключа
|
|
234
|
+
let apiKey;
|
|
235
|
+
const authHeader = req.headers.authorization;
|
|
236
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
237
|
+
apiKey = authHeader.substring(7);
|
|
238
|
+
}
|
|
239
|
+
if (!apiKey && req.headers['x-api-key']) {
|
|
240
|
+
apiKey = req.headers['x-api-key'];
|
|
241
|
+
}
|
|
242
|
+
if (apiKey) {
|
|
243
|
+
const validatedKey = await apiKeyService.validateKey(apiKey);
|
|
244
|
+
if (validatedKey) {
|
|
245
|
+
const billingAccount = await tokenService.getBillingAccount(validatedKey.userId);
|
|
246
|
+
req.apiContext = {
|
|
247
|
+
apiKey: validatedKey,
|
|
248
|
+
userId: validatedKey.userId,
|
|
249
|
+
rateLimitState: (await tokenService.getRateLimitState(validatedKey.id)),
|
|
250
|
+
billingAccount
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
next();
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=api-auth.js.map
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin API Routes for ArchiCore
|
|
3
|
+
*/
|
|
4
|
+
import { Router } from 'express';
|
|
5
|
+
import { AuthService } from '../services/auth-service.js';
|
|
6
|
+
import { authMiddleware, adminMiddleware } from './auth.js';
|
|
7
|
+
import { Logger } from '../../utils/logger.js';
|
|
8
|
+
export const adminRouter = Router();
|
|
9
|
+
const authService = new AuthService();
|
|
10
|
+
// All admin routes require authentication and admin role
|
|
11
|
+
adminRouter.use(authMiddleware);
|
|
12
|
+
adminRouter.use(adminMiddleware);
|
|
13
|
+
/**
|
|
14
|
+
* GET /api/admin/users
|
|
15
|
+
* Get all users
|
|
16
|
+
*/
|
|
17
|
+
adminRouter.get('/users', async (_req, res) => {
|
|
18
|
+
try {
|
|
19
|
+
const users = await authService.getAllUsers();
|
|
20
|
+
res.json({ users });
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
Logger.error('Failed to get users:', error);
|
|
24
|
+
res.status(500).json({ error: 'Failed to get users' });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* GET /api/admin/users/:id
|
|
29
|
+
* Get specific user
|
|
30
|
+
*/
|
|
31
|
+
adminRouter.get('/users/:id', async (req, res) => {
|
|
32
|
+
try {
|
|
33
|
+
const { id } = req.params;
|
|
34
|
+
const user = await authService.getUser(id);
|
|
35
|
+
if (!user) {
|
|
36
|
+
res.status(404).json({ error: 'User not found' });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const { passwordHash, ...sanitized } = user;
|
|
40
|
+
res.json({ user: sanitized });
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
Logger.error('Failed to get user:', error);
|
|
44
|
+
res.status(500).json({ error: 'Failed to get user' });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* PUT /api/admin/users/:id/tier
|
|
49
|
+
* Update user's subscription tier
|
|
50
|
+
*/
|
|
51
|
+
adminRouter.put('/users/:id/tier', async (req, res) => {
|
|
52
|
+
try {
|
|
53
|
+
const { id } = req.params;
|
|
54
|
+
const { tier } = req.body;
|
|
55
|
+
const validTiers = ['free', 'mid', 'pro', 'admin'];
|
|
56
|
+
if (!tier || !validTiers.includes(tier)) {
|
|
57
|
+
res.status(400).json({ error: 'Invalid tier' });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const updated = await authService.updateUserTier(id, tier);
|
|
61
|
+
if (!updated) {
|
|
62
|
+
res.status(404).json({ error: 'User not found' });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
res.json({ success: true });
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
Logger.error('Failed to update user tier:', error);
|
|
69
|
+
res.status(500).json({ error: 'Failed to update tier' });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
/**
|
|
73
|
+
* DELETE /api/admin/users/:id
|
|
74
|
+
* Delete user
|
|
75
|
+
*/
|
|
76
|
+
adminRouter.delete('/users/:id', async (req, res) => {
|
|
77
|
+
try {
|
|
78
|
+
const { id } = req.params;
|
|
79
|
+
const deleted = await authService.deleteUser(id);
|
|
80
|
+
if (!deleted) {
|
|
81
|
+
res.status(400).json({ error: 'Cannot delete user (admin or not found)' });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
res.json({ success: true });
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
Logger.error('Failed to delete user:', error);
|
|
88
|
+
res.status(500).json({ error: 'Failed to delete user' });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
/**
|
|
92
|
+
* GET /api/admin/stats
|
|
93
|
+
* Get system statistics
|
|
94
|
+
*/
|
|
95
|
+
adminRouter.get('/stats', async (_req, res) => {
|
|
96
|
+
try {
|
|
97
|
+
const users = await authService.getAllUsers();
|
|
98
|
+
const stats = {
|
|
99
|
+
totalUsers: users.length,
|
|
100
|
+
byTier: {
|
|
101
|
+
free: users.filter(u => u.tier === 'free').length,
|
|
102
|
+
mid: users.filter(u => u.tier === 'mid').length,
|
|
103
|
+
pro: users.filter(u => u.tier === 'pro').length,
|
|
104
|
+
admin: users.filter(u => u.tier === 'admin').length
|
|
105
|
+
},
|
|
106
|
+
byProvider: {
|
|
107
|
+
email: users.filter(u => u.provider === 'email').length,
|
|
108
|
+
github: users.filter(u => u.provider === 'github').length,
|
|
109
|
+
google: users.filter(u => u.provider === 'google').length
|
|
110
|
+
},
|
|
111
|
+
recentUsers: users
|
|
112
|
+
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
|
113
|
+
.slice(0, 5)
|
|
114
|
+
.map(u => ({ id: u.id, username: u.username, tier: u.tier, createdAt: u.createdAt }))
|
|
115
|
+
};
|
|
116
|
+
res.json(stats);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
Logger.error('Failed to get stats:', error);
|
|
120
|
+
res.status(500).json({ error: 'Failed to get statistics' });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
//# sourceMappingURL=admin.js.map
|