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,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cdnRouter = void 0;
4
+ const express_1 = require("express");
5
+ const prisma_js_1 = require("../../config/prisma.js");
6
+ const stream_file_js_1 = require("../files/stream-file.js");
7
+ exports.cdnRouter = (0, express_1.Router)();
8
+ async function findCdnFile(id) {
9
+ const file = await prisma_js_1.prisma.file.findFirst({
10
+ where: {
11
+ id,
12
+ status: 'active',
13
+ deletedAt: null,
14
+ },
15
+ include: {
16
+ connectedAccount: true,
17
+ },
18
+ });
19
+ if (!file || file.connectedAccount.status !== 'connected') {
20
+ return null;
21
+ }
22
+ return file;
23
+ }
24
+ function setCdnHeaders(res, file) {
25
+ const etag = `"${file.id}-${file.updatedAt ? file.updatedAt.getTime() : file.createdAt.getTime()}"`;
26
+ res.setHeader('Access-Control-Allow-Origin', '*');
27
+ res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS');
28
+ res.setHeader('Access-Control-Expose-Headers', 'Content-Length, Content-Range, Content-Type, ETag, Cache-Control');
29
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
30
+ res.setHeader('ETag', etag);
31
+ res.setHeader('Last-Modified', (file.updatedAt || file.createdAt).toUTCString());
32
+ return etag;
33
+ }
34
+ // View/Preview route (inline disposition for <img> and web embedding)
35
+ exports.cdnRouter.all('/view/:id', async (req, res, next) => {
36
+ try {
37
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
38
+ return res.status(405).json({ code: 'METHOD_NOT_ALLOWED', message: 'Only GET and HEAD methods are supported.' });
39
+ }
40
+ const file = await findCdnFile(String(req.params.id));
41
+ if (!file) {
42
+ return res.status(404).json({ code: 'FILE_NOT_FOUND', message: 'File not found or inactive.' });
43
+ }
44
+ const etag = setCdnHeaders(res, file);
45
+ const ifNoneMatch = req.header('if-none-match');
46
+ if (ifNoneMatch && (ifNoneMatch === etag || ifNoneMatch === `W/${etag}` || ifNoneMatch === '*')) {
47
+ return res.status(304).end();
48
+ }
49
+ if (req.method === 'HEAD') {
50
+ res.setHeader('Content-Type', file.mimeType);
51
+ res.setHeader('Content-Length', file.sizeBytes.toString());
52
+ res.setHeader('Accept-Ranges', 'bytes');
53
+ res.setHeader('Content-Disposition', `inline; filename="${file.name.replaceAll('"', '')}"`);
54
+ return res.status(200).end();
55
+ }
56
+ return (0, stream_file_js_1.streamProviderFile)(file, req.headers.range, res, {
57
+ disposition: 'inline',
58
+ headers: {
59
+ 'Cache-Control': 'public, max-age=31536000, immutable',
60
+ 'Access-Control-Allow-Origin': '*',
61
+ },
62
+ });
63
+ }
64
+ catch (error) {
65
+ return next(error);
66
+ }
67
+ });
68
+ // Raw/Download route (attachment disposition for file download)
69
+ exports.cdnRouter.all('/raw/:id', async (req, res, next) => {
70
+ try {
71
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
72
+ return res.status(405).json({ code: 'METHOD_NOT_ALLOWED', message: 'Only GET and HEAD methods are supported.' });
73
+ }
74
+ const file = await findCdnFile(String(req.params.id));
75
+ if (!file) {
76
+ return res.status(404).json({ code: 'FILE_NOT_FOUND', message: 'File not found or inactive.' });
77
+ }
78
+ const etag = setCdnHeaders(res, file);
79
+ const ifNoneMatch = req.header('if-none-match');
80
+ if (ifNoneMatch && (ifNoneMatch === etag || ifNoneMatch === `W/${etag}` || ifNoneMatch === '*')) {
81
+ return res.status(304).end();
82
+ }
83
+ if (req.method === 'HEAD') {
84
+ res.setHeader('Content-Type', file.mimeType);
85
+ res.setHeader('Content-Length', file.sizeBytes.toString());
86
+ res.setHeader('Accept-Ranges', 'bytes');
87
+ res.setHeader('Content-Disposition', `attachment; filename="${file.name.replaceAll('"', '')}"`);
88
+ return res.status(200).end();
89
+ }
90
+ return (0, stream_file_js_1.streamProviderFile)(file, req.headers.range, res, {
91
+ disposition: 'attachment',
92
+ headers: {
93
+ 'Cache-Control': 'public, max-age=31536000, immutable',
94
+ 'Access-Control-Allow-Origin': '*',
95
+ },
96
+ });
97
+ }
98
+ catch (error) {
99
+ return next(error);
100
+ }
101
+ });
102
+ // Redirect /cdn/:id to /cdn/view/:id
103
+ exports.cdnRouter.get('/:id', (req, res) => {
104
+ res.redirect(301, `/cdn/view/${req.params.id}`);
105
+ });
@@ -0,0 +1,313 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.connectedAccountRouter = void 0;
4
+ const express_1 = require("express");
5
+ const googleapis_1 = require("googleapis");
6
+ const zod_1 = require("zod");
7
+ const env_js_1 = require("../../config/env.js");
8
+ const prisma_js_1 = require("../../config/prisma.js");
9
+ const auth_middleware_js_1 = require("../../middleware/auth.middleware.js");
10
+ const crypto_js_1 = require("../../utils/crypto.js");
11
+ const password_js_1 = require("../../utils/password.js");
12
+ const google_service_js_1 = require("../google/google.service.js");
13
+ const s3_service_js_1 = require("../s3/s3.service.js");
14
+ exports.connectedAccountRouter = (0, express_1.Router)();
15
+ const s3ConnectSchema = zod_1.z.object({
16
+ name: zod_1.z.string().trim().min(1).max(191),
17
+ bucket: zod_1.z.string().trim().min(1).max(191),
18
+ region: zod_1.z.string().trim().min(1).max(191),
19
+ endpoint: zod_1.z.string().url().optional().or(zod_1.z.literal('')),
20
+ accessKeyId: zod_1.z.string().min(1),
21
+ secretAccessKey: zod_1.z.string().min(1),
22
+ forcePathStyle: zod_1.z.boolean().optional(),
23
+ quotaBytes: zod_1.z.string().regex(/^\d+$/).optional().nullable(),
24
+ });
25
+ async function syncQuotaForAccount(account) {
26
+ if (account.provider === 's3')
27
+ return (0, s3_service_js_1.syncS3Quota)(account.id);
28
+ return (0, google_service_js_1.syncGoogleQuota)(account.id);
29
+ }
30
+ exports.connectedAccountRouter.get('/', auth_middleware_js_1.requireAuth, async (req, res, next) => {
31
+ try {
32
+ const accounts = await prisma_js_1.prisma.connectedAccount.findMany({
33
+ where: { userId: req.user.id, status: 'connected' },
34
+ include: { storageAccount: true },
35
+ orderBy: { createdAt: 'desc' },
36
+ });
37
+ const missingQuota = accounts.filter((account) => !account.storageAccount?.lastSyncedAt);
38
+ for (const account of missingQuota)
39
+ await syncQuotaForAccount(account).catch(() => undefined);
40
+ const syncedAccounts = missingQuota.length > 0
41
+ ? await prisma_js_1.prisma.connectedAccount.findMany({
42
+ where: { userId: req.user.id, status: 'connected' },
43
+ include: { storageAccount: true },
44
+ orderBy: { createdAt: 'desc' },
45
+ })
46
+ : accounts;
47
+ return res.json({
48
+ accounts: syncedAccounts.map(({ accessTokenEncrypted: _a, refreshTokenEncrypted: _r, storageAccount, ...account }) => ({
49
+ ...account,
50
+ storageAccount: storageAccount ? {
51
+ ...storageAccount,
52
+ totalBytes: storageAccount.totalBytes?.toString() ?? null,
53
+ usedBytes: storageAccount.usedBytes.toString(),
54
+ availableBytes: storageAccount.availableBytes?.toString() ?? null,
55
+ trashBytes: storageAccount.trashBytes?.toString() ?? null,
56
+ } : null,
57
+ })),
58
+ });
59
+ }
60
+ catch (error) {
61
+ return next(error);
62
+ }
63
+ });
64
+ async function createGoogleConnectUrl(req) {
65
+ const query = zod_1.z.object({ providerConfigId: zod_1.z.string().min(1).optional() }).parse(req.query);
66
+ const config = query.providerConfigId
67
+ ? await prisma_js_1.prisma.providerConfig.findFirstOrThrow({ where: { id: query.providerConfigId, OR: [{ userId: req.user.id }, { userId: null }], provider: 'google_drive', status: 'active' } })
68
+ : await prisma_js_1.prisma.providerConfig.findFirstOrThrow({ where: { userId: null, provider: 'google_drive', status: 'active' }, orderBy: { createdAt: 'desc' } });
69
+ const state = (0, crypto_js_1.randomToken)();
70
+ await prisma_js_1.prisma.oauthState.create({ data: { userId: req.user.id, providerConfigId: config.id, flow: 'connect', stateHash: (0, crypto_js_1.hashToken)(state), expiresAt: new Date(Date.now() + 10 * 60_000) } });
71
+ const client = (0, google_service_js_1.createOAuthClient)(config);
72
+ return client.generateAuthUrl({
73
+ access_type: 'offline',
74
+ prompt: 'consent',
75
+ include_granted_scopes: true,
76
+ scope: config.scopes,
77
+ state,
78
+ });
79
+ }
80
+ exports.connectedAccountRouter.post('/s3', auth_middleware_js_1.requireAuth, async (req, res, next) => {
81
+ try {
82
+ const body = s3ConnectSchema.parse(req.body);
83
+ const providerConfig = await prisma_js_1.prisma.providerConfig.findFirstOrThrow({ where: { provider: 'google_drive', status: 'active' }, orderBy: { createdAt: 'desc' } });
84
+ const providerAccountId = `${body.bucket}:${body.endpoint || body.region}`;
85
+ const existingAccount = await prisma_js_1.prisma.connectedAccount.findUnique({ where: { userId_provider_providerAccountId: { userId: req.user.id, provider: 's3', providerAccountId } } });
86
+ const account = existingAccount
87
+ ? await prisma_js_1.prisma.connectedAccount.update({
88
+ where: { id: existingAccount.id },
89
+ data: {
90
+ providerConfigId: providerConfig.id,
91
+ email: `${body.bucket} (S3)`,
92
+ displayName: body.name,
93
+ accessTokenEncrypted: (0, crypto_js_1.encryptText)('s3'),
94
+ refreshTokenEncrypted: (0, crypto_js_1.encryptText)((0, crypto_js_1.randomToken)()),
95
+ tokenExpiresAt: new Date(Date.now() + 100 * 365 * 24 * 60 * 60 * 1000),
96
+ scopes: [],
97
+ status: 'connected',
98
+ },
99
+ })
100
+ : await prisma_js_1.prisma.connectedAccount.create({ data: {
101
+ userId: req.user.id,
102
+ providerConfigId: providerConfig.id,
103
+ provider: 's3',
104
+ providerAccountId,
105
+ email: `${body.bucket} (S3)`,
106
+ displayName: body.name,
107
+ accessTokenEncrypted: (0, crypto_js_1.encryptText)('s3'),
108
+ refreshTokenEncrypted: (0, crypto_js_1.encryptText)((0, crypto_js_1.randomToken)()),
109
+ tokenExpiresAt: new Date(Date.now() + 100 * 365 * 24 * 60 * 60 * 1000),
110
+ scopes: [],
111
+ status: 'connected',
112
+ } });
113
+ const config = await prisma_js_1.prisma.s3StorageConfig.upsert({
114
+ where: { connectedAccountId: account.id },
115
+ create: {
116
+ userId: req.user.id,
117
+ connectedAccountId: account.id,
118
+ name: body.name,
119
+ bucket: body.bucket,
120
+ region: body.region,
121
+ endpoint: body.endpoint || null,
122
+ accessKeyIdEncrypted: (0, crypto_js_1.encryptText)(body.accessKeyId),
123
+ secretAccessKeyEncrypted: (0, crypto_js_1.encryptText)(body.secretAccessKey),
124
+ forcePathStyle: body.forcePathStyle ?? Boolean(body.endpoint),
125
+ quotaBytes: body.quotaBytes ? BigInt(body.quotaBytes) : null,
126
+ },
127
+ update: {
128
+ name: body.name,
129
+ bucket: body.bucket,
130
+ region: body.region,
131
+ endpoint: body.endpoint || null,
132
+ accessKeyIdEncrypted: (0, crypto_js_1.encryptText)(body.accessKeyId),
133
+ secretAccessKeyEncrypted: (0, crypto_js_1.encryptText)(body.secretAccessKey),
134
+ forcePathStyle: body.forcePathStyle ?? Boolean(body.endpoint),
135
+ quotaBytes: body.quotaBytes ? BigInt(body.quotaBytes) : null,
136
+ status: 'active',
137
+ },
138
+ });
139
+ try {
140
+ await (0, s3_service_js_1.testS3Connection)(config);
141
+ const quota = await (0, s3_service_js_1.syncS3Quota)(account.id);
142
+ return res.status(201).json({
143
+ account: {
144
+ ...account,
145
+ storageAccount: { ...quota, totalBytes: quota.totalBytes?.toString() ?? null, usedBytes: quota.usedBytes.toString(), availableBytes: quota.availableBytes?.toString() ?? null, trashBytes: quota.trashBytes?.toString() ?? null },
146
+ },
147
+ });
148
+ }
149
+ catch (error) {
150
+ if (!existingAccount)
151
+ await prisma_js_1.prisma.connectedAccount.delete({ where: { id: account.id } }).catch(() => undefined);
152
+ throw error;
153
+ }
154
+ }
155
+ catch (error) {
156
+ return next(error);
157
+ }
158
+ });
159
+ exports.connectedAccountRouter.get('/google/connect-url', auth_middleware_js_1.requireAuth, async (req, res, next) => {
160
+ try {
161
+ const url = await createGoogleConnectUrl(req);
162
+ return res.json({ url });
163
+ }
164
+ catch (error) {
165
+ return next(error);
166
+ }
167
+ });
168
+ exports.connectedAccountRouter.get('/google/connect', auth_middleware_js_1.requireAuth, async (req, res, next) => {
169
+ try {
170
+ const url = await createGoogleConnectUrl(req);
171
+ return res.redirect(url);
172
+ }
173
+ catch (error) {
174
+ return next(error);
175
+ }
176
+ });
177
+ exports.connectedAccountRouter.get('/google/callback', async (req, res, next) => {
178
+ try {
179
+ const query = zod_1.z.object({ code: zod_1.z.string(), state: zod_1.z.string() }).parse(req.query);
180
+ const oauthState = await prisma_js_1.prisma.oauthState.findUniqueOrThrow({ where: { stateHash: (0, crypto_js_1.hashToken)(query.state) }, include: { providerConfig: true } });
181
+ if (oauthState.usedAt || oauthState.expiresAt < new Date())
182
+ return res.status(400).json({ code: 'GOOGLE_OAUTH_STATE_INVALID', message: 'OAuth state expired.' });
183
+ const client = (0, google_service_js_1.createOAuthClient)(oauthState.providerConfig);
184
+ const tokenResult = await client.getToken(query.code);
185
+ const tokens = tokenResult.tokens;
186
+ if (!tokens.access_token)
187
+ return res.status(400).json({ code: 'GOOGLE_OAUTH_FAILED', message: 'Google did not return required tokens.' });
188
+ client.setCredentials(tokens);
189
+ const oauth2 = googleapis_1.google.oauth2({ version: 'v2', auth: client });
190
+ const profile = await oauth2.userinfo.get();
191
+ const providerAccountId = profile.data.id;
192
+ const email = profile.data.email;
193
+ if (!providerAccountId || !email)
194
+ return res.status(400).json({ code: 'GOOGLE_PROFILE_FAILED', message: 'Google profile missing id or email.' });
195
+ if (oauthState.flow === 'login') {
196
+ const name = profile.data.name || email.split('@')[0] || 'Google User';
197
+ const user = await prisma_js_1.prisma.user.upsert({
198
+ where: { email },
199
+ create: { email, name, passwordHash: await (0, password_js_1.hashPassword)((0, crypto_js_1.randomToken)(32)) },
200
+ update: { name },
201
+ });
202
+ const existingAccount = await prisma_js_1.prisma.connectedAccount.findUnique({ where: { userId_provider_providerAccountId: { userId: user.id, provider: 'google_drive', providerAccountId } } });
203
+ const refreshTokenEncrypted = tokens.refresh_token ? (0, crypto_js_1.encryptText)(tokens.refresh_token) : existingAccount?.refreshTokenEncrypted;
204
+ if (!refreshTokenEncrypted) {
205
+ console.error('Google login failed: no refresh token received and no existing account. Has refresh_token:', !!tokens.refresh_token);
206
+ return res.redirect(`${env_js_1.env.FRONTEND_URL}/google-auth?status=error`);
207
+ }
208
+ const account = await prisma_js_1.prisma.connectedAccount.upsert({
209
+ where: { userId_provider_providerAccountId: { userId: user.id, provider: 'google_drive', providerAccountId } },
210
+ create: {
211
+ userId: user.id,
212
+ providerConfigId: oauthState.providerConfigId,
213
+ provider: 'google_drive',
214
+ providerAccountId,
215
+ email,
216
+ displayName: profile.data.name,
217
+ avatarUrl: profile.data.picture,
218
+ accessTokenEncrypted: (0, crypto_js_1.encryptText)(tokens.access_token),
219
+ refreshTokenEncrypted,
220
+ tokenExpiresAt: new Date(tokens.expiry_date ?? Date.now() + 3600_000),
221
+ scopes: oauthState.providerConfig.scopes,
222
+ status: 'connected',
223
+ },
224
+ update: {
225
+ providerConfigId: oauthState.providerConfigId,
226
+ email,
227
+ displayName: profile.data.name,
228
+ avatarUrl: profile.data.picture,
229
+ accessTokenEncrypted: (0, crypto_js_1.encryptText)(tokens.access_token),
230
+ refreshTokenEncrypted,
231
+ tokenExpiresAt: new Date(tokens.expiry_date ?? Date.now() + 3600_000),
232
+ scopes: oauthState.providerConfig.scopes,
233
+ status: 'connected',
234
+ },
235
+ });
236
+ await prisma_js_1.prisma.oauthState.update({ where: { id: oauthState.id }, data: { usedAt: new Date(), userId: user.id } });
237
+ await (0, google_service_js_1.syncGoogleQuota)(account.id).catch(() => undefined);
238
+ const handoffToken = (0, crypto_js_1.randomToken)();
239
+ 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) } });
240
+ return res.redirect(`${env_js_1.env.FRONTEND_URL}/google-auth?token=${handoffToken}`);
241
+ }
242
+ if (oauthState.flow !== 'connect' || !oauthState.userId)
243
+ return res.status(400).json({ code: 'GOOGLE_OAUTH_STATE_INVALID', message: 'OAuth state expired.' });
244
+ const existingAccount = await prisma_js_1.prisma.connectedAccount.findUnique({ where: { userId_provider_providerAccountId: { userId: oauthState.userId, provider: 'google_drive', providerAccountId } } });
245
+ const refreshTokenEncrypted = tokens.refresh_token ? (0, crypto_js_1.encryptText)(tokens.refresh_token) : existingAccount?.refreshTokenEncrypted;
246
+ if (!refreshTokenEncrypted)
247
+ return res.status(400).json({ code: 'GOOGLE_OAUTH_FAILED', message: 'Google did not return required tokens.' });
248
+ const account = await prisma_js_1.prisma.connectedAccount.upsert({
249
+ where: { userId_provider_providerAccountId: { userId: oauthState.userId, provider: 'google_drive', providerAccountId } },
250
+ create: {
251
+ userId: oauthState.userId,
252
+ providerConfigId: oauthState.providerConfigId,
253
+ provider: 'google_drive',
254
+ providerAccountId,
255
+ email,
256
+ displayName: profile.data.name,
257
+ avatarUrl: profile.data.picture,
258
+ accessTokenEncrypted: (0, crypto_js_1.encryptText)(tokens.access_token),
259
+ refreshTokenEncrypted,
260
+ tokenExpiresAt: new Date(tokens.expiry_date ?? Date.now() + 3600_000),
261
+ scopes: oauthState.providerConfig.scopes,
262
+ status: 'connected',
263
+ },
264
+ update: {
265
+ providerConfigId: oauthState.providerConfigId,
266
+ email,
267
+ displayName: profile.data.name,
268
+ avatarUrl: profile.data.picture,
269
+ accessTokenEncrypted: (0, crypto_js_1.encryptText)(tokens.access_token),
270
+ refreshTokenEncrypted,
271
+ tokenExpiresAt: new Date(tokens.expiry_date ?? Date.now() + 3600_000),
272
+ scopes: oauthState.providerConfig.scopes,
273
+ status: 'connected',
274
+ },
275
+ });
276
+ await prisma_js_1.prisma.oauthState.update({ where: { id: oauthState.id }, data: { usedAt: new Date() } });
277
+ await (0, google_service_js_1.syncGoogleQuota)(account.id);
278
+ return res.redirect(`${env_js_1.env.FRONTEND_URL}/google-connected?status=success`);
279
+ }
280
+ catch (error) {
281
+ console.error('Google OAuth callback failed:', error);
282
+ return res.redirect(`${env_js_1.env.FRONTEND_URL}/google-connected?status=error`);
283
+ }
284
+ });
285
+ exports.connectedAccountRouter.post('/:id/sync-quota', auth_middleware_js_1.requireAuth, async (req, res, next) => {
286
+ try {
287
+ const accountId = String(req.params.id);
288
+ const account = await prisma_js_1.prisma.connectedAccount.findFirstOrThrow({ where: { id: accountId, userId: req.user.id } });
289
+ const quota = await syncQuotaForAccount(account);
290
+ return res.json({
291
+ quota: {
292
+ ...quota,
293
+ totalBytes: quota.totalBytes?.toString() ?? null,
294
+ usedBytes: quota.usedBytes.toString(),
295
+ availableBytes: quota.availableBytes?.toString() ?? null,
296
+ trashBytes: quota.trashBytes?.toString() ?? null,
297
+ },
298
+ });
299
+ }
300
+ catch (error) {
301
+ return next(error);
302
+ }
303
+ });
304
+ exports.connectedAccountRouter.delete('/:id', auth_middleware_js_1.requireAuth, async (req, res, next) => {
305
+ try {
306
+ const accountId = String(req.params.id);
307
+ await prisma_js_1.prisma.connectedAccount.updateMany({ where: { id: accountId, userId: req.user.id }, data: { status: 'disconnected' } });
308
+ return res.json({ status: 'ok' });
309
+ }
310
+ catch (error) {
311
+ return next(error);
312
+ }
313
+ });