@progalaxyelabs/stonescriptphp-files 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ProGalaxy eLabs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # @progalaxyelabs/stonescriptphp-files
2
+
3
+ Shared file server for ProGalaxy E-Labs platforms — Azure Blob Storage + JWT auth
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install express @progalaxyelabs/stonescriptphp-files
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```javascript
14
+ import { createFilesServer } from '@progalaxyelabs/stonescriptphp-files';
15
+
16
+ // Zero-config setup (uses environment variables)
17
+ createFilesServer().listen();
18
+ ```
19
+
20
+ ## Environment Variables
21
+
22
+ ```bash
23
+ AZURE_STORAGE_CONNECTION_STRING=your_connection_string
24
+ AZURE_CONTAINER_NAME=platform-files # optional, defaults to 'platform-files'
25
+ JWT_PUBLIC_KEY=your_jwt_public_key
26
+ PORT=3000 # optional, defaults to 3000
27
+ ```
28
+
29
+ ## Advanced Configuration
30
+
31
+ ```javascript
32
+ import { createFilesServer } from '@progalaxyelabs/stonescriptphp-files';
33
+
34
+ const server = createFilesServer({
35
+ port: 3000,
36
+ containerName: 'my-files',
37
+ azureConnectionString: 'your_connection_string',
38
+ jwtPublicKey: 'your_public_key',
39
+ maxFileSize: 100 * 1024 * 1024, // 100MB
40
+ corsOrigins: '*' // or ['https://example.com']
41
+ });
42
+
43
+ server.listen();
44
+ ```
45
+
46
+ ## API Endpoints
47
+
48
+ ### POST /upload
49
+ Upload a file (requires JWT authentication)
50
+
51
+ **Request:** multipart/form-data with `file` field
52
+ **Response:**
53
+ ```json
54
+ {
55
+ "success": true,
56
+ "file": {
57
+ "id": "uuid-here",
58
+ "name": "filename.pdf",
59
+ "contentType": "application/pdf",
60
+ "size": 12345,
61
+ "uploadedAt": "2025-02-07T12:00:00.000Z"
62
+ }
63
+ }
64
+ ```
65
+
66
+ ### GET /files/:id
67
+ Download a file (requires JWT authentication, validates ownership)
68
+
69
+ ### GET /files
70
+ List all files for authenticated user
71
+
72
+ ### DELETE /files/:id
73
+ Delete a file (requires JWT authentication, validates ownership)
74
+
75
+ ### GET /health
76
+ Health check endpoint (no authentication required)
77
+
78
+ ## Composable Usage
79
+
80
+ ```javascript
81
+ import express from 'express';
82
+ import {
83
+ AzureStorageClient,
84
+ createAuthMiddleware,
85
+ createUploadRouter,
86
+ createDownloadRouter
87
+ } from '@progalaxyelabs/stonescriptphp-files';
88
+
89
+ const app = express();
90
+ const storage = new AzureStorageClient(connectionString, containerName);
91
+ const authenticate = createAuthMiddleware(publicKey);
92
+
93
+ await storage.initialize();
94
+
95
+ app.use(authenticate, createUploadRouter(storage));
96
+ app.use(authenticate, createDownloadRouter(storage));
97
+
98
+ app.listen(3000);
99
+ ```
100
+
101
+ ## Features
102
+
103
+ - ✅ Azure Blob Storage integration
104
+ - ✅ JWT Bearer token authentication (RS256/ES256/HS256)
105
+ - ✅ File ownership validation
106
+ - ✅ Streaming uploads and downloads
107
+ - ✅ Configurable file size limits
108
+ - ✅ CORS support
109
+ - ✅ Graceful shutdown handling
110
+ - ✅ Health check endpoint
111
+ - ✅ ESM module support
112
+
113
+ ## Requirements
114
+
115
+ - Node.js >=24.0.0
116
+ - Express ^5.0.0 (peer dependency)
117
+
118
+ ## License
119
+
120
+ MIT
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@progalaxyelabs/stonescriptphp-files",
3
+ "version": "1.0.0",
4
+ "description": "Shared file server for ProGalaxy E-Labs platforms — Azure Blob Storage + JWT auth",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=24.0.0"
12
+ },
13
+ "peerDependencies": {
14
+ "express": "^5.0.0"
15
+ },
16
+ "dependencies": {
17
+ "@azure/storage-blob": "^12.24.0",
18
+ "multer": "^1.4.5-lts.1",
19
+ "jsonwebtoken": "^9.0.2",
20
+ "cors": "^2.8.5",
21
+ "uuid": "^11.0.5"
22
+ },
23
+ "keywords": [
24
+ "azure",
25
+ "blob-storage",
26
+ "file-upload",
27
+ "jwt",
28
+ "express"
29
+ ],
30
+ "author": "ProGalaxy E-Labs <info@progalaxyelabs.com> (https://www.progalaxyelabs.com)",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/progalaxyelabs/stonescriptphp-files.git"
35
+ },
36
+ "homepage": "https://github.com/progalaxyelabs/stonescriptphp-files#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/progalaxyelabs/stonescriptphp-files/issues"
39
+ }
40
+ }
package/src/auth.js ADDED
@@ -0,0 +1,103 @@
1
+ import jwt from 'jsonwebtoken';
2
+
3
+ /**
4
+ * JWT validation middleware factory
5
+ * Creates a middleware that validates JWT tokens and extracts user_id for authorization
6
+ *
7
+ * @param {string} publicKey - JWT public key for token verification (RS256/ES256/HS256)
8
+ * @returns {Function} Express middleware function
9
+ */
10
+ export function createAuthMiddleware(publicKey) {
11
+ if (!publicKey) {
12
+ throw new Error('JWT public key is required');
13
+ }
14
+
15
+ return function authenticateJWT(req, res, next) {
16
+ const authHeader = req.headers.authorization;
17
+
18
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
19
+ return res.status(401).json({
20
+ error: 'Unauthorized',
21
+ message: 'Missing or invalid authorization header'
22
+ });
23
+ }
24
+
25
+ const token = authHeader.substring(7); // Remove 'Bearer ' prefix
26
+
27
+ try {
28
+ // Verify token with public key
29
+ const decoded = jwt.verify(token, publicKey, {
30
+ algorithms: ['RS256', 'ES256', 'HS256']
31
+ });
32
+
33
+ // Extract user_id from token
34
+ // Common JWT claims: sub, user_id, userId, id
35
+ const userId = decoded.sub || decoded.user_id || decoded.userId || decoded.id;
36
+
37
+ if (!userId) {
38
+ return res.status(401).json({
39
+ error: 'Unauthorized',
40
+ message: 'Token missing user identifier'
41
+ });
42
+ }
43
+
44
+ // Attach user info to request
45
+ req.user = {
46
+ id: userId,
47
+ email: decoded.email,
48
+ roles: decoded.roles || [],
49
+ ...decoded
50
+ };
51
+
52
+ next();
53
+ } catch (error) {
54
+ console.error('JWT validation error:', error.message);
55
+
56
+ if (error.name === 'TokenExpiredError') {
57
+ return res.status(401).json({
58
+ error: 'Unauthorized',
59
+ message: 'Token expired'
60
+ });
61
+ }
62
+
63
+ if (error.name === 'JsonWebTokenError') {
64
+ return res.status(401).json({
65
+ error: 'Unauthorized',
66
+ message: 'Invalid token'
67
+ });
68
+ }
69
+
70
+ return res.status(401).json({
71
+ error: 'Unauthorized',
72
+ message: 'Token validation failed'
73
+ });
74
+ }
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Optional: Role-based authorization middleware factory
80
+ * Use after authenticateJWT to check for specific roles
81
+ */
82
+ export function requireRole(...allowedRoles) {
83
+ return (req, res, next) => {
84
+ if (!req.user) {
85
+ return res.status(401).json({
86
+ error: 'Unauthorized',
87
+ message: 'Authentication required'
88
+ });
89
+ }
90
+
91
+ const userRoles = req.user.roles || [];
92
+ const hasRole = allowedRoles.some(role => userRoles.includes(role));
93
+
94
+ if (!hasRole) {
95
+ return res.status(403).json({
96
+ error: 'Forbidden',
97
+ message: 'Insufficient permissions'
98
+ });
99
+ }
100
+
101
+ next();
102
+ };
103
+ }
@@ -0,0 +1,200 @@
1
+ import { BlobServiceClient } from '@azure/storage-blob';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+
4
+ /**
5
+ * Azure Blob Storage operations
6
+ * Handles upload, download, list, and delete operations
7
+ */
8
+ export class AzureStorageClient {
9
+ constructor(connectionString, containerName = 'platform-files') {
10
+ if (!connectionString) {
11
+ throw new Error('Azure Storage connection string is required');
12
+ }
13
+
14
+ this.connectionString = connectionString;
15
+ this.containerName = containerName;
16
+ this.blobServiceClient = null;
17
+ this.containerClient = null;
18
+ }
19
+
20
+ /**
21
+ * Initialize Azure Blob Storage client
22
+ */
23
+ async initialize() {
24
+ try {
25
+ // Create BlobServiceClient
26
+ this.blobServiceClient = BlobServiceClient.fromConnectionString(this.connectionString);
27
+
28
+ // Get container client
29
+ this.containerClient = this.blobServiceClient.getContainerClient(this.containerName);
30
+
31
+ // Create container if it doesn't exist
32
+ await this.containerClient.createIfNotExists({
33
+ access: 'private' // CRITICAL: No public access
34
+ });
35
+
36
+ console.log(`Azure Blob Storage initialized: container=${this.containerName}`);
37
+ } catch (error) {
38
+ console.error('Failed to initialize Azure Blob Storage:', error);
39
+ throw error;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Upload file to Azure Blob Storage
45
+ * @param {string} userId - User ID (for namespacing)
46
+ * @param {Buffer} fileBuffer - File content
47
+ * @param {string} originalFilename - Original filename
48
+ * @param {string} contentType - MIME type
49
+ * @returns {Object} File metadata
50
+ */
51
+ async uploadFile(userId, fileBuffer, originalFilename, contentType) {
52
+ if (!this.containerClient) {
53
+ throw new Error('Azure Storage not initialized');
54
+ }
55
+
56
+ // Generate unique blob name: {user_id}/{uuid}.{ext}
57
+ const fileId = uuidv4();
58
+ const extension = originalFilename.split('.').pop();
59
+ const blobName = `${userId}/${fileId}.${extension}`;
60
+
61
+ const blockBlobClient = this.containerClient.getBlockBlobClient(blobName);
62
+
63
+ // Upload with metadata
64
+ await blockBlobClient.upload(fileBuffer, fileBuffer.length, {
65
+ blobHTTPHeaders: {
66
+ blobContentType: contentType
67
+ },
68
+ metadata: {
69
+ user_id: userId,
70
+ original_filename: originalFilename,
71
+ content_type: contentType,
72
+ uploaded_at: new Date().toISOString(),
73
+ file_id: fileId
74
+ }
75
+ });
76
+
77
+ return {
78
+ fileId,
79
+ blobName,
80
+ userId,
81
+ originalFilename,
82
+ contentType,
83
+ size: fileBuffer.length,
84
+ uploadedAt: new Date().toISOString()
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Download file from Azure Blob Storage
90
+ * @param {string} fileId - File ID (UUID)
91
+ * @param {string} userId - User ID (for authorization)
92
+ * @returns {Object} { stream, metadata }
93
+ */
94
+ async downloadFile(fileId, userId) {
95
+ if (!this.containerClient) {
96
+ throw new Error('Azure Storage not initialized');
97
+ }
98
+
99
+ // List blobs with file_id metadata to find the blob
100
+ const blobs = this.containerClient.listBlobsFlat({
101
+ prefix: `${userId}/`,
102
+ includeMetadata: true
103
+ });
104
+
105
+ for await (const blob of blobs) {
106
+ if (blob.metadata && blob.metadata.file_id === fileId) {
107
+ const blockBlobClient = this.containerClient.getBlockBlobClient(blob.name);
108
+
109
+ // Verify ownership
110
+ if (blob.metadata.user_id !== userId) {
111
+ throw new Error('Unauthorized: File belongs to different user');
112
+ }
113
+
114
+ // Download blob
115
+ const downloadResponse = await blockBlobClient.download(0);
116
+
117
+ return {
118
+ stream: downloadResponse.readableStreamBody,
119
+ metadata: {
120
+ fileName: blob.metadata.original_filename,
121
+ contentType: blob.metadata.content_type,
122
+ size: blob.properties.contentLength
123
+ }
124
+ };
125
+ }
126
+ }
127
+
128
+ throw new Error('File not found');
129
+ }
130
+
131
+ /**
132
+ * List user's files
133
+ * @param {string} userId - User ID
134
+ * @returns {Array} List of file metadata
135
+ */
136
+ async listFiles(userId) {
137
+ if (!this.containerClient) {
138
+ throw new Error('Azure Storage not initialized');
139
+ }
140
+
141
+ const files = [];
142
+ const blobs = this.containerClient.listBlobsFlat({
143
+ prefix: `${userId}/`,
144
+ includeMetadata: true
145
+ });
146
+
147
+ for await (const blob of blobs) {
148
+ if (blob.metadata) {
149
+ files.push({
150
+ fileId: blob.metadata.file_id,
151
+ fileName: blob.metadata.original_filename,
152
+ contentType: blob.metadata.content_type,
153
+ size: blob.properties.contentLength,
154
+ uploadedAt: blob.metadata.uploaded_at
155
+ });
156
+ }
157
+ }
158
+
159
+ return files;
160
+ }
161
+
162
+ /**
163
+ * Delete file from Azure Blob Storage
164
+ * @param {string} fileId - File ID (UUID)
165
+ * @param {string} userId - User ID (for authorization)
166
+ */
167
+ async deleteFile(fileId, userId) {
168
+ if (!this.containerClient) {
169
+ throw new Error('Azure Storage not initialized');
170
+ }
171
+
172
+ // Find blob by file_id
173
+ const blobs = this.containerClient.listBlobsFlat({
174
+ prefix: `${userId}/`,
175
+ includeMetadata: true
176
+ });
177
+
178
+ for await (const blob of blobs) {
179
+ if (blob.metadata && blob.metadata.file_id === fileId) {
180
+ // Verify ownership
181
+ if (blob.metadata.user_id !== userId) {
182
+ throw new Error('Unauthorized: File belongs to different user');
183
+ }
184
+
185
+ const blockBlobClient = this.containerClient.getBlockBlobClient(blob.name);
186
+ await blockBlobClient.delete();
187
+ return;
188
+ }
189
+ }
190
+
191
+ throw new Error('File not found');
192
+ }
193
+
194
+ /**
195
+ * Get container client (for advanced operations)
196
+ */
197
+ getContainerClient() {
198
+ return this.containerClient;
199
+ }
200
+ }
package/src/index.js ADDED
@@ -0,0 +1,114 @@
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import { AzureStorageClient } from './azure-storage.js';
4
+ import { createAuthMiddleware } from './auth.js';
5
+ import { createUploadRouter } from './routes/upload.js';
6
+ import { createDownloadRouter } from './routes/download.js';
7
+ import { createListRouter } from './routes/list.js';
8
+ import { createDeleteRouter } from './routes/delete.js';
9
+ import { createHealthRouter } from './routes/health.js';
10
+
11
+ /**
12
+ * Create a configured files server
13
+ * @param {Object} config - Configuration options
14
+ * @param {number} config.port - Server port (default: process.env.PORT || 3000)
15
+ * @param {string} config.containerName - Azure container name (default: process.env.AZURE_CONTAINER_NAME || 'platform-files')
16
+ * @param {string} config.azureConnectionString - Azure storage connection string (default: process.env.AZURE_STORAGE_CONNECTION_STRING)
17
+ * @param {string} config.jwtPublicKey - JWT public key for auth (default: process.env.JWT_PUBLIC_KEY)
18
+ * @param {number} config.maxFileSize - Max file size in bytes (default: 100MB)
19
+ * @param {string|string[]} config.corsOrigins - CORS allowed origins (default: '*')
20
+ * @returns {Object} Server object with app, storage, and listen() method
21
+ */
22
+ export function createFilesServer(config = {}) {
23
+ const port = config.port || process.env.PORT || 3000;
24
+ const containerName = config.containerName || process.env.AZURE_CONTAINER_NAME || 'platform-files';
25
+ const connectionString = config.azureConnectionString || process.env.AZURE_STORAGE_CONNECTION_STRING;
26
+ const jwtPublicKey = config.jwtPublicKey || process.env.JWT_PUBLIC_KEY;
27
+ const maxFileSize = config.maxFileSize || 100 * 1024 * 1024;
28
+ const corsOrigins = config.corsOrigins || '*';
29
+
30
+ // Create storage client
31
+ const storage = new AzureStorageClient(connectionString, containerName);
32
+
33
+ // Create auth middleware
34
+ const authenticate = createAuthMiddleware(jwtPublicKey);
35
+
36
+ // Create Express app
37
+ const app = express();
38
+
39
+ // Configure CORS
40
+ app.use(cors({ origin: corsOrigins }));
41
+
42
+ // Body parsing middleware
43
+ app.use(express.json());
44
+ app.use(express.urlencoded({ extended: true }));
45
+
46
+ // Request logging
47
+ app.use((req, res, next) => {
48
+ console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
49
+ next();
50
+ });
51
+
52
+ // Routes
53
+ app.use(createHealthRouter());
54
+ app.use(authenticate, createUploadRouter(storage, maxFileSize));
55
+ app.use(authenticate, createDownloadRouter(storage));
56
+ app.use(authenticate, createListRouter(storage));
57
+ app.use(authenticate, createDeleteRouter(storage));
58
+
59
+ // 404 handler
60
+ app.use((req, res) => {
61
+ res.status(404).json({
62
+ error: 'Not Found',
63
+ message: 'The requested endpoint does not exist'
64
+ });
65
+ });
66
+
67
+ // Error handler
68
+ app.use((err, req, res, next) => {
69
+ console.error('Unhandled error:', err);
70
+ res.status(500).json({
71
+ error: 'Internal Server Error',
72
+ message: 'An unexpected error occurred'
73
+ });
74
+ });
75
+
76
+ // Server object with listen method
77
+ const server = {
78
+ app,
79
+ storage,
80
+ listen: () => {
81
+ return storage.initialize().then(() => {
82
+ const httpServer = app.listen(port, () => {
83
+ console.log(`Files server listening on port ${port}`);
84
+ console.log(`Health check: http://localhost:${port}/health`);
85
+ });
86
+
87
+ // Graceful shutdown
88
+ const shutdown = () => {
89
+ console.log('Shutting down gracefully...');
90
+ httpServer.close(() => {
91
+ console.log('Server closed');
92
+ process.exit(0);
93
+ });
94
+ };
95
+
96
+ process.on('SIGTERM', shutdown);
97
+ process.on('SIGINT', shutdown);
98
+
99
+ return httpServer;
100
+ });
101
+ }
102
+ };
103
+
104
+ return server;
105
+ }
106
+
107
+ // Named exports for advanced/composable usage
108
+ export { AzureStorageClient } from './azure-storage.js';
109
+ export { createAuthMiddleware } from './auth.js';
110
+ export { createUploadRouter } from './routes/upload.js';
111
+ export { createDownloadRouter } from './routes/download.js';
112
+ export { createListRouter } from './routes/list.js';
113
+ export { createDeleteRouter } from './routes/delete.js';
114
+ export { createHealthRouter } from './routes/health.js';
@@ -0,0 +1,63 @@
1
+ import express from 'express';
2
+
3
+ /**
4
+ * Delete file router factory
5
+ * @param {AzureStorageClient} storage - Azure storage client instance
6
+ * @returns {express.Router} Express router for delete endpoint
7
+ */
8
+ export function createDeleteRouter(storage) {
9
+ const router = express.Router();
10
+
11
+ /**
12
+ * DELETE /files/:id
13
+ * Delete file from Azure Blob Storage
14
+ * Requires: JWT authentication (applied by parent app)
15
+ * Validates: User owns the file
16
+ */
17
+ router.delete('/files/:id', async (req, res) => {
18
+ try {
19
+ const fileId = req.params.id;
20
+ const userId = req.user.id;
21
+
22
+ // Validate file ID format (UUID)
23
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
24
+ if (!uuidRegex.test(fileId)) {
25
+ return res.status(400).json({
26
+ error: 'Bad Request',
27
+ message: 'Invalid file ID format'
28
+ });
29
+ }
30
+
31
+ // Delete file from Azure (validates ownership)
32
+ await storage.deleteFile(fileId, userId);
33
+
34
+ res.status(200).json({
35
+ success: true,
36
+ message: 'File deleted successfully'
37
+ });
38
+ } catch (error) {
39
+ console.error('Delete error:', error);
40
+
41
+ if (error.message === 'File not found') {
42
+ return res.status(404).json({
43
+ error: 'Not Found',
44
+ message: 'File not found'
45
+ });
46
+ }
47
+
48
+ if (error.message.startsWith('Unauthorized:')) {
49
+ return res.status(403).json({
50
+ error: 'Forbidden',
51
+ message: error.message
52
+ });
53
+ }
54
+
55
+ res.status(500).json({
56
+ error: 'Internal Server Error',
57
+ message: 'Failed to delete file'
58
+ });
59
+ }
60
+ });
61
+
62
+ return router;
63
+ }
@@ -0,0 +1,77 @@
1
+ import express from 'express';
2
+
3
+ /**
4
+ * Download router factory
5
+ * @param {AzureStorageClient} storage - Azure storage client instance
6
+ * @returns {express.Router} Express router for download endpoint
7
+ */
8
+ export function createDownloadRouter(storage) {
9
+ const router = express.Router();
10
+
11
+ /**
12
+ * GET /files/:id
13
+ * Download file from Azure Blob Storage
14
+ * Requires: JWT authentication (applied by parent app)
15
+ * Validates: User owns the file
16
+ */
17
+ router.get('/files/:id', async (req, res) => {
18
+ try {
19
+ const fileId = req.params.id;
20
+ const userId = req.user.id;
21
+
22
+ // Validate file ID format (UUID)
23
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
24
+ if (!uuidRegex.test(fileId)) {
25
+ return res.status(400).json({
26
+ error: 'Bad Request',
27
+ message: 'Invalid file ID format'
28
+ });
29
+ }
30
+
31
+ // Download file from Azure (validates ownership)
32
+ const { stream, metadata } = await storage.downloadFile(fileId, userId);
33
+
34
+ // Set response headers
35
+ res.setHeader('Content-Type', metadata.contentType);
36
+ res.setHeader('Content-Length', metadata.size);
37
+ res.setHeader('Content-Disposition', `attachment; filename="${metadata.fileName}"`);
38
+
39
+ // Stream file to response
40
+ stream.pipe(res);
41
+
42
+ // Handle stream errors
43
+ stream.on('error', (error) => {
44
+ console.error('Stream error:', error);
45
+ if (!res.headersSent) {
46
+ res.status(500).json({
47
+ error: 'Internal Server Error',
48
+ message: 'Failed to stream file'
49
+ });
50
+ }
51
+ });
52
+ } catch (error) {
53
+ console.error('Download error:', error);
54
+
55
+ if (error.message === 'File not found') {
56
+ return res.status(404).json({
57
+ error: 'Not Found',
58
+ message: 'File not found'
59
+ });
60
+ }
61
+
62
+ if (error.message.startsWith('Unauthorized:')) {
63
+ return res.status(403).json({
64
+ error: 'Forbidden',
65
+ message: error.message
66
+ });
67
+ }
68
+
69
+ res.status(500).json({
70
+ error: 'Internal Server Error',
71
+ message: 'Failed to download file'
72
+ });
73
+ }
74
+ });
75
+
76
+ return router;
77
+ }
@@ -0,0 +1,23 @@
1
+ import express from 'express';
2
+
3
+ /**
4
+ * Health check router factory
5
+ * @returns {express.Router} Express router for health endpoint
6
+ */
7
+ export function createHealthRouter() {
8
+ const router = express.Router();
9
+
10
+ /**
11
+ * GET /health
12
+ * Public health check endpoint (no authentication required)
13
+ */
14
+ router.get('/health', (req, res) => {
15
+ res.status(200).json({
16
+ status: 'healthy',
17
+ service: 'files-service',
18
+ timestamp: new Date().toISOString()
19
+ });
20
+ });
21
+
22
+ return router;
23
+ }
@@ -0,0 +1,40 @@
1
+ import express from 'express';
2
+
3
+ /**
4
+ * List files router factory
5
+ * @param {AzureStorageClient} storage - Azure storage client instance
6
+ * @returns {express.Router} Express router for list endpoint
7
+ */
8
+ export function createListRouter(storage) {
9
+ const router = express.Router();
10
+
11
+ /**
12
+ * GET /files
13
+ * List user's files from Azure Blob Storage
14
+ * Requires: JWT authentication (applied by parent app)
15
+ * Returns: Array of file metadata for current user
16
+ */
17
+ router.get('/files', async (req, res) => {
18
+ try {
19
+ const userId = req.user.id;
20
+
21
+ // List files for current user
22
+ const files = await storage.listFiles(userId);
23
+
24
+ res.status(200).json({
25
+ success: true,
26
+ count: files.length,
27
+ files: files
28
+ });
29
+ } catch (error) {
30
+ console.error('List files error:', error);
31
+
32
+ res.status(500).json({
33
+ error: 'Internal Server Error',
34
+ message: 'Failed to list files'
35
+ });
36
+ }
37
+ });
38
+
39
+ return router;
40
+ }
@@ -0,0 +1,78 @@
1
+ import express from 'express';
2
+ import multer from 'multer';
3
+
4
+ /**
5
+ * Upload router factory
6
+ * @param {AzureStorageClient} storage - Azure storage client instance
7
+ * @param {number} maxFileSize - Maximum file size in bytes (default: 100MB)
8
+ * @returns {express.Router} Express router for upload endpoint
9
+ */
10
+ export function createUploadRouter(storage, maxFileSize = 100 * 1024 * 1024) {
11
+ const router = express.Router();
12
+
13
+ // Configure multer for memory storage (files stored in RAM before Azure upload)
14
+ const upload = multer({
15
+ storage: multer.memoryStorage(),
16
+ limits: {
17
+ fileSize: maxFileSize,
18
+ files: 1 // Single file upload
19
+ },
20
+ fileFilter: (req, file, cb) => {
21
+ // Optional: Add file type validation here
22
+ // For now, accept all file types
23
+ cb(null, true);
24
+ }
25
+ });
26
+
27
+ /**
28
+ * POST /upload
29
+ * Upload file to Azure Blob Storage
30
+ * Requires: JWT authentication (applied by parent app)
31
+ * Body: multipart/form-data with 'file' field
32
+ */
33
+ router.post('/upload', upload.single('file'), async (req, res) => {
34
+ try {
35
+ // Validate file was uploaded
36
+ if (!req.file) {
37
+ return res.status(400).json({
38
+ error: 'Bad Request',
39
+ message: 'No file uploaded'
40
+ });
41
+ }
42
+
43
+ const userId = req.user.id;
44
+ const { buffer, originalname, mimetype } = req.file;
45
+
46
+ // Upload to Azure Blob Storage
47
+ const fileMetadata = await storage.uploadFile(userId, buffer, originalname, mimetype);
48
+
49
+ // Return success response
50
+ res.status(201).json({
51
+ success: true,
52
+ file: {
53
+ id: fileMetadata.fileId,
54
+ name: fileMetadata.originalFilename,
55
+ contentType: fileMetadata.contentType,
56
+ size: fileMetadata.size,
57
+ uploadedAt: fileMetadata.uploadedAt
58
+ }
59
+ });
60
+ } catch (error) {
61
+ console.error('Upload error:', error);
62
+
63
+ if (error.code === 'LIMIT_FILE_SIZE') {
64
+ return res.status(413).json({
65
+ error: 'Payload Too Large',
66
+ message: `File size exceeds ${Math.round(maxFileSize / 1024 / 1024)}MB limit`
67
+ });
68
+ }
69
+
70
+ res.status(500).json({
71
+ error: 'Internal Server Error',
72
+ message: 'Failed to upload file'
73
+ });
74
+ }
75
+ });
76
+
77
+ return router;
78
+ }