9drive 1.0.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.
Files changed (104) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +389 -0
  3. package/backend/dist/app.js +113 -0
  4. package/backend/dist/config/env.js +75 -0
  5. package/backend/dist/config/prisma.js +30 -0
  6. package/backend/dist/middleware/api-key.middleware.js +38 -0
  7. package/backend/dist/middleware/auth.middleware.js +21 -0
  8. package/backend/dist/middleware/error.middleware.js +7 -0
  9. package/backend/dist/modules/api-keys/api-key.routes.js +90 -0
  10. package/backend/dist/modules/audit-logs/audit-log.routes.js +20 -0
  11. package/backend/dist/modules/auth/auth.routes.js +201 -0
  12. package/backend/dist/modules/cdn/cdn.routes.js +105 -0
  13. package/backend/dist/modules/connected-accounts/connected-account.routes.js +313 -0
  14. package/backend/dist/modules/files/file.routes.js +1013 -0
  15. package/backend/dist/modules/files/stream-file.js +10 -0
  16. package/backend/dist/modules/files/stream-google-file.js +79 -0
  17. package/backend/dist/modules/folders/folder.routes.js +546 -0
  18. package/backend/dist/modules/google/google.service.js +485 -0
  19. package/backend/dist/modules/invites/invite.routes.js +127 -0
  20. package/backend/dist/modules/provider-configs/provider-config.routes.js +53 -0
  21. package/backend/dist/modules/public/public.routes.js +44 -0
  22. package/backend/dist/modules/public-api/public-api.routes.js +8 -0
  23. package/backend/dist/modules/s3/s3.service.js +104 -0
  24. package/backend/dist/modules/storage/storage.routes.js +108 -0
  25. package/backend/dist/modules/system/system.routes.js +663 -0
  26. package/backend/dist/modules/uploads/upload.routes.js +569 -0
  27. package/backend/dist/scripts/debug-query.js +19 -0
  28. package/backend/dist/scripts/seed-google-config-if-present.js +49 -0
  29. package/backend/dist/scripts/seed-google-config.js +40 -0
  30. package/backend/dist/scripts/test-api-upload.js +37 -0
  31. package/backend/dist/scripts/test-external-upload.js +181 -0
  32. package/backend/dist/scripts/test-s3-connection.js +33 -0
  33. package/backend/dist/server.js +7 -0
  34. package/backend/dist/utils/audit.js +20 -0
  35. package/backend/dist/utils/crypto.js +33 -0
  36. package/backend/dist/utils/jwt.js +15 -0
  37. package/backend/dist/utils/password.js +14 -0
  38. package/backend/prisma/generated/sqlite-client/client.d.ts +1 -0
  39. package/backend/prisma/generated/sqlite-client/client.js +5 -0
  40. package/backend/prisma/generated/sqlite-client/default.d.ts +1 -0
  41. package/backend/prisma/generated/sqlite-client/default.js +5 -0
  42. package/backend/prisma/generated/sqlite-client/edge.d.ts +1 -0
  43. package/backend/prisma/generated/sqlite-client/edge.js +436 -0
  44. package/backend/prisma/generated/sqlite-client/index-browser.js +422 -0
  45. package/backend/prisma/generated/sqlite-client/index.d.ts +35178 -0
  46. package/backend/prisma/generated/sqlite-client/index.js +457 -0
  47. package/backend/prisma/generated/sqlite-client/package.json +183 -0
  48. package/backend/prisma/generated/sqlite-client/query_engine-windows.dll.node +0 -0
  49. package/backend/prisma/generated/sqlite-client/query_engine_bg.js +2 -0
  50. package/backend/prisma/generated/sqlite-client/query_engine_bg.wasm +0 -0
  51. package/backend/prisma/generated/sqlite-client/runtime/edge-esm.js +35 -0
  52. package/backend/prisma/generated/sqlite-client/runtime/edge.js +35 -0
  53. package/backend/prisma/generated/sqlite-client/runtime/index-browser.d.ts +370 -0
  54. package/backend/prisma/generated/sqlite-client/runtime/index-browser.js +17 -0
  55. package/backend/prisma/generated/sqlite-client/runtime/library.d.ts +3982 -0
  56. package/backend/prisma/generated/sqlite-client/runtime/library.js +147 -0
  57. package/backend/prisma/generated/sqlite-client/runtime/react-native.js +84 -0
  58. package/backend/prisma/generated/sqlite-client/runtime/wasm-compiler-edge.js +85 -0
  59. package/backend/prisma/generated/sqlite-client/runtime/wasm-engine-edge.js +38 -0
  60. package/backend/prisma/generated/sqlite-client/schema.prisma +354 -0
  61. package/backend/prisma/generated/sqlite-client/wasm-edge-light-loader.mjs +5 -0
  62. package/backend/prisma/generated/sqlite-client/wasm-worker-loader.mjs +5 -0
  63. package/backend/prisma/generated/sqlite-client/wasm.d.ts +1 -0
  64. package/backend/prisma/generated/sqlite-client/wasm.js +443 -0
  65. package/backend/prisma/migrations/20260604084325_init/migration.sql +191 -0
  66. package/backend/prisma/migrations/20260604090020_global_provider_config/migration.sql +8 -0
  67. package/backend/prisma/migrations/20260604091957_virtual_folders/migration.sql +32 -0
  68. package/backend/prisma/migrations/20260604095418_nested_folders/migration.sql +8 -0
  69. package/backend/prisma/migrations/20260604095705_file_shares/migration.sql +22 -0
  70. package/backend/prisma/migrations/20260604100802_file_preview_tokens/migration.sql +20 -0
  71. package/backend/prisma/migrations/20260604120000_add_file_share_token/migration.sql +3 -0
  72. package/backend/prisma/migrations/20260604130000_workspace_invites/migration.sql +17 -0
  73. package/backend/prisma/migrations/20260604133000_invite_targets/migration.sql +9 -0
  74. package/backend/prisma/migrations/20260605090000_query_indexes/migration.sql +12 -0
  75. package/backend/prisma/migrations/20260605100000_folder_icons/migration.sql +1 -0
  76. package/backend/prisma/migrations/20260605110000_google_auth_handoff/migration.sql +22 -0
  77. package/backend/prisma/migrations/20260611120000_s3_storage_support/migration.sql +31 -0
  78. package/backend/prisma/migrations/20260611130000_upload_routing_policy/migration.sql +14 -0
  79. package/backend/prisma/migrations/20260611140000_api_keys/migration.sql +21 -0
  80. package/backend/prisma/migrations/20260630030000_upload_session_resumable_fields/migration.sql +9 -0
  81. package/backend/prisma/migrations/migration_lock.toml +3 -0
  82. package/backend/prisma/schema.prisma +353 -0
  83. package/backend/prisma/schema.sqlite.prisma +354 -0
  84. package/backend/public/assets/index-B4ifIM_G.js +135 -0
  85. package/backend/public/assets/index-DwOPZrmt.css +2 -0
  86. package/backend/public/assets/ort-wasm-simd-threaded.jsep-D5Jk56-t.wasm +0 -0
  87. package/backend/public/assets/ort.bundle.min-CpXgll2e.js +2746 -0
  88. package/backend/public/assets/ort.bundle.min-OfoG_cy9.mjs +2817 -0
  89. package/backend/public/assets/ort.webgpu.bundle.min-C1vdBqT7.js +2746 -0
  90. package/backend/public/assets/ort.webgpu.bundle.min-CEayb2S6.mjs +2817 -0
  91. package/backend/public/assets/pdf.worker.min-Dswkl-cV.mjs +29 -0
  92. package/backend/public/favicon.ico +74 -0
  93. package/backend/public/favicon.svg +74 -0
  94. package/backend/public/icons.svg +24 -0
  95. package/backend/public/index.html +34 -0
  96. package/backend/public/manifest.webmanifest +1 -0
  97. package/backend/public/maskable-icon.svg +22 -0
  98. package/backend/public/pwa-192x192.svg +22 -0
  99. package/backend/public/pwa-512x512.svg +22 -0
  100. package/backend/public/registerSW.js +1 -0
  101. package/backend/public/sw.js +1 -0
  102. package/backend/public/workbox-9c191d2f.js +1 -0
  103. package/bin/9drive.js +320 -0
  104. package/package.json +60 -0
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.app = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const cors_1 = __importDefault(require("cors"));
10
+ const express_1 = __importDefault(require("express"));
11
+ const env_js_1 = require("./config/env.js");
12
+ const error_middleware_js_1 = require("./middleware/error.middleware.js");
13
+ const auth_routes_js_1 = require("./modules/auth/auth.routes.js");
14
+ const provider_config_routes_js_1 = require("./modules/provider-configs/provider-config.routes.js");
15
+ const connected_account_routes_js_1 = require("./modules/connected-accounts/connected-account.routes.js");
16
+ const storage_routes_js_1 = require("./modules/storage/storage.routes.js");
17
+ const upload_routes_js_1 = require("./modules/uploads/upload.routes.js");
18
+ const file_routes_js_1 = require("./modules/files/file.routes.js");
19
+ const folder_routes_js_1 = require("./modules/folders/folder.routes.js");
20
+ const public_routes_js_1 = require("./modules/public/public.routes.js");
21
+ const invite_routes_js_1 = require("./modules/invites/invite.routes.js");
22
+ const api_key_routes_js_1 = require("./modules/api-keys/api-key.routes.js");
23
+ const public_api_routes_js_1 = require("./modules/public-api/public-api.routes.js");
24
+ const cdn_routes_js_1 = require("./modules/cdn/cdn.routes.js");
25
+ const audit_log_routes_js_1 = require("./modules/audit-logs/audit-log.routes.js");
26
+ const system_routes_js_1 = require("./modules/system/system.routes.js");
27
+ const baseDir = typeof __dirname !== 'undefined' ? __dirname : process.cwd();
28
+ // Find frontend public build if available (for unified CLI or production hosting)
29
+ const possiblePublicDirs = [
30
+ path_1.default.resolve(baseDir, '../public'),
31
+ path_1.default.resolve(baseDir, '../../frontend/dist'),
32
+ path_1.default.resolve(process.cwd(), 'dist-frontend'),
33
+ path_1.default.resolve(process.cwd(), 'public'),
34
+ ];
35
+ const publicDir = possiblePublicDirs.find((dir) => fs_1.default.existsSync(path_1.default.join(dir, 'index.html')));
36
+ exports.app = (0, express_1.default)();
37
+ exports.app.set('trust proxy', true);
38
+ // Dynamic CORS: Allow open access for CDN delivery and public API routes, allow frontend origins
39
+ exports.app.use((req, res, next) => {
40
+ if (req.path.startsWith('/cdn') || req.path.startsWith('/api') || req.path.startsWith('/public')) {
41
+ return (0, cors_1.default)({ origin: '*' })(req, res, next);
42
+ }
43
+ const host = req.get('host');
44
+ return (0, cors_1.default)({
45
+ origin: (origin, callback) => {
46
+ if (!origin ||
47
+ origin === env_js_1.env.FRONTEND_URL ||
48
+ origin.startsWith('http://localhost:') ||
49
+ origin.startsWith('http://127.0.0.1:') ||
50
+ (host && (origin === `http://${host}` || origin === `https://${host}`))) {
51
+ return callback(null, true);
52
+ }
53
+ return callback(null, false);
54
+ },
55
+ credentials: true,
56
+ })(req, res, next);
57
+ });
58
+ exports.app.use(express_1.default.json({ limit: '1mb' }));
59
+ exports.app.get('/health', (_req, res) => res.json({ status: 'ok' }));
60
+ exports.app.use('/cdn', cdn_routes_js_1.cdnRouter);
61
+ exports.app.use('/api', public_api_routes_js_1.publicApiRouter);
62
+ exports.app.use('/public', public_routes_js_1.publicRouter);
63
+ // Core module routers (both root and /api prefixes supported)
64
+ exports.app.use('/auth', auth_routes_js_1.authRouter);
65
+ exports.app.use('/api/auth', auth_routes_js_1.authRouter);
66
+ exports.app.use('/api-keys', api_key_routes_js_1.apiKeyRouter);
67
+ exports.app.use('/api/api-keys', api_key_routes_js_1.apiKeyRouter);
68
+ exports.app.use('/provider-configs', provider_config_routes_js_1.providerConfigRouter);
69
+ exports.app.use('/api/provider-configs', provider_config_routes_js_1.providerConfigRouter);
70
+ exports.app.use('/connected-accounts', connected_account_routes_js_1.connectedAccountRouter);
71
+ exports.app.use('/api/connected-accounts', connected_account_routes_js_1.connectedAccountRouter);
72
+ exports.app.use('/storage', storage_routes_js_1.storageRouter);
73
+ exports.app.use('/api/storage', storage_routes_js_1.storageRouter);
74
+ exports.app.use('/uploads', upload_routes_js_1.uploadRouter);
75
+ exports.app.use('/api/uploads', upload_routes_js_1.uploadRouter);
76
+ exports.app.use('/files', file_routes_js_1.fileRouter);
77
+ exports.app.use('/api/files', file_routes_js_1.fileRouter);
78
+ exports.app.use('/folders', folder_routes_js_1.folderRouter);
79
+ exports.app.use('/api/folders', folder_routes_js_1.folderRouter);
80
+ exports.app.use('/invites', invite_routes_js_1.inviteRouter);
81
+ exports.app.use('/api/invites', invite_routes_js_1.inviteRouter);
82
+ exports.app.use('/audit-logs', audit_log_routes_js_1.auditLogRouter);
83
+ exports.app.use('/api/audit-logs', audit_log_routes_js_1.auditLogRouter);
84
+ exports.app.use('/system', system_routes_js_1.systemRouter);
85
+ exports.app.use('/api/system', system_routes_js_1.systemRouter);
86
+ // Serve frontend SPA if bundle is found
87
+ if (publicDir) {
88
+ exports.app.use(express_1.default.static(publicDir));
89
+ exports.app.use((req, res, next) => {
90
+ if (req.path.startsWith('/api') ||
91
+ req.path.startsWith('/cdn') ||
92
+ req.path.startsWith('/public') ||
93
+ req.path.startsWith('/auth') ||
94
+ req.path.startsWith('/api-keys') ||
95
+ req.path.startsWith('/provider-configs') ||
96
+ req.path.startsWith('/connected-accounts') ||
97
+ req.path.startsWith('/storage') ||
98
+ req.path.startsWith('/uploads') ||
99
+ req.path.startsWith('/files') ||
100
+ req.path.startsWith('/folders') ||
101
+ req.path.startsWith('/invites') ||
102
+ req.path.startsWith('/audit-logs') ||
103
+ req.path.startsWith('/system') ||
104
+ req.path.startsWith('/health')) {
105
+ return next();
106
+ }
107
+ if (req.method === 'GET') {
108
+ return res.sendFile(path_1.default.join(publicDir, 'index.html'));
109
+ }
110
+ next();
111
+ });
112
+ }
113
+ exports.app.use(error_middleware_js_1.errorMiddleware);
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.env = void 0;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const dotenv_1 = __importDefault(require("dotenv"));
12
+ const zod_1 = require("zod");
13
+ dotenv_1.default.config();
14
+ // Support ~/.9drive config directory for CLI & local-first zero-config mode
15
+ const defaultDataDir = path_1.default.join(os_1.default.homedir(), '.9drive');
16
+ const dataDir = process.env.NINEDRIVE_DATA_DIR || defaultDataDir;
17
+ const configPath = path_1.default.join(dataDir, 'config.json');
18
+ let fileConfig = {};
19
+ if (fs_1.default.existsSync(configPath)) {
20
+ try {
21
+ fileConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
22
+ }
23
+ catch {
24
+ console.warn(`[9Drive] Warning: Could not parse configuration from ${configPath}`);
25
+ }
26
+ }
27
+ // Generate persistent secrets if not provided
28
+ const defaultAccessSecret = process.env.JWT_ACCESS_SECRET ||
29
+ fileConfig.JWT_ACCESS_SECRET ||
30
+ crypto_1.default.randomBytes(32).toString('hex');
31
+ const defaultEncryptionKey = process.env.TOKEN_ENCRYPTION_KEY ||
32
+ fileConfig.TOKEN_ENCRYPTION_KEY ||
33
+ crypto_1.default.randomBytes(32).toString('hex');
34
+ const defaultPort = Number(process.env.APP_PORT || fileConfig.APP_PORT || 4000);
35
+ const defaultFrontendUrl = process.env.FRONTEND_URL || fileConfig.FRONTEND_URL || `http://localhost:${defaultPort}`;
36
+ // Standard database URL: if not given in env or config, fallback to local sqlite in ~/.9drive/9drive.db
37
+ const defaultDbPath = path_1.default.join(dataDir, '9drive.db').replace(/\\/g, '/');
38
+ const defaultDatabaseUrl = process.env.DATABASE_URL || fileConfig.DATABASE_URL || `file:${defaultDbPath}`;
39
+ // Auto-save generated config if running in CLI mode or without explicit .env file
40
+ if (!fs_1.default.existsSync(configPath) && (process.env.NINEDRIVE_CLI === 'true' || !process.env.DATABASE_URL)) {
41
+ try {
42
+ fs_1.default.mkdirSync(dataDir, { recursive: true });
43
+ fs_1.default.writeFileSync(configPath, JSON.stringify({
44
+ APP_PORT: defaultPort,
45
+ DATABASE_URL: defaultDatabaseUrl,
46
+ FRONTEND_URL: defaultFrontendUrl,
47
+ JWT_ACCESS_SECRET: defaultAccessSecret,
48
+ TOKEN_ENCRYPTION_KEY: defaultEncryptionKey,
49
+ }, null, 2));
50
+ }
51
+ catch { }
52
+ }
53
+ const envSchema = zod_1.z.object({
54
+ DATABASE_URL: zod_1.z.string().min(1),
55
+ APP_PORT: zod_1.z.coerce.number().default(4000),
56
+ FRONTEND_URL: zod_1.z.string().url(),
57
+ JWT_ACCESS_SECRET: zod_1.z.string().min(32),
58
+ TOKEN_ENCRYPTION_KEY: zod_1.z.string().min(32),
59
+ ACCESS_TOKEN_TTL_SECONDS: zod_1.z.coerce.number().default(900),
60
+ REFRESH_TOKEN_TTL_DAYS: zod_1.z.coerce.number().default(30),
61
+ MAX_UPLOAD_BYTES: zod_1.z.coerce.number().default(5 * 1024 * 1024 * 1024),
62
+ RECAPTCHA_SECRET_KEY: zod_1.z.string().optional(),
63
+ CDN_BASE_URL: zod_1.z.string().optional(),
64
+ SERVE_FRONTEND: zod_1.z.coerce.boolean().default(false),
65
+ NINEDRIVE_DATA_DIR: zod_1.z.string().default(dataDir),
66
+ });
67
+ exports.env = envSchema.parse({
68
+ ...fileConfig,
69
+ DATABASE_URL: defaultDatabaseUrl,
70
+ APP_PORT: defaultPort,
71
+ FRONTEND_URL: defaultFrontendUrl,
72
+ JWT_ACCESS_SECRET: defaultAccessSecret,
73
+ TOKEN_ENCRYPTION_KEY: defaultEncryptionKey,
74
+ ...process.env,
75
+ });
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prisma = void 0;
4
+ const client_1 = require("@prisma/client");
5
+ const dbUrl = process.env.DATABASE_URL || '';
6
+ const isSqlite = dbUrl.startsWith('file:') || dbUrl.startsWith('sqlite:');
7
+ let clientInstance = null;
8
+ if (isSqlite) {
9
+ try {
10
+ // Dynamic synchronous require for CommonJS
11
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
12
+ const sqliteModule = require('../../prisma/generated/sqlite-client/index.js');
13
+ const SqliteClient = sqliteModule.PrismaClient;
14
+ clientInstance = new SqliteClient({
15
+ datasources: {
16
+ db: {
17
+ url: dbUrl,
18
+ },
19
+ },
20
+ });
21
+ }
22
+ catch (err) {
23
+ console.warn('[Prisma] Could not load SQLite client from generated folder, using default client.');
24
+ clientInstance = new client_1.PrismaClient();
25
+ }
26
+ }
27
+ else {
28
+ clientInstance = new client_1.PrismaClient();
29
+ }
30
+ exports.prisma = clientInstance;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requireApiKey = requireApiKey;
4
+ const prisma_js_1 = require("../config/prisma.js");
5
+ const crypto_js_1 = require("../utils/crypto.js");
6
+ function normalizeScopes(value) {
7
+ return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
8
+ }
9
+ function requireApiKey(scope) {
10
+ return async (req, res, next) => {
11
+ try {
12
+ const authHeader = req.header('Authorization');
13
+ const xApiKey = req.header('x-api-key');
14
+ let rawKey = '';
15
+ if (xApiKey && typeof xApiKey === 'string') {
16
+ rawKey = xApiKey.trim();
17
+ }
18
+ else if (authHeader?.startsWith('Bearer ')) {
19
+ rawKey = authHeader.slice(7).trim();
20
+ }
21
+ if (!rawKey)
22
+ return res.status(401).json({ code: 'API_KEY_REQUIRED', message: 'API key required (via x-api-key or Authorization: Bearer).' });
23
+ const apiKey = await prisma_js_1.prisma.apiKey.findUnique({ where: { keyHash: (0, crypto_js_1.hashToken)(rawKey) } });
24
+ if (!apiKey || apiKey.status !== 'active' || apiKey.revokedAt || (apiKey.expiresAt && apiKey.expiresAt <= new Date()))
25
+ return res.status(401).json({ code: 'API_KEY_INVALID', message: 'Invalid API key.' });
26
+ const scopes = normalizeScopes(apiKey.scopes);
27
+ if (!scopes.includes(scope))
28
+ return res.status(403).json({ code: 'API_KEY_FORBIDDEN', message: 'API key does not have required scope.' });
29
+ req.user = { id: apiKey.userId, sessionId: `api-key:${apiKey.id}` };
30
+ req.apiKey = { id: apiKey.id, scopes };
31
+ await prisma_js_1.prisma.apiKey.update({ where: { id: apiKey.id }, data: { lastUsedAt: new Date() } }).catch(() => undefined);
32
+ return next();
33
+ }
34
+ catch {
35
+ return res.status(401).json({ code: 'API_KEY_INVALID', message: 'Invalid API key.' });
36
+ }
37
+ };
38
+ }
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requireAuth = requireAuth;
4
+ const prisma_js_1 = require("../config/prisma.js");
5
+ const jwt_js_1 = require("../utils/jwt.js");
6
+ async function requireAuth(req, res, next) {
7
+ try {
8
+ const header = req.header('Authorization');
9
+ if (!header?.startsWith('Bearer '))
10
+ return res.status(401).json({ code: 'AUTH_REQUIRED', message: 'Bearer token required.' });
11
+ const payload = (0, jwt_js_1.verifyAccessToken)(header.slice(7));
12
+ const session = await prisma_js_1.prisma.userSession.findUnique({ where: { id: payload.sid } });
13
+ if (!session || session.revokedAt || session.expiresAt < new Date())
14
+ return res.status(401).json({ code: 'AUTH_SESSION_EXPIRED', message: 'Session expired.' });
15
+ req.user = { id: payload.sub, sessionId: payload.sid };
16
+ return next();
17
+ }
18
+ catch {
19
+ return res.status(401).json({ code: 'AUTH_INVALID_TOKEN', message: 'Invalid token.' });
20
+ }
21
+ }
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.errorMiddleware = errorMiddleware;
4
+ function errorMiddleware(error, _req, res, _next) {
5
+ const message = error instanceof Error ? error.message : 'Internal server error';
6
+ return res.status(500).json({ code: 'INTERNAL_SERVER_ERROR', message });
7
+ }
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.apiKeyRouter = void 0;
4
+ const express_1 = require("express");
5
+ const zod_1 = require("zod");
6
+ const prisma_js_1 = require("../../config/prisma.js");
7
+ const auth_middleware_js_1 = require("../../middleware/auth.middleware.js");
8
+ const crypto_js_1 = require("../../utils/crypto.js");
9
+ exports.apiKeyRouter = (0, express_1.Router)();
10
+ exports.apiKeyRouter.use(auth_middleware_js_1.requireAuth);
11
+ const createSchema = zod_1.z.object({ name: zod_1.z.string().trim().min(1).max(191), expiresAt: zod_1.z.string().datetime().nullable().optional() });
12
+ const scopes = ['files:upload'];
13
+ function serializeApiKey(apiKey) {
14
+ return {
15
+ id: apiKey.id,
16
+ name: apiKey.name,
17
+ keyPrefix: apiKey.keyPrefix,
18
+ scopes: Array.isArray(apiKey.scopes) ? apiKey.scopes : [],
19
+ status: apiKey.status,
20
+ lastUsedAt: apiKey.lastUsedAt?.toISOString() ?? null,
21
+ expiresAt: apiKey.expiresAt?.toISOString() ?? null,
22
+ revokedAt: apiKey.revokedAt?.toISOString() ?? null,
23
+ createdAt: apiKey.createdAt.toISOString(),
24
+ };
25
+ }
26
+ exports.apiKeyRouter.get('/', async (req, res, next) => {
27
+ try {
28
+ const apiKeys = await prisma_js_1.prisma.apiKey.findMany({ where: { userId: req.user.id }, orderBy: { createdAt: 'desc' } });
29
+ return res.json({ apiKeys: apiKeys.map(serializeApiKey) });
30
+ }
31
+ catch (error) {
32
+ return next(error);
33
+ }
34
+ });
35
+ exports.apiKeyRouter.post('/', async (req, res, next) => {
36
+ try {
37
+ const body = createSchema.parse(req.body);
38
+ const secret = `9d_live_${(0, crypto_js_1.randomToken)(32)}`;
39
+ const apiKey = await prisma_js_1.prisma.apiKey.create({
40
+ data: {
41
+ userId: req.user.id,
42
+ name: body.name,
43
+ keyPrefix: secret.slice(0, 16),
44
+ keyHash: (0, crypto_js_1.hashToken)(secret),
45
+ scopes,
46
+ expiresAt: body.expiresAt ? new Date(body.expiresAt) : null,
47
+ },
48
+ });
49
+ return res.status(201).json({ apiKey: serializeApiKey(apiKey), secret });
50
+ }
51
+ catch (error) {
52
+ return next(error);
53
+ }
54
+ });
55
+ exports.apiKeyRouter.patch('/:id/status', async (req, res, next) => {
56
+ try {
57
+ const statusSchema = zod_1.z.object({ status: zod_1.z.enum(['active', 'revoked']) });
58
+ const body = statusSchema.parse(req.body);
59
+ const key = await prisma_js_1.prisma.apiKey.findFirst({
60
+ where: { id: String(req.params.id), userId: req.user.id },
61
+ });
62
+ if (!key)
63
+ return res.status(404).json({ error: 'API key not found' });
64
+ const updated = await prisma_js_1.prisma.apiKey.update({
65
+ where: { id: key.id },
66
+ data: {
67
+ status: body.status,
68
+ revokedAt: body.status === 'revoked' ? new Date() : null,
69
+ },
70
+ });
71
+ return res.json({ apiKey: serializeApiKey(updated) });
72
+ }
73
+ catch (error) {
74
+ return next(error);
75
+ }
76
+ });
77
+ exports.apiKeyRouter.delete('/:id', async (req, res, next) => {
78
+ try {
79
+ const deleted = await prisma_js_1.prisma.apiKey.deleteMany({
80
+ where: { id: String(req.params.id), userId: req.user.id },
81
+ });
82
+ if (deleted.count === 0) {
83
+ return res.status(404).json({ error: 'API key not found' });
84
+ }
85
+ return res.json({ status: 'ok', deletedCount: deleted.count });
86
+ }
87
+ catch (error) {
88
+ return next(error);
89
+ }
90
+ });
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.auditLogRouter = void 0;
4
+ const express_1 = require("express");
5
+ const prisma_js_1 = require("../../config/prisma.js");
6
+ const auth_middleware_js_1 = require("../../middleware/auth.middleware.js");
7
+ exports.auditLogRouter = (0, express_1.Router)();
8
+ exports.auditLogRouter.get('/', auth_middleware_js_1.requireAuth, async (req, res, next) => {
9
+ try {
10
+ const logs = await prisma_js_1.prisma.auditLog.findMany({
11
+ where: { userId: req.user.id },
12
+ orderBy: { createdAt: 'desc' },
13
+ take: 100
14
+ });
15
+ return res.json({ logs });
16
+ }
17
+ catch (error) {
18
+ return next(error);
19
+ }
20
+ });
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.authRouter = void 0;
4
+ const express_1 = require("express");
5
+ const googleapis_1 = require("googleapis");
6
+ const zod_1 = require("zod");
7
+ const prisma_js_1 = require("../../config/prisma.js");
8
+ const env_js_1 = require("../../config/env.js");
9
+ const auth_middleware_js_1 = require("../../middleware/auth.middleware.js");
10
+ const password_js_1 = require("../../utils/password.js");
11
+ const crypto_js_1 = require("../../utils/crypto.js");
12
+ const jwt_js_1 = require("../../utils/jwt.js");
13
+ const google_service_js_1 = require("../google/google.service.js");
14
+ exports.authRouter = (0, express_1.Router)();
15
+ const registerSchema = zod_1.z.object({ name: zod_1.z.string().min(2), email: zod_1.z.string().email(), password: zod_1.z.string().min(8), captchaToken: zod_1.z.string().optional() });
16
+ const loginSchema = zod_1.z.object({ email: zod_1.z.string().email(), password: zod_1.z.string().min(1) });
17
+ const refreshSchema = zod_1.z.object({ refreshToken: zod_1.z.string().min(1) });
18
+ const googleExchangeSchema = zod_1.z.object({ token: zod_1.z.string().min(1) });
19
+ async function createSession(userId, req) {
20
+ const refreshToken = (0, crypto_js_1.randomToken)();
21
+ const expiresAt = new Date(Date.now() + env_js_1.env.REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000);
22
+ const session = await prisma_js_1.prisma.userSession.create({
23
+ data: {
24
+ userId,
25
+ refreshTokenHash: (0, crypto_js_1.hashToken)(refreshToken),
26
+ userAgent: req.header('User-Agent'),
27
+ ipAddress: req.ip,
28
+ expiresAt,
29
+ },
30
+ });
31
+ return { accessToken: (0, jwt_js_1.signAccessToken)({ sub: userId, sid: session.id }), refreshToken };
32
+ }
33
+ async function verifyCaptcha(token) {
34
+ if (!env_js_1.env.RECAPTCHA_SECRET_KEY)
35
+ return true;
36
+ if (!token)
37
+ return false;
38
+ const form = new URLSearchParams({ secret: env_js_1.env.RECAPTCHA_SECRET_KEY, response: token });
39
+ const response = await fetch('https://www.google.com/recaptcha/api/siteverify', { method: 'POST', body: form });
40
+ const data = await response.json();
41
+ return Boolean(data.success);
42
+ }
43
+ exports.authRouter.post('/register', async (req, res, next) => {
44
+ try {
45
+ const body = registerSchema.parse(req.body);
46
+ if (!(await verifyCaptcha(body.captchaToken)))
47
+ return res.status(400).json({ code: 'CAPTCHA_FAILED', message: 'Captcha verification failed.' });
48
+ const existing = await prisma_js_1.prisma.user.findUnique({ where: { email: body.email } });
49
+ if (existing)
50
+ return res.status(409).json({ code: 'AUTH_EMAIL_TAKEN', message: 'Email already registered.' });
51
+ const user = await prisma_js_1.prisma.user.create({ data: { name: body.name, email: body.email, passwordHash: await (0, password_js_1.hashPassword)(body.password) } });
52
+ const tokens = await createSession(user.id, req);
53
+ return res.status(201).json({ ...tokens, user: { id: user.id, name: user.name, email: user.email } });
54
+ }
55
+ catch (error) {
56
+ return next(error);
57
+ }
58
+ });
59
+ exports.authRouter.post('/login', async (req, res, next) => {
60
+ try {
61
+ const body = loginSchema.parse(req.body);
62
+ const user = await prisma_js_1.prisma.user.findUnique({ where: { email: body.email } });
63
+ if (!user || !(await (0, password_js_1.verifyPassword)(user.passwordHash, body.password)))
64
+ return res.status(401).json({ code: 'AUTH_INVALID_CREDENTIALS', message: 'Invalid email or password.' });
65
+ const tokens = await createSession(user.id, req);
66
+ return res.json({ ...tokens, user: { id: user.id, name: user.name, email: user.email } });
67
+ }
68
+ catch (error) {
69
+ return next(error);
70
+ }
71
+ });
72
+ exports.authRouter.get('/google/url', async (_req, res, next) => {
73
+ try {
74
+ const config = await prisma_js_1.prisma.providerConfig.findFirstOrThrow({ where: { userId: null, provider: 'google_drive', status: 'active' }, orderBy: { createdAt: 'desc' } });
75
+ const state = (0, crypto_js_1.randomToken)();
76
+ await prisma_js_1.prisma.oauthState.create({ data: { providerConfigId: config.id, flow: 'login', stateHash: (0, crypto_js_1.hashToken)(state), expiresAt: new Date(Date.now() + 10 * 60_000) } });
77
+ const client = (0, google_service_js_1.createOAuthClient)(config);
78
+ const url = client.generateAuthUrl({
79
+ access_type: 'offline',
80
+ prompt: 'consent',
81
+ include_granted_scopes: true,
82
+ scope: config.scopes,
83
+ state,
84
+ });
85
+ return res.json({ url });
86
+ }
87
+ catch (error) {
88
+ return next(error);
89
+ }
90
+ });
91
+ exports.authRouter.get('/google/callback', async (req, res) => {
92
+ try {
93
+ const query = zod_1.z.object({ code: zod_1.z.string(), state: zod_1.z.string() }).parse(req.query);
94
+ const oauthState = await prisma_js_1.prisma.oauthState.findUniqueOrThrow({ where: { stateHash: (0, crypto_js_1.hashToken)(query.state) }, include: { providerConfig: true } });
95
+ if (oauthState.flow !== 'login' || oauthState.usedAt || oauthState.expiresAt < new Date())
96
+ return res.redirect(`${env_js_1.env.FRONTEND_URL}/google-auth?status=error`);
97
+ const client = (0, google_service_js_1.createOAuthClient)(oauthState.providerConfig);
98
+ const tokenResult = await client.getToken(query.code);
99
+ const tokens = tokenResult.tokens;
100
+ if (!tokens.access_token)
101
+ return res.redirect(`${env_js_1.env.FRONTEND_URL}/google-auth?status=error`);
102
+ client.setCredentials(tokens);
103
+ const oauth2 = googleapis_1.google.oauth2({ version: 'v2', auth: client });
104
+ const profile = await oauth2.userinfo.get();
105
+ const providerAccountId = profile.data.id;
106
+ const email = profile.data.email;
107
+ if (!providerAccountId || !email)
108
+ return res.redirect(`${env_js_1.env.FRONTEND_URL}/google-auth?status=error`);
109
+ const name = profile.data.name || email.split('@')[0] || 'Google User';
110
+ const user = await prisma_js_1.prisma.user.upsert({
111
+ where: { email },
112
+ create: { email, name, passwordHash: await (0, password_js_1.hashPassword)((0, crypto_js_1.randomToken)(32)) },
113
+ update: { name },
114
+ });
115
+ const existingAccount = await prisma_js_1.prisma.connectedAccount.findUnique({ where: { userId_provider_providerAccountId: { userId: user.id, provider: 'google_drive', providerAccountId } } });
116
+ const refreshTokenEncrypted = tokens.refresh_token ? (0, crypto_js_1.encryptText)(tokens.refresh_token) : existingAccount?.refreshTokenEncrypted;
117
+ if (!refreshTokenEncrypted)
118
+ return res.redirect(`${env_js_1.env.FRONTEND_URL}/google-auth?status=error`);
119
+ const account = await prisma_js_1.prisma.connectedAccount.upsert({
120
+ where: { userId_provider_providerAccountId: { userId: user.id, provider: 'google_drive', providerAccountId } },
121
+ create: {
122
+ userId: user.id,
123
+ providerConfigId: oauthState.providerConfigId,
124
+ provider: 'google_drive',
125
+ providerAccountId,
126
+ email,
127
+ displayName: profile.data.name,
128
+ avatarUrl: profile.data.picture,
129
+ accessTokenEncrypted: (0, crypto_js_1.encryptText)(tokens.access_token),
130
+ refreshTokenEncrypted,
131
+ tokenExpiresAt: new Date(tokens.expiry_date ?? Date.now() + 3600_000),
132
+ scopes: oauthState.providerConfig.scopes,
133
+ status: 'connected',
134
+ },
135
+ update: {
136
+ providerConfigId: oauthState.providerConfigId,
137
+ email,
138
+ displayName: profile.data.name,
139
+ avatarUrl: profile.data.picture,
140
+ accessTokenEncrypted: (0, crypto_js_1.encryptText)(tokens.access_token),
141
+ refreshTokenEncrypted,
142
+ tokenExpiresAt: new Date(tokens.expiry_date ?? Date.now() + 3600_000),
143
+ scopes: oauthState.providerConfig.scopes,
144
+ status: 'connected',
145
+ },
146
+ });
147
+ await prisma_js_1.prisma.oauthState.update({ where: { id: oauthState.id }, data: { usedAt: new Date(), userId: user.id } });
148
+ await (0, google_service_js_1.syncGoogleQuota)(account.id).catch(() => undefined);
149
+ const handoffToken = (0, crypto_js_1.randomToken)();
150
+ await prisma_js_1.prisma.authHandoff.create({ data: { userId: user.id, tokenHash: (0, crypto_js_1.hashToken)(handoffToken), expiresAt: new Date(Date.now() + 5 * 60_000) } });
151
+ return res.redirect(`${env_js_1.env.FRONTEND_URL}/google-auth?token=${handoffToken}`);
152
+ }
153
+ catch (error) {
154
+ console.error('Google Auth callback failed:', error);
155
+ return res.redirect(`${env_js_1.env.FRONTEND_URL}/google-auth?status=error`);
156
+ }
157
+ });
158
+ exports.authRouter.post('/google/exchange', async (req, res, next) => {
159
+ try {
160
+ const body = googleExchangeSchema.parse(req.body);
161
+ const handoff = await prisma_js_1.prisma.authHandoff.findFirst({ where: { tokenHash: (0, crypto_js_1.hashToken)(body.token), usedAt: null, expiresAt: { gt: new Date() } }, include: { user: true } });
162
+ if (!handoff)
163
+ return res.status(401).json({ code: 'AUTH_GOOGLE_HANDOFF_INVALID', message: 'Google login session expired.' });
164
+ await prisma_js_1.prisma.authHandoff.update({ where: { id: handoff.id }, data: { usedAt: new Date() } });
165
+ const tokens = await createSession(handoff.userId, req);
166
+ return res.json({ ...tokens, user: { id: handoff.user.id, name: handoff.user.name, email: handoff.user.email } });
167
+ }
168
+ catch (error) {
169
+ return next(error);
170
+ }
171
+ });
172
+ exports.authRouter.post('/refresh', async (req, res, next) => {
173
+ try {
174
+ const body = refreshSchema.parse(req.body);
175
+ const session = await prisma_js_1.prisma.userSession.findFirst({ where: { refreshTokenHash: (0, crypto_js_1.hashToken)(body.refreshToken), revokedAt: null, expiresAt: { gt: new Date() } } });
176
+ if (!session)
177
+ return res.status(401).json({ code: 'AUTH_SESSION_EXPIRED', message: 'Refresh token expired.' });
178
+ return res.json({ accessToken: (0, jwt_js_1.signAccessToken)({ sub: session.userId, sid: session.id }) });
179
+ }
180
+ catch (error) {
181
+ return next(error);
182
+ }
183
+ });
184
+ exports.authRouter.post('/logout', auth_middleware_js_1.requireAuth, async (req, res, next) => {
185
+ try {
186
+ await prisma_js_1.prisma.userSession.update({ where: { id: req.user.sessionId }, data: { revokedAt: new Date() } });
187
+ return res.json({ status: 'ok' });
188
+ }
189
+ catch (error) {
190
+ return next(error);
191
+ }
192
+ });
193
+ exports.authRouter.get('/me', auth_middleware_js_1.requireAuth, async (req, res, next) => {
194
+ try {
195
+ const user = await prisma_js_1.prisma.user.findUniqueOrThrow({ where: { id: req.user.id }, select: { id: true, name: true, email: true, status: true } });
196
+ return res.json({ user });
197
+ }
198
+ catch (error) {
199
+ return next(error);
200
+ }
201
+ });