Spaces:
Runtime error
Runtime error
| import { | |
| Controller, | |
| Post, | |
| Delete, | |
| Get, | |
| Query, | |
| Param, | |
| UseInterceptors, | |
| UploadedFile, | |
| HttpCode, | |
| HttpStatus, | |
| Res, | |
| } from '@nestjs/common'; | |
| import { FileInterceptor } from '@nestjs/platform-express'; | |
| import { | |
| ApiTags, | |
| ApiOperation, | |
| ApiConsumes, | |
| ApiQuery, | |
| ApiBody, | |
| } from '@nestjs/swagger'; | |
| import { BoxStorageService } from './box-storage.service'; | |
| import { LogToDiscord } from 'src/shared/decorators/log-to-discord.decorator'; | |
| import { ConfigService } from '@nestjs/config'; | |
| import type { Response } from 'express'; | |
| import axios from 'axios'; | |
| import { FileType } from './enums/file-type.enum'; | |
| import { SkipApiToken } from 'src/shared/decorators/skip-api-token.decorator'; | |
| ('Box Storage') | |
| ('box') | |
| export class BoxStorageController { | |
| constructor( | |
| private readonly boxStorageService: BoxStorageService, | |
| private readonly configService: ConfigService, | |
| ) {} | |
| ('error-illustration') | |
| () | |
| ({ | |
| summary: '404 illustration asset (same-origin)', | |
| description: | |
| 'Serves the illustration used on the HTML 404 page. Proxies remote CDN and caches in memory with a safe SVG fallback.', | |
| }) | |
| async errorIllustration(() res: Response) { | |
| const { contentType, body } = | |
| await this.boxStorageService.get404Illustration(); | |
| res.setHeader('Content-Type', contentType); | |
| res.setHeader('Cache-Control', 'public, max-age=86400'); | |
| return res.status(200).send(body); | |
| } | |
| ('oauth/authorize') | |
| () | |
| () | |
| ({ | |
| summary: 'Start OAuth 2.0 authorization flow', | |
| description: 'Redirects to Box.com authorization page to get user consent', | |
| }) | |
| authorize(() res: Response) { | |
| const clientId = this.configService.get<string>('boxStorage.clientId'); | |
| const redirectUri = | |
| this.configService.get<string>('boxStorage.redirectUri') || | |
| 'http://localhost:3000/box/oauth/callback'; | |
| if (!clientId) { | |
| return res.status(500).json({ error: 'Box Client ID not configured' }); | |
| } | |
| const authUrl = `https://account.box.com/api/oauth2/authorize?client_id=${clientId}&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}`; | |
| return res.redirect(authUrl); | |
| } | |
| ('oauth/callback') | |
| () | |
| () | |
| ({ | |
| summary: 'OAuth 2.0 callback endpoint', | |
| description: | |
| 'Receives authorization code from Box and exchanges it for access and refresh tokens', | |
| }) | |
| async oauthCallback( | |
| ('code') code: string, | |
| ('error') error: string, | |
| ) { | |
| if (error) { | |
| return { | |
| success: false, | |
| error: error, | |
| message: 'Authorization failed', | |
| }; | |
| } | |
| if (!code) { | |
| return { | |
| success: false, | |
| message: 'No authorization code received', | |
| }; | |
| } | |
| try { | |
| const clientId = this.configService.get<string>('boxStorage.clientId'); | |
| const clientSecret = this.configService.get<string>( | |
| 'boxStorage.clientSecret', | |
| ); | |
| const redirectUri = | |
| this.configService.get<string>('boxStorage.redirectUri') || | |
| 'http://localhost:3000/box/oauth/callback'; | |
| if (!clientId || !clientSecret) { | |
| return { | |
| success: false, | |
| message: 'Box OAuth credentials not configured', | |
| }; | |
| } | |
| const response = await axios.post( | |
| 'https://api.box.com/oauth2/token', | |
| new URLSearchParams({ | |
| grant_type: 'authorization_code', | |
| code: code, | |
| client_id: clientId, | |
| client_secret: clientSecret, | |
| redirect_uri: redirectUri, | |
| } as Record<string, string>), | |
| { | |
| headers: { | |
| 'Content-Type': 'application/x-www-form-urlencoded', | |
| }, | |
| }, | |
| ); | |
| // Save tokens to file automatically | |
| await this.boxStorageService.saveInitialTokens( | |
| response.data.access_token, | |
| response.data.refresh_token, | |
| response.data.expires_in, | |
| ); | |
| return { | |
| success: true, | |
| message: | |
| '✅ OAuth authorization successful! Tokens saved to box-tokens.json', | |
| tokens: { | |
| access_token: response.data.access_token, | |
| refresh_token: response.data.refresh_token, | |
| expires_in: response.data.expires_in, | |
| }, | |
| instructions: [ | |
| '✅ Tokens have been automatically saved to box-tokens.json', | |
| '✅ No need to update .env file manually!', | |
| '✅ Your app will now work automatically', | |
| 'ℹ️ The tokens will be auto-refreshed in the background', | |
| ], | |
| }; | |
| } catch (err) { | |
| return { | |
| success: false, | |
| error: err.response?.data || err.message, | |
| message: 'Failed to exchange authorization code for tokens', | |
| }; | |
| } | |
| } | |
| ('upload') | |
| () | |
| ({ summary: 'Upload file to Box.com' }) | |
| ('multipart/form-data') | |
| ({ | |
| name: 'fileType', | |
| enum: FileType, | |
| required: false, | |
| description: | |
| 'Type of file to determine upload folder (IMAGE, VIDEO, DOCUMENT). Defaults to main folder if not provided.', | |
| }) | |
| ({ | |
| name: 'customFilename', | |
| required: false, | |
| description: 'Custom filename (optional)', | |
| }) | |
| ({ | |
| schema: { | |
| type: 'object', | |
| properties: { | |
| file: { | |
| type: 'string', | |
| format: 'binary', | |
| }, | |
| }, | |
| }, | |
| }) | |
| (FileInterceptor('file')) | |
| async uploadFile( | |
| () file: any, | |
| ('fileType') fileType?: FileType, | |
| ('customFilename') customFilename?: string, | |
| ) { | |
| return this.boxStorageService.uploadFile({ | |
| fileBuffer: file.buffer, | |
| filename: file.originalname, | |
| fileType: fileType, | |
| customFilename: customFilename, | |
| }); | |
| } | |
| ('by-id') | |
| () | |
| (HttpStatus.OK) | |
| ({ summary: 'Delete file by Box file ID' }) | |
| ({ | |
| name: 'fileId', | |
| required: true, | |
| description: 'Box file ID', | |
| type: String, | |
| }) | |
| async deleteFileById(('fileId') fileId: string) { | |
| const result = await this.boxStorageService.deleteFile(fileId); | |
| return { | |
| message: 'File deleted successfully', | |
| ...result, | |
| }; | |
| } | |
| ('file/:fileId') | |
| () | |
| ({ | |
| summary: 'Stream file content from Box', | |
| description: | |
| 'Streams file content through your server. Supports images, videos, audio, PDFs, and more.', | |
| }) | |
| async streamFile(() res: Response, ('fileId') fileId: string) { | |
| try { | |
| // Get file metadata, stream, and detected content type from service | |
| const { metadata, stream, contentType } = | |
| await this.boxStorageService.getFileWithStream(fileId); | |
| // Set appropriate headers for inline display | |
| res.setHeader('Content-Type', contentType); | |
| res.setHeader('Content-Length', metadata.size); | |
| res.setHeader('Cache-Control', 'public, max-age=31536000'); | |
| res.setHeader('Accept-Ranges', 'bytes'); // Enable video seeking | |
| res.setHeader( | |
| 'Content-Disposition', | |
| `inline; filename="${metadata.name}"`, | |
| ); | |
| // Stream file content | |
| stream.pipe(res); | |
| } catch (error) { | |
| // Check if it's a 404 error (file not found) | |
| const isNotFound = | |
| error.status === 404 || error.status === HttpStatus.NOT_FOUND; | |
| if (isNotFound) { | |
| // Send beautiful retro TV 404 page | |
| res.status(404).send(this.boxStorageService.generate404Page(fileId)); | |
| } else { | |
| // Other errors - send JSON | |
| res.status(error.status || 500).json({ | |
| success: false, | |
| message: error.message || 'Failed to stream file', | |
| }); | |
| } | |
| } | |
| } | |
| } | |