mirror of
https://github.com/hexastack/hexabot
synced 2025-01-22 18:45:57 +00:00
Merge pull request #489 from Hexastack/feat/add-channel-signed-urls-support
feat: add support for signed public urls for channels
This commit is contained in:
commit
0fb357f906
@ -13,6 +13,7 @@ import {
|
||||
Module,
|
||||
RequestMethod,
|
||||
} from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { InjectDynamicProviders } from 'nestjs-dynamic-providers';
|
||||
|
||||
import { AttachmentModule } from '@/attachment/attachment.module';
|
||||
@ -38,10 +39,10 @@ export interface ChannelModuleOptions {
|
||||
'dist/.hexabot/custom/extensions/channels/**/*.channel.js',
|
||||
)
|
||||
@Module({
|
||||
imports: [ChatModule, AttachmentModule, CmsModule, HttpModule, JwtModule],
|
||||
controllers: [WebhookController, ChannelController],
|
||||
providers: [ChannelService],
|
||||
exports: [ChannelService],
|
||||
imports: [ChatModule, AttachmentModule, CmsModule, HttpModule],
|
||||
})
|
||||
export class ChannelModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
|
@ -98,6 +98,19 @@ export class ChannelService {
|
||||
handler.handle(req, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a download request for a specific channel.
|
||||
*
|
||||
* @param channel - The channel for which the request is being handled.
|
||||
* @param token - The file JWT token.
|
||||
* @param req - The HTTP express request object.
|
||||
* @returns A promise that resolves a streamable if the signed url is valid.
|
||||
*/
|
||||
async download(channel: string, token: string, req: Request) {
|
||||
const handler = this.getChannelHandler(`${channel}-channel`);
|
||||
return await handler.download(token, req);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a websocket request for the web channel.
|
||||
*
|
||||
|
@ -8,17 +8,28 @@
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import {
|
||||
Inject,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
OnModuleInit,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
|
||||
import { Attachment } from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentService } from '@/attachment/services/attachment.service';
|
||||
import { SubscriberCreateDto } from '@/chat/dto/subscriber.dto';
|
||||
import {
|
||||
StdOutgoingEnvelope,
|
||||
StdOutgoingMessage,
|
||||
} from '@/chat/schemas/types/message';
|
||||
import { config } from '@/config';
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
import { SettingService } from '@/setting/services/setting.service';
|
||||
import { Extension } from '@/utils/generics/extension';
|
||||
import { buildURL } from '@/utils/helpers/URL';
|
||||
import { HyphenToUnderscore } from '@/utils/types/extension';
|
||||
import { SocketRequest } from '@/websocket/utils/socket-request';
|
||||
import { SocketResponse } from '@/websocket/utils/socket-response';
|
||||
@ -37,6 +48,19 @@ export default abstract class ChannelHandler<
|
||||
{
|
||||
private readonly settings: ChannelSetting<N>[];
|
||||
|
||||
@Inject(AttachmentService)
|
||||
protected readonly attachmentService: AttachmentService;
|
||||
|
||||
@Inject(JwtService)
|
||||
protected readonly jwtService: JwtService;
|
||||
|
||||
protected readonly jwtSignOptions: JwtSignOptions = {
|
||||
secret: config.parameters.signedUrl.secret,
|
||||
expiresIn: config.parameters.signedUrl.expiresIn,
|
||||
algorithm: 'HS256',
|
||||
encoding: 'utf-8',
|
||||
};
|
||||
|
||||
constructor(
|
||||
name: N,
|
||||
protected readonly settingService: SettingService,
|
||||
@ -200,4 +224,54 @@ export default abstract class ChannelHandler<
|
||||
// Do nothing, override in channel
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a signed URL for downloading an attachment.
|
||||
*
|
||||
* This function creates a signed URL for a given attachment using a JWT token.
|
||||
* The signed URL includes the attachment name and a token as query parameters.
|
||||
*
|
||||
* @param attachment The attachment ID or object to generate a signed URL for.
|
||||
* @return A signed URL string for downloading the specified attachment.
|
||||
*/
|
||||
protected async getPublicUrl(attachment: string | Attachment) {
|
||||
const resource =
|
||||
typeof attachment === 'string'
|
||||
? await this.attachmentService.findOne(attachment)
|
||||
: attachment;
|
||||
|
||||
if (!resource) {
|
||||
throw new NotFoundException('Unable to find attachment');
|
||||
}
|
||||
|
||||
const token = this.jwtService.sign({ ...resource }, this.jwtSignOptions);
|
||||
const [name, _suffix] = this.getName().split('-');
|
||||
return buildURL(
|
||||
config.apiBaseUrl,
|
||||
`/webhook/${name}/download/${resource.name}?t=${encodeURIComponent(token)}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads an attachment using a signed token.
|
||||
*
|
||||
* This function verifies the provided token and retrieves the corresponding
|
||||
* attachment as a streamable file. If the verification fails or the attachment
|
||||
* cannot be located, it throws a NotFoundException.
|
||||
*
|
||||
* @param token The signed token used to verify and locate the attachment.
|
||||
* @param req - The HTTP express request object.
|
||||
* @return A streamable file of the attachment.
|
||||
* @throws NotFoundException if the attachment cannot be found or the token is invalid.
|
||||
*/
|
||||
public async download(token: string, _req: Request) {
|
||||
try {
|
||||
const result = this.jwtService.verify(token, this.jwtSignOptions);
|
||||
const attachment = plainToClass(Attachment, result);
|
||||
return await this.attachmentService.download(attachment);
|
||||
} catch (err) {
|
||||
this.logger.error('Failed to download attachment', err);
|
||||
throw new NotFoundException('Unable to locate attachment');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
|
||||
*/
|
||||
|
||||
import { Controller, Get, Param, Post, Req, Res } from '@nestjs/common';
|
||||
import { Controller, Get, Param, Post, Query, Req, Res } from '@nestjs/common';
|
||||
import { Request, Response } from 'express'; // Import the Express request and response types
|
||||
|
||||
import { LoggerService } from '@/logger/logger.service';
|
||||
@ -21,6 +21,28 @@ export class WebhookController {
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Handles GET requests to download a file
|
||||
*
|
||||
* @param channel - The name of the channel for which the request is being sent.
|
||||
* @param filename - The name of the requested file
|
||||
* @param t - The JWT Token query param.
|
||||
* @param req - The HTTP express request object.
|
||||
*
|
||||
* @returns A promise that resolves a streamable file.
|
||||
*/
|
||||
@Roles('public')
|
||||
@Get(':channel/download/:name')
|
||||
async handleDownload(
|
||||
@Param('channel') channel: string,
|
||||
@Param('name') name: string,
|
||||
@Query('t') token: string,
|
||||
@Req() req: Request,
|
||||
) {
|
||||
this.logger.log('Channel download request: ', channel, name);
|
||||
return await this.channelService.download(channel, token, req);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles GET requests of a specific channel.
|
||||
* This endpoint is accessible to public access (messaging platforms).
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
@ -101,6 +102,7 @@ describe('BlockService', () => {
|
||||
ContextVarModel,
|
||||
LanguageModel,
|
||||
]),
|
||||
JwtModule,
|
||||
],
|
||||
providers: [
|
||||
EventEmitter2,
|
||||
|
@ -114,6 +114,11 @@ export const config: Config = {
|
||||
? Number(process.env.UPLOAD_MAX_SIZE_IN_BYTES)
|
||||
: 50 * 1024 * 1024, // 50 MB in bytes
|
||||
appName: 'Hexabot.ai',
|
||||
signedUrl: {
|
||||
salt: parseInt(process.env.SALT_LENGTH || '12'),
|
||||
secret: process.env.SIGNED_URL_SECRET || 'DEFAULT_SIGNED_URL_SECRET',
|
||||
expiresIn: process.env.SIGNED_URL_EXPIRES_IN || '24H',
|
||||
},
|
||||
},
|
||||
pagination: {
|
||||
limit: 10,
|
||||
|
@ -81,6 +81,7 @@ export type Config = {
|
||||
storageMode: 'disk' | 'memory';
|
||||
maxUploadSize: number;
|
||||
appName: string;
|
||||
signedUrl: TJwtOptions;
|
||||
};
|
||||
pagination: {
|
||||
limit: number;
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Request } from 'express';
|
||||
@ -78,6 +79,7 @@ describe('WebChannelHandler', () => {
|
||||
LabelModel,
|
||||
UserModel,
|
||||
]),
|
||||
JwtModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
@ -57,6 +58,7 @@ describe(`Web event wrapper`, () => {
|
||||
MessageModel,
|
||||
MenuModel,
|
||||
]),
|
||||
JwtModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
@ -23,7 +23,8 @@ PASSWORD_RESET_JWT_SECRET=dev_only
|
||||
PASSWORD_RESET_EXPIRES_IN=1h
|
||||
CONFIRM_ACCOUNT_SECRET=dev_only
|
||||
CONFIRM_ACCOUNT_EXPIRES_IN=1h
|
||||
FRONTEND_DOCKER_IMAGE=linuxtry
|
||||
SIGNED_URL_SECRET=dev_only
|
||||
SIGNED_URL_EXPIRES_IN=1h
|
||||
I18N_TRANSLATION_FILENAME=messages
|
||||
|
||||
# Mongo configs
|
||||
|
Loading…
Reference in New Issue
Block a user