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.
- package/LICENSE +201 -0
- package/README.md +389 -0
- package/backend/dist/app.js +113 -0
- package/backend/dist/config/env.js +75 -0
- package/backend/dist/config/prisma.js +30 -0
- package/backend/dist/middleware/api-key.middleware.js +38 -0
- package/backend/dist/middleware/auth.middleware.js +21 -0
- package/backend/dist/middleware/error.middleware.js +7 -0
- package/backend/dist/modules/api-keys/api-key.routes.js +90 -0
- package/backend/dist/modules/audit-logs/audit-log.routes.js +20 -0
- package/backend/dist/modules/auth/auth.routes.js +201 -0
- package/backend/dist/modules/cdn/cdn.routes.js +105 -0
- package/backend/dist/modules/connected-accounts/connected-account.routes.js +313 -0
- package/backend/dist/modules/files/file.routes.js +1013 -0
- package/backend/dist/modules/files/stream-file.js +10 -0
- package/backend/dist/modules/files/stream-google-file.js +79 -0
- package/backend/dist/modules/folders/folder.routes.js +546 -0
- package/backend/dist/modules/google/google.service.js +485 -0
- package/backend/dist/modules/invites/invite.routes.js +127 -0
- package/backend/dist/modules/provider-configs/provider-config.routes.js +53 -0
- package/backend/dist/modules/public/public.routes.js +44 -0
- package/backend/dist/modules/public-api/public-api.routes.js +8 -0
- package/backend/dist/modules/s3/s3.service.js +104 -0
- package/backend/dist/modules/storage/storage.routes.js +108 -0
- package/backend/dist/modules/system/system.routes.js +663 -0
- package/backend/dist/modules/uploads/upload.routes.js +569 -0
- package/backend/dist/scripts/debug-query.js +19 -0
- package/backend/dist/scripts/seed-google-config-if-present.js +49 -0
- package/backend/dist/scripts/seed-google-config.js +40 -0
- package/backend/dist/scripts/test-api-upload.js +37 -0
- package/backend/dist/scripts/test-external-upload.js +181 -0
- package/backend/dist/scripts/test-s3-connection.js +33 -0
- package/backend/dist/server.js +7 -0
- package/backend/dist/utils/audit.js +20 -0
- package/backend/dist/utils/crypto.js +33 -0
- package/backend/dist/utils/jwt.js +15 -0
- package/backend/dist/utils/password.js +14 -0
- package/backend/prisma/generated/sqlite-client/client.d.ts +1 -0
- package/backend/prisma/generated/sqlite-client/client.js +5 -0
- package/backend/prisma/generated/sqlite-client/default.d.ts +1 -0
- package/backend/prisma/generated/sqlite-client/default.js +5 -0
- package/backend/prisma/generated/sqlite-client/edge.d.ts +1 -0
- package/backend/prisma/generated/sqlite-client/edge.js +436 -0
- package/backend/prisma/generated/sqlite-client/index-browser.js +422 -0
- package/backend/prisma/generated/sqlite-client/index.d.ts +35178 -0
- package/backend/prisma/generated/sqlite-client/index.js +457 -0
- package/backend/prisma/generated/sqlite-client/package.json +183 -0
- package/backend/prisma/generated/sqlite-client/query_engine-windows.dll.node +0 -0
- package/backend/prisma/generated/sqlite-client/query_engine_bg.js +2 -0
- package/backend/prisma/generated/sqlite-client/query_engine_bg.wasm +0 -0
- package/backend/prisma/generated/sqlite-client/runtime/edge-esm.js +35 -0
- package/backend/prisma/generated/sqlite-client/runtime/edge.js +35 -0
- package/backend/prisma/generated/sqlite-client/runtime/index-browser.d.ts +370 -0
- package/backend/prisma/generated/sqlite-client/runtime/index-browser.js +17 -0
- package/backend/prisma/generated/sqlite-client/runtime/library.d.ts +3982 -0
- package/backend/prisma/generated/sqlite-client/runtime/library.js +147 -0
- package/backend/prisma/generated/sqlite-client/runtime/react-native.js +84 -0
- package/backend/prisma/generated/sqlite-client/runtime/wasm-compiler-edge.js +85 -0
- package/backend/prisma/generated/sqlite-client/runtime/wasm-engine-edge.js +38 -0
- package/backend/prisma/generated/sqlite-client/schema.prisma +354 -0
- package/backend/prisma/generated/sqlite-client/wasm-edge-light-loader.mjs +5 -0
- package/backend/prisma/generated/sqlite-client/wasm-worker-loader.mjs +5 -0
- package/backend/prisma/generated/sqlite-client/wasm.d.ts +1 -0
- package/backend/prisma/generated/sqlite-client/wasm.js +443 -0
- package/backend/prisma/migrations/20260604084325_init/migration.sql +191 -0
- package/backend/prisma/migrations/20260604090020_global_provider_config/migration.sql +8 -0
- package/backend/prisma/migrations/20260604091957_virtual_folders/migration.sql +32 -0
- package/backend/prisma/migrations/20260604095418_nested_folders/migration.sql +8 -0
- package/backend/prisma/migrations/20260604095705_file_shares/migration.sql +22 -0
- package/backend/prisma/migrations/20260604100802_file_preview_tokens/migration.sql +20 -0
- package/backend/prisma/migrations/20260604120000_add_file_share_token/migration.sql +3 -0
- package/backend/prisma/migrations/20260604130000_workspace_invites/migration.sql +17 -0
- package/backend/prisma/migrations/20260604133000_invite_targets/migration.sql +9 -0
- package/backend/prisma/migrations/20260605090000_query_indexes/migration.sql +12 -0
- package/backend/prisma/migrations/20260605100000_folder_icons/migration.sql +1 -0
- package/backend/prisma/migrations/20260605110000_google_auth_handoff/migration.sql +22 -0
- package/backend/prisma/migrations/20260611120000_s3_storage_support/migration.sql +31 -0
- package/backend/prisma/migrations/20260611130000_upload_routing_policy/migration.sql +14 -0
- package/backend/prisma/migrations/20260611140000_api_keys/migration.sql +21 -0
- package/backend/prisma/migrations/20260630030000_upload_session_resumable_fields/migration.sql +9 -0
- package/backend/prisma/migrations/migration_lock.toml +3 -0
- package/backend/prisma/schema.prisma +353 -0
- package/backend/prisma/schema.sqlite.prisma +354 -0
- package/backend/public/assets/index-B4ifIM_G.js +135 -0
- package/backend/public/assets/index-DwOPZrmt.css +2 -0
- package/backend/public/assets/ort-wasm-simd-threaded.jsep-D5Jk56-t.wasm +0 -0
- package/backend/public/assets/ort.bundle.min-CpXgll2e.js +2746 -0
- package/backend/public/assets/ort.bundle.min-OfoG_cy9.mjs +2817 -0
- package/backend/public/assets/ort.webgpu.bundle.min-C1vdBqT7.js +2746 -0
- package/backend/public/assets/ort.webgpu.bundle.min-CEayb2S6.mjs +2817 -0
- package/backend/public/assets/pdf.worker.min-Dswkl-cV.mjs +29 -0
- package/backend/public/favicon.ico +74 -0
- package/backend/public/favicon.svg +74 -0
- package/backend/public/icons.svg +24 -0
- package/backend/public/index.html +34 -0
- package/backend/public/manifest.webmanifest +1 -0
- package/backend/public/maskable-icon.svg +22 -0
- package/backend/public/pwa-192x192.svg +22 -0
- package/backend/public/pwa-512x512.svg +22 -0
- package/backend/public/registerSW.js +1 -0
- package/backend/public/sw.js +1 -0
- package/backend/public/workbox-9c191d2f.js +1 -0
- package/bin/9drive.js +320 -0
- package/package.json +60 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const prisma_js_1 = require("../config/prisma.js");
|
|
4
|
+
const crypto_js_1 = require("../utils/crypto.js");
|
|
5
|
+
const scopes = [
|
|
6
|
+
'https://www.googleapis.com/auth/drive',
|
|
7
|
+
'https://www.googleapis.com/auth/userinfo.email',
|
|
8
|
+
'https://www.googleapis.com/auth/userinfo.profile',
|
|
9
|
+
];
|
|
10
|
+
async function main() {
|
|
11
|
+
const clientId = process.env.GOOGLE_CLIENT_ID;
|
|
12
|
+
const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
|
|
13
|
+
const redirectUri = process.env.GOOGLE_REDIRECT_URI ?? 'http://localhost:4000/connected-accounts/google/callback';
|
|
14
|
+
if (!clientId || !clientSecret)
|
|
15
|
+
throw new Error('GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are required.');
|
|
16
|
+
await prisma_js_1.prisma.providerConfig.updateMany({
|
|
17
|
+
where: { userId: null, provider: 'google_drive', status: 'active' },
|
|
18
|
+
data: { status: 'disabled' },
|
|
19
|
+
});
|
|
20
|
+
const config = await prisma_js_1.prisma.providerConfig.create({
|
|
21
|
+
data: {
|
|
22
|
+
userId: null,
|
|
23
|
+
provider: 'google_drive',
|
|
24
|
+
clientIdEncrypted: (0, crypto_js_1.encryptText)(clientId),
|
|
25
|
+
clientSecretEncrypted: (0, crypto_js_1.encryptText)(clientSecret),
|
|
26
|
+
redirectUri,
|
|
27
|
+
scopes,
|
|
28
|
+
status: 'active',
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
console.log(`Seeded global Google Drive config: ${config.id}`);
|
|
32
|
+
}
|
|
33
|
+
main()
|
|
34
|
+
.catch((error) => {
|
|
35
|
+
console.error(error);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
})
|
|
38
|
+
.finally(async () => {
|
|
39
|
+
await prisma_js_1.prisma.$disconnect();
|
|
40
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const prisma_js_1 = require("../config/prisma.js");
|
|
4
|
+
const s3_service_js_1 = require("../modules/s3/s3.service.js");
|
|
5
|
+
const crypto_js_1 = require("../utils/crypto.js");
|
|
6
|
+
const apiUrl = process.env.API_URL || 'http://localhost:4000';
|
|
7
|
+
async function main() {
|
|
8
|
+
const user = await prisma_js_1.prisma.user.findFirst({ select: { id: true } });
|
|
9
|
+
if (!user)
|
|
10
|
+
throw new Error('No user found. Register a user first.');
|
|
11
|
+
const secret = `9d_live_${(0, crypto_js_1.randomToken)(32)}`;
|
|
12
|
+
const apiKey = await prisma_js_1.prisma.apiKey.create({ data: { userId: user.id, name: 'API upload smoke test', keyPrefix: secret.slice(0, 16), keyHash: (0, crypto_js_1.hashToken)(secret), scopes: ['files:upload'] } });
|
|
13
|
+
const content = 'api upload smoke test';
|
|
14
|
+
const form = new FormData();
|
|
15
|
+
form.append('filesMeta', JSON.stringify([{ fieldName: 'file-0', fileName: 'api-upload-smoke.txt', mimeType: 'text/plain', sizeBytes: String(Buffer.byteLength(content)) }]));
|
|
16
|
+
form.append('file-0', new Blob([content], { type: 'text/plain' }), 'api-upload-smoke.txt');
|
|
17
|
+
const response = await fetch(`${apiUrl}/api/v1/uploads`, { method: 'POST', headers: { Authorization: `Bearer ${secret}` }, body: form });
|
|
18
|
+
const text = await response.text();
|
|
19
|
+
if (!response.ok)
|
|
20
|
+
throw new Error(`Upload failed: ${response.status} ${text}`);
|
|
21
|
+
const data = JSON.parse(text);
|
|
22
|
+
for (const uploaded of data.files ?? []) {
|
|
23
|
+
const file = await prisma_js_1.prisma.file.findUnique({ where: { id: uploaded.id }, include: { connectedAccount: true } });
|
|
24
|
+
if (file?.provider === 's3')
|
|
25
|
+
await (0, s3_service_js_1.deleteS3Object)(file).catch(() => undefined);
|
|
26
|
+
if (file)
|
|
27
|
+
await prisma_js_1.prisma.file.update({ where: { id: file.id }, data: { status: 'deleted', deletedAt: new Date() } });
|
|
28
|
+
}
|
|
29
|
+
await prisma_js_1.prisma.apiKey.update({ where: { id: apiKey.id }, data: { status: 'revoked', revokedAt: new Date() } });
|
|
30
|
+
console.log(text);
|
|
31
|
+
}
|
|
32
|
+
main().catch((error) => {
|
|
33
|
+
console.error(error instanceof Error ? error.message : 'API upload test failed');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}).finally(async () => {
|
|
36
|
+
await prisma_js_1.prisma.$disconnect();
|
|
37
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
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
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const prisma_js_1 = require("../config/prisma.js");
|
|
9
|
+
const crypto_js_1 = require("../utils/crypto.js");
|
|
10
|
+
const apiUrl = process.env.API_URL || 'http://localhost:4000';
|
|
11
|
+
// A minimal valid 1x1 PNG image (red pixel)
|
|
12
|
+
const samplePngBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==';
|
|
13
|
+
const samplePngBuffer = Buffer.from(samplePngBase64, 'base64');
|
|
14
|
+
async function run() {
|
|
15
|
+
console.log('--- [1/6] Finding user & generating API Key for external platform ---');
|
|
16
|
+
const user = await prisma_js_1.prisma.user.findFirst({ select: { id: true, email: true } });
|
|
17
|
+
if (!user)
|
|
18
|
+
throw new Error('No user found in database. Register or seed a user first.');
|
|
19
|
+
const secret = `9d_live_${(0, crypto_js_1.randomToken)(32)}`;
|
|
20
|
+
const apiKey = await prisma_js_1.prisma.apiKey.create({
|
|
21
|
+
data: {
|
|
22
|
+
userId: user.id,
|
|
23
|
+
name: 'External Platform Client Test',
|
|
24
|
+
keyPrefix: secret.slice(0, 16),
|
|
25
|
+
keyHash: (0, crypto_js_1.hashToken)(secret),
|
|
26
|
+
scopes: ['files:upload'],
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
console.log(`Created API Key: ${apiKey.keyPrefix}... for user: ${user.email}`);
|
|
30
|
+
console.log('\n--- [2/6] Simulating External Platform Upload with x-api-key ---');
|
|
31
|
+
const form = new FormData();
|
|
32
|
+
// Directly append file as an external website would
|
|
33
|
+
form.append('file', new Blob([samplePngBuffer], { type: 'image/png' }), 'external-sample-image.png');
|
|
34
|
+
const uploadRes = await fetch(`${apiUrl}/api/v1/uploads`, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: {
|
|
37
|
+
'x-api-key': secret, // Using x-api-key header!
|
|
38
|
+
},
|
|
39
|
+
body: form,
|
|
40
|
+
});
|
|
41
|
+
const uploadText = await uploadRes.text();
|
|
42
|
+
console.log(`Upload HTTP Status: ${uploadRes.status}`);
|
|
43
|
+
console.log(`Upload Response JSON: ${uploadText}`);
|
|
44
|
+
if (!uploadRes.ok) {
|
|
45
|
+
throw new Error(`Upload failed with status ${uploadRes.status}: ${uploadText}`);
|
|
46
|
+
}
|
|
47
|
+
const uploadData = JSON.parse(uploadText);
|
|
48
|
+
if (!uploadData.success || !uploadData.file?.url) {
|
|
49
|
+
throw new Error('Upload response does not contain success: true or file.url');
|
|
50
|
+
}
|
|
51
|
+
const uploadedFile = uploadData.file;
|
|
52
|
+
console.log('\nUploaded file metadata:');
|
|
53
|
+
console.log(` - ID: ${uploadedFile.id}`);
|
|
54
|
+
console.log(` - Name: ${uploadedFile.name}`);
|
|
55
|
+
console.log(` - MimeType: ${uploadedFile.mimeType}`);
|
|
56
|
+
console.log(` - Size: ${uploadedFile.size} bytes`);
|
|
57
|
+
console.log(` - CDN View URL: ${uploadedFile.url}`);
|
|
58
|
+
console.log(` - CDN Raw URL: ${uploadedFile.downloadUrl}`);
|
|
59
|
+
console.log('\n--- [3/6] Verifying GET /cdn/view/:id (Inline CDN Delivery) ---');
|
|
60
|
+
const viewRes = await fetch(uploadedFile.url);
|
|
61
|
+
console.log(`View HTTP Status: ${viewRes.status}`);
|
|
62
|
+
console.log(`View Content-Type: ${viewRes.headers.get('content-type')}`);
|
|
63
|
+
console.log(`View Cache-Control: ${viewRes.headers.get('cache-control')}`);
|
|
64
|
+
console.log(`View CORS Allow-Origin: ${viewRes.headers.get('access-control-allow-origin')}`);
|
|
65
|
+
console.log(`View Content-Disposition: ${viewRes.headers.get('content-disposition')}`);
|
|
66
|
+
console.log(`View ETag: ${viewRes.headers.get('etag')}`);
|
|
67
|
+
if (viewRes.status !== 200) {
|
|
68
|
+
throw new Error(`Expected HTTP 200 from CDN view, got ${viewRes.status}`);
|
|
69
|
+
}
|
|
70
|
+
const cacheControl = viewRes.headers.get('cache-control') || '';
|
|
71
|
+
if (!cacheControl.includes('public') || !cacheControl.includes('max-age=31536000') || !cacheControl.includes('immutable')) {
|
|
72
|
+
throw new Error(`Cache-Control header missing required CDN directives: ${cacheControl}`);
|
|
73
|
+
}
|
|
74
|
+
const corsOrigin = viewRes.headers.get('access-control-allow-origin');
|
|
75
|
+
if (corsOrigin !== '*') {
|
|
76
|
+
throw new Error(`Expected Access-Control-Allow-Origin: *, got ${corsOrigin}`);
|
|
77
|
+
}
|
|
78
|
+
const viewBytes = Buffer.from(await viewRes.arrayBuffer());
|
|
79
|
+
if (viewBytes.length !== samplePngBuffer.length) {
|
|
80
|
+
throw new Error(`Downloaded image size mismatch: expected ${samplePngBuffer.length}, got ${viewBytes.length}`);
|
|
81
|
+
}
|
|
82
|
+
console.log(`[PASS] CDN image delivered inline with correct bytes & headers!`);
|
|
83
|
+
console.log('\n--- [4/6] Verifying Conditional Request (304 Not Modified) ---');
|
|
84
|
+
const etag = viewRes.headers.get('etag');
|
|
85
|
+
if (etag) {
|
|
86
|
+
const conditionalRes = await fetch(uploadedFile.url, {
|
|
87
|
+
headers: { 'if-none-match': etag },
|
|
88
|
+
});
|
|
89
|
+
console.log(`Conditional Request HTTP Status: ${conditionalRes.status}`);
|
|
90
|
+
if (conditionalRes.status !== 304) {
|
|
91
|
+
throw new Error(`Expected 304 Not Modified for matching ETag, got ${conditionalRes.status}`);
|
|
92
|
+
}
|
|
93
|
+
console.log('[PASS] 304 Not Modified correctly returned for ETag cache hit!');
|
|
94
|
+
}
|
|
95
|
+
console.log('\n--- [5/6] Verifying GET /cdn/raw/:id (Attachment Download) ---');
|
|
96
|
+
const rawRes = await fetch(uploadedFile.downloadUrl);
|
|
97
|
+
console.log(`Raw HTTP Status: ${rawRes.status}`);
|
|
98
|
+
console.log(`Raw Content-Disposition: ${rawRes.headers.get('content-disposition')}`);
|
|
99
|
+
if (!rawRes.headers.get('content-disposition')?.includes('attachment;')) {
|
|
100
|
+
throw new Error('Raw route did not return attachment Content-Disposition');
|
|
101
|
+
}
|
|
102
|
+
console.log('[PASS] Raw download route verified!');
|
|
103
|
+
console.log('\n--- [6/6] Generating Demo HTML Page for Visual In-Browser Verification ---');
|
|
104
|
+
const htmlContent = `<!DOCTYPE html>
|
|
105
|
+
<html lang="id">
|
|
106
|
+
<head>
|
|
107
|
+
<meta charset="UTF-8" />
|
|
108
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
109
|
+
<title>9Drive Object Storage & Media Gateway - Demo Preview</title>
|
|
110
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
111
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
112
|
+
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
113
|
+
<style>
|
|
114
|
+
* { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Plus Jakarta Sans', sans-serif; }
|
|
115
|
+
body { background: #0b0f19; color: #f3f4f6; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 24px; }
|
|
116
|
+
.card { background: rgba(17, 24, 39, 0.8); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 16px; padding: 32px; max-width: 640px; width: 100%; box-shadow: 0 20px 40px rgba(0,0,0,0.5); backdrop-filter: blur(12px); }
|
|
117
|
+
.badge { display: inline-block; background: rgba(16, 185, 129, 0.15); color: #10b981; font-size: 12px; font-weight: 600; padding: 4px 12px; border-radius: 9999px; margin-bottom: 16px; border: 1px solid rgba(16, 185, 129, 0.3); }
|
|
118
|
+
h1 { font-size: 24px; font-weight: 700; margin-bottom: 8px; color: #ffffff; }
|
|
119
|
+
p { color: #9ca3af; font-size: 14px; margin-bottom: 24px; line-height: 1.6; }
|
|
120
|
+
.preview-box { background: #030712; border: 2px dashed rgba(255, 255, 255, 0.15); border-radius: 12px; padding: 24px; text-align: center; margin-bottom: 24px; }
|
|
121
|
+
.preview-box img { max-width: 100%; max-height: 240px; border-radius: 8px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); image-rendering: pixelated; width: 120px; height: 120px; border: 2px solid #3b82f6; }
|
|
122
|
+
.preview-label { font-size: 12px; color: #6b7280; margin-top: 12px; }
|
|
123
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; margin-bottom: 24px; }
|
|
124
|
+
th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid rgba(255, 255, 255, 0.05); }
|
|
125
|
+
th { color: #9ca3af; font-weight: 500; width: 35%; }
|
|
126
|
+
td { color: #e5e7eb; word-break: break-all; font-family: monospace; font-size: 12px; }
|
|
127
|
+
.btn-group { display: flex; gap: 12px; }
|
|
128
|
+
.btn { flex: 1; text-align: center; padding: 12px 18px; border-radius: 8px; font-weight: 600; font-size: 14px; text-decoration: none; transition: all 0.2s; cursor: pointer; }
|
|
129
|
+
.btn-primary { background: #2563eb; color: #ffffff; }
|
|
130
|
+
.btn-primary:hover { background: #1d4ed8; }
|
|
131
|
+
.btn-secondary { background: rgba(255, 255, 255, 0.05); color: #d1d5db; border: 1px solid rgba(255, 255, 255, 0.1); }
|
|
132
|
+
.btn-secondary:hover { background: rgba(255, 255, 255, 0.1); }
|
|
133
|
+
</style>
|
|
134
|
+
</head>
|
|
135
|
+
<body>
|
|
136
|
+
<div class="card">
|
|
137
|
+
<div class="badge">✓ Local CDN Gateway Live</div>
|
|
138
|
+
<h1>9Drive Media Gateway</h1>
|
|
139
|
+
<p>File ini diunggah via <code>POST /api/v1/uploads</code> menggunakan <code>x-api-key</code> dan langsung didistribusikan melalui route CDN baru:</p>
|
|
140
|
+
|
|
141
|
+
<div class="preview-box">
|
|
142
|
+
<!-- Direct CDN delivery route rendered via <img> tag -->
|
|
143
|
+
<img src="${uploadedFile.url}" alt="Uploaded via 9Drive Object Storage" />
|
|
144
|
+
<div class="preview-label"><img src="${uploadedFile.url}" /></div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<table>
|
|
148
|
+
<tr><th>File ID</th><td>${uploadedFile.id}</td></tr>
|
|
149
|
+
<tr><th>File Name</th><td>${uploadedFile.name}</td></tr>
|
|
150
|
+
<tr><th>Mime Type</th><td>${uploadedFile.mimeType}</td></tr>
|
|
151
|
+
<tr><th>File Size</th><td>${uploadedFile.size} bytes</td></tr>
|
|
152
|
+
<tr><th>Storage Provider</th><td>${uploadedFile.provider}</td></tr>
|
|
153
|
+
<tr><th>Cache-Control</th><td>public, max-age=31536000, immutable</td></tr>
|
|
154
|
+
<tr><th>CORS Header</th><td>Access-Control-Allow-Origin: *</td></tr>
|
|
155
|
+
<tr><th>CDN View URL</th><td><a href="${uploadedFile.url}" target="_blank" style="color: #60a5fa;">${uploadedFile.url}</a></td></tr>
|
|
156
|
+
<tr><th>CDN Raw URL</th><td><a href="${uploadedFile.downloadUrl}" target="_blank" style="color: #60a5fa;">${uploadedFile.downloadUrl}</a></td></tr>
|
|
157
|
+
</table>
|
|
158
|
+
|
|
159
|
+
<div class="btn-group">
|
|
160
|
+
<a href="${uploadedFile.url}" target="_blank" class="btn btn-primary">Buka CDN View</a>
|
|
161
|
+
<a href="${uploadedFile.downloadUrl}" target="_blank" class="btn btn-secondary">Unduh File (Raw)</a>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</body>
|
|
165
|
+
</html>`;
|
|
166
|
+
const htmlPath = node_path_1.default.resolve(process.cwd(), '..', 'test-cdn-preview.html');
|
|
167
|
+
node_fs_1.default.writeFileSync(htmlPath, htmlContent, 'utf-8');
|
|
168
|
+
console.log(`\nDemo HTML file written to: ${htmlPath}`);
|
|
169
|
+
console.log('You can open this HTML file in your browser to inspect the rendered image!');
|
|
170
|
+
console.log('\n======================================================');
|
|
171
|
+
console.log('🎉 ALL END-TO-END VERIFICATION CHECKS PASSED!');
|
|
172
|
+
console.log('======================================================');
|
|
173
|
+
}
|
|
174
|
+
run()
|
|
175
|
+
.catch((err) => {
|
|
176
|
+
console.error('\n❌ Test failed:', err);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
})
|
|
179
|
+
.finally(async () => {
|
|
180
|
+
await prisma_js_1.prisma.$disconnect();
|
|
181
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
4
|
+
const prisma_js_1 = require("../config/prisma.js");
|
|
5
|
+
const s3_service_js_1 = require("../modules/s3/s3.service.js");
|
|
6
|
+
const accountId = process.argv[2];
|
|
7
|
+
async function main() {
|
|
8
|
+
const config = await prisma_js_1.prisma.s3StorageConfig.findFirst({
|
|
9
|
+
where: { status: 'active', ...(accountId ? { connectedAccountId: accountId } : {}) },
|
|
10
|
+
orderBy: { createdAt: 'desc' },
|
|
11
|
+
});
|
|
12
|
+
if (!config) {
|
|
13
|
+
console.error(JSON.stringify({ ok: false, message: 'No active S3 config found. Connect S3 from /settings first.' }));
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
await (0, s3_service_js_1.testS3Connection)(config);
|
|
18
|
+
const listed = await (0, s3_service_js_1.createS3Client)(config).send(new client_s3_1.ListObjectsV2Command({ Bucket: config.bucket, Prefix: config.prefix, MaxKeys: 1 }));
|
|
19
|
+
console.log(JSON.stringify({ ok: true, accountId: config.connectedAccountId, bucket: config.bucket, endpoint: config.endpoint, region: config.region, prefix: config.prefix, keyCount: listed.KeyCount ?? 0, isTruncated: listed.IsTruncated ?? false }));
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
console.error(JSON.stringify({ ok: false, accountId: config.connectedAccountId, bucket: config.bucket, endpoint: config.endpoint, name: error instanceof Error ? error.name : 'Error', message: error instanceof Error ? error.message : 'S3 connection failed' }));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
await prisma_js_1.prisma.$disconnect();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
main().catch(async (error) => {
|
|
30
|
+
console.error(JSON.stringify({ ok: false, message: error instanceof Error ? error.message : 'S3 connection test failed' }));
|
|
31
|
+
await prisma_js_1.prisma.$disconnect();
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const app_js_1 = require("./app.js");
|
|
4
|
+
const env_js_1 = require("./config/env.js");
|
|
5
|
+
app_js_1.app.listen(env_js_1.env.APP_PORT, () => {
|
|
6
|
+
console.log(`Backend running on http://localhost:${env_js_1.env.APP_PORT}`);
|
|
7
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createAuditLog = createAuditLog;
|
|
4
|
+
const prisma_js_1 = require("../config/prisma.js");
|
|
5
|
+
async function createAuditLog(userId, action, entityType, entityId, metadata) {
|
|
6
|
+
try {
|
|
7
|
+
await prisma_js_1.prisma.auditLog.create({
|
|
8
|
+
data: {
|
|
9
|
+
userId,
|
|
10
|
+
action,
|
|
11
|
+
entityType,
|
|
12
|
+
entityId,
|
|
13
|
+
metadata: metadata ? JSON.stringify(metadata) : undefined
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
console.error('Failed to create audit log:', error);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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.encryptText = encryptText;
|
|
7
|
+
exports.decryptText = decryptText;
|
|
8
|
+
exports.randomToken = randomToken;
|
|
9
|
+
exports.hashToken = hashToken;
|
|
10
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
11
|
+
const env_js_1 = require("../config/env.js");
|
|
12
|
+
const key = node_crypto_1.default.createHash('sha256').update(env_js_1.env.TOKEN_ENCRYPTION_KEY).digest();
|
|
13
|
+
function encryptText(value) {
|
|
14
|
+
const iv = node_crypto_1.default.randomBytes(12);
|
|
15
|
+
const cipher = node_crypto_1.default.createCipheriv('aes-256-gcm', key, iv);
|
|
16
|
+
const encrypted = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
|
|
17
|
+
const tag = cipher.getAuthTag();
|
|
18
|
+
return `${iv.toString('base64')}:${tag.toString('base64')}:${encrypted.toString('base64')}`;
|
|
19
|
+
}
|
|
20
|
+
function decryptText(value) {
|
|
21
|
+
const [ivRaw, tagRaw, encryptedRaw] = value.split(':');
|
|
22
|
+
if (!ivRaw || !tagRaw || !encryptedRaw)
|
|
23
|
+
throw new Error('Invalid encrypted payload');
|
|
24
|
+
const decipher = node_crypto_1.default.createDecipheriv('aes-256-gcm', key, Buffer.from(ivRaw, 'base64'));
|
|
25
|
+
decipher.setAuthTag(Buffer.from(tagRaw, 'base64'));
|
|
26
|
+
return Buffer.concat([decipher.update(Buffer.from(encryptedRaw, 'base64')), decipher.final()]).toString('utf8');
|
|
27
|
+
}
|
|
28
|
+
function randomToken(bytes = 32) {
|
|
29
|
+
return node_crypto_1.default.randomBytes(bytes).toString('base64url');
|
|
30
|
+
}
|
|
31
|
+
function hashToken(token) {
|
|
32
|
+
return node_crypto_1.default.createHash('sha256').update(token).digest('hex');
|
|
33
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
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.signAccessToken = signAccessToken;
|
|
7
|
+
exports.verifyAccessToken = verifyAccessToken;
|
|
8
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
9
|
+
const env_js_1 = require("../config/env.js");
|
|
10
|
+
function signAccessToken(payload) {
|
|
11
|
+
return jsonwebtoken_1.default.sign(payload, env_js_1.env.JWT_ACCESS_SECRET, { expiresIn: env_js_1.env.ACCESS_TOKEN_TTL_SECONDS });
|
|
12
|
+
}
|
|
13
|
+
function verifyAccessToken(token) {
|
|
14
|
+
return jsonwebtoken_1.default.verify(token, env_js_1.env.JWT_ACCESS_SECRET);
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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.hashPassword = hashPassword;
|
|
7
|
+
exports.verifyPassword = verifyPassword;
|
|
8
|
+
const argon2_1 = __importDefault(require("argon2"));
|
|
9
|
+
function hashPassword(password) {
|
|
10
|
+
return argon2_1.default.hash(password);
|
|
11
|
+
}
|
|
12
|
+
function verifyPassword(hash, password) {
|
|
13
|
+
return argon2_1.default.verify(hash, password);
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./index"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./index"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./default"
|