streamflix-api / src /shared /modules /upload /box-storage /box-storage.controller.ts
Akshar2325
feat(box-storage): add LogToDiscord decorator to OAuth and upload endpoints
cfe53d1
Raw
History Blame
7.78 kB
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';
@ApiTags('Box Storage')
@Controller('box')
export class BoxStorageController {
constructor(
private readonly boxStorageService: BoxStorageService,
private readonly configService: ConfigService,
) {}
@Get('error-illustration')
@SkipApiToken()
@ApiOperation({
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() 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);
}
@Get('oauth/authorize')
@LogToDiscord()
@SkipApiToken()
@ApiOperation({
summary: 'Start OAuth 2.0 authorization flow',
description: 'Redirects to Box.com authorization page to get user consent',
})
authorize(@Res() 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);
}
@Get('oauth/callback')
@LogToDiscord()
@SkipApiToken()
@ApiOperation({
summary: 'OAuth 2.0 callback endpoint',
description:
'Receives authorization code from Box and exchanges it for access and refresh tokens',
})
async oauthCallback(
@Query('code') code: string,
@Query('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',
};
}
}
@Post('upload')
@LogToDiscord()
@ApiOperation({ summary: 'Upload file to Box.com' })
@ApiConsumes('multipart/form-data')
@ApiQuery({
name: 'fileType',
enum: FileType,
required: false,
description:
'Type of file to determine upload folder (IMAGE, VIDEO, DOCUMENT). Defaults to main folder if not provided.',
})
@ApiQuery({
name: 'customFilename',
required: false,
description: 'Custom filename (optional)',
})
@ApiBody({
schema: {
type: 'object',
properties: {
file: {
type: 'string',
format: 'binary',
},
},
},
})
@UseInterceptors(FileInterceptor('file'))
async uploadFile(
@UploadedFile() file: any,
@Query('fileType') fileType?: FileType,
@Query('customFilename') customFilename?: string,
) {
return this.boxStorageService.uploadFile({
fileBuffer: file.buffer,
filename: file.originalname,
fileType: fileType,
customFilename: customFilename,
});
}
@Delete('by-id')
@LogToDiscord()
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Delete file by Box file ID' })
@ApiQuery({
name: 'fileId',
required: true,
description: 'Box file ID',
type: String,
})
async deleteFileById(@Query('fileId') fileId: string) {
const result = await this.boxStorageService.deleteFile(fileId);
return {
message: 'File deleted successfully',
...result,
};
}
@Get('file/:fileId')
@SkipApiToken()
@ApiOperation({
summary: 'Stream file content from Box',
description:
'Streams file content through your server. Supports images, videos, audio, PDFs, and more.',
})
async streamFile(@Res() res: Response, @Param('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',
});
}
}
}
}