mirror of
https://github.com/hexastack/hexabot
synced 2025-04-19 05:45:36 +00:00
feat: add support for signed public urls for channels
This commit is contained in:
parent
4ea98abfbf
commit
03cf7e6877
@ -13,6 +13,7 @@ import {
|
|||||||
Module,
|
Module,
|
||||||
RequestMethod,
|
RequestMethod,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { InjectDynamicProviders } from 'nestjs-dynamic-providers';
|
import { InjectDynamicProviders } from 'nestjs-dynamic-providers';
|
||||||
|
|
||||||
import { AttachmentModule } from '@/attachment/attachment.module';
|
import { AttachmentModule } from '@/attachment/attachment.module';
|
||||||
@ -38,10 +39,10 @@ export interface ChannelModuleOptions {
|
|||||||
'dist/.hexabot/custom/extensions/channels/**/*.channel.js',
|
'dist/.hexabot/custom/extensions/channels/**/*.channel.js',
|
||||||
)
|
)
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [ChatModule, AttachmentModule, CmsModule, HttpModule, JwtModule],
|
||||||
controllers: [WebhookController, ChannelController],
|
controllers: [WebhookController, ChannelController],
|
||||||
providers: [ChannelService],
|
providers: [ChannelService],
|
||||||
exports: [ChannelService],
|
exports: [ChannelService],
|
||||||
imports: [ChatModule, AttachmentModule, CmsModule, HttpModule],
|
|
||||||
})
|
})
|
||||||
export class ChannelModule {
|
export class ChannelModule {
|
||||||
configure(consumer: MiddlewareConsumer) {
|
configure(consumer: MiddlewareConsumer) {
|
||||||
|
@ -98,6 +98,19 @@ export class ChannelService {
|
|||||||
handler.handle(req, res);
|
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.
|
* Handles a websocket request for the web channel.
|
||||||
*
|
*
|
||||||
|
@ -8,17 +8,28 @@
|
|||||||
|
|
||||||
import path from 'path';
|
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 { 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 { SubscriberCreateDto } from '@/chat/dto/subscriber.dto';
|
||||||
import {
|
import {
|
||||||
StdOutgoingEnvelope,
|
StdOutgoingEnvelope,
|
||||||
StdOutgoingMessage,
|
StdOutgoingMessage,
|
||||||
} from '@/chat/schemas/types/message';
|
} from '@/chat/schemas/types/message';
|
||||||
|
import { config } from '@/config';
|
||||||
import { LoggerService } from '@/logger/logger.service';
|
import { LoggerService } from '@/logger/logger.service';
|
||||||
import { SettingService } from '@/setting/services/setting.service';
|
import { SettingService } from '@/setting/services/setting.service';
|
||||||
import { Extension } from '@/utils/generics/extension';
|
import { Extension } from '@/utils/generics/extension';
|
||||||
|
import { buildURL } from '@/utils/helpers/URL';
|
||||||
import { HyphenToUnderscore } from '@/utils/types/extension';
|
import { HyphenToUnderscore } from '@/utils/types/extension';
|
||||||
import { SocketRequest } from '@/websocket/utils/socket-request';
|
import { SocketRequest } from '@/websocket/utils/socket-request';
|
||||||
import { SocketResponse } from '@/websocket/utils/socket-response';
|
import { SocketResponse } from '@/websocket/utils/socket-response';
|
||||||
@ -37,6 +48,19 @@ export default abstract class ChannelHandler<
|
|||||||
{
|
{
|
||||||
private readonly settings: ChannelSetting<N>[];
|
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(
|
constructor(
|
||||||
name: N,
|
name: N,
|
||||||
protected readonly settingService: SettingService,
|
protected readonly settingService: SettingService,
|
||||||
@ -200,4 +224,54 @@ export default abstract class ChannelHandler<
|
|||||||
// Do nothing, override in channel
|
// Do nothing, override in channel
|
||||||
next();
|
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).
|
* 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 { Request, Response } from 'express'; // Import the Express request and response types
|
||||||
|
|
||||||
import { LoggerService } from '@/logger/logger.service';
|
import { LoggerService } from '@/logger/logger.service';
|
||||||
@ -21,6 +21,28 @@ export class WebhookController {
|
|||||||
private readonly logger: LoggerService,
|
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.
|
* Handles GET requests of a specific channel.
|
||||||
* This endpoint is accessible to public access (messaging platforms).
|
* This endpoint is accessible to public access (messaging platforms).
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { MongooseModule } from '@nestjs/mongoose';
|
import { MongooseModule } from '@nestjs/mongoose';
|
||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
|
|
||||||
@ -101,6 +102,7 @@ describe('BlockService', () => {
|
|||||||
ContextVarModel,
|
ContextVarModel,
|
||||||
LanguageModel,
|
LanguageModel,
|
||||||
]),
|
]),
|
||||||
|
JwtModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
EventEmitter2,
|
EventEmitter2,
|
||||||
|
@ -114,6 +114,11 @@ export const config: Config = {
|
|||||||
? Number(process.env.UPLOAD_MAX_SIZE_IN_BYTES)
|
? Number(process.env.UPLOAD_MAX_SIZE_IN_BYTES)
|
||||||
: 50 * 1024 * 1024, // 50 MB in bytes
|
: 50 * 1024 * 1024, // 50 MB in bytes
|
||||||
appName: 'Hexabot.ai',
|
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: {
|
pagination: {
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
@ -81,6 +81,7 @@ export type Config = {
|
|||||||
storageMode: 'disk' | 'memory';
|
storageMode: 'disk' | 'memory';
|
||||||
maxUploadSize: number;
|
maxUploadSize: number;
|
||||||
appName: string;
|
appName: string;
|
||||||
|
signedUrl: TJwtOptions;
|
||||||
};
|
};
|
||||||
pagination: {
|
pagination: {
|
||||||
limit: number;
|
limit: number;
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { MongooseModule } from '@nestjs/mongoose';
|
import { MongooseModule } from '@nestjs/mongoose';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
@ -78,6 +79,7 @@ describe('WebChannelHandler', () => {
|
|||||||
LabelModel,
|
LabelModel,
|
||||||
UserModel,
|
UserModel,
|
||||||
]),
|
]),
|
||||||
|
JwtModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { MongooseModule } from '@nestjs/mongoose';
|
import { MongooseModule } from '@nestjs/mongoose';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
@ -57,6 +58,7 @@ describe(`Web event wrapper`, () => {
|
|||||||
MessageModel,
|
MessageModel,
|
||||||
MenuModel,
|
MenuModel,
|
||||||
]),
|
]),
|
||||||
|
JwtModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
@ -23,7 +23,8 @@ PASSWORD_RESET_JWT_SECRET=dev_only
|
|||||||
PASSWORD_RESET_EXPIRES_IN=1h
|
PASSWORD_RESET_EXPIRES_IN=1h
|
||||||
CONFIRM_ACCOUNT_SECRET=dev_only
|
CONFIRM_ACCOUNT_SECRET=dev_only
|
||||||
CONFIRM_ACCOUNT_EXPIRES_IN=1h
|
CONFIRM_ACCOUNT_EXPIRES_IN=1h
|
||||||
FRONTEND_DOCKER_IMAGE=linuxtry
|
SIGNED_URL_SECRET=dev_only
|
||||||
|
SIGNED_URL_EXPIRES_IN=1h
|
||||||
I18N_TRANSLATION_FILENAME=messages
|
I18N_TRANSLATION_FILENAME=messages
|
||||||
|
|
||||||
# Mongo configs
|
# Mongo configs
|
||||||
|
Loading…
Reference in New Issue
Block a user