mirror of
https://github.com/hexastack/hexabot
synced 2025-01-22 10:35:37 +00:00
refactor: rename context to resourceRef
This commit is contained in:
parent
505cd247a1
commit
3f9dd692bf
@ -110,7 +110,7 @@ describe('AttachmentController', () => {
|
||||
file: [],
|
||||
},
|
||||
{} as Request,
|
||||
{ context: 'block_attachment' },
|
||||
{ resourceRef: 'block_attachment' },
|
||||
);
|
||||
await expect(promiseResult).rejects.toThrow(
|
||||
new BadRequestException('No file was selected'),
|
||||
@ -128,7 +128,7 @@ describe('AttachmentController', () => {
|
||||
{
|
||||
session: { passport: { user: { id: '9'.repeat(24) } } },
|
||||
} as unknown as Request,
|
||||
{ context: 'block_attachment' },
|
||||
{ resourceRef: 'block_attachment' },
|
||||
);
|
||||
const [name] = attachmentFile.filename.split('.');
|
||||
expect(attachmentService.create).toHaveBeenCalledWith({
|
||||
@ -136,7 +136,7 @@ describe('AttachmentController', () => {
|
||||
type: attachmentFile.mimetype,
|
||||
name: attachmentFile.originalname,
|
||||
location: expect.stringMatching(new RegExp(`^/${name}`)),
|
||||
context: 'block_attachment',
|
||||
resourceRef: 'block_attachment',
|
||||
access: 'public',
|
||||
createdByRef: 'User',
|
||||
createdBy: '9'.repeat(24),
|
||||
@ -145,7 +145,7 @@ describe('AttachmentController', () => {
|
||||
[
|
||||
{
|
||||
...attachment,
|
||||
context: 'block_attachment',
|
||||
resourceRef: 'block_attachment',
|
||||
createdByRef: 'User',
|
||||
createdBy: '9'.repeat(24),
|
||||
},
|
||||
|
@ -67,7 +67,7 @@ export class AttachmentController extends BaseController<Attachment> {
|
||||
async filterCount(
|
||||
@Query(
|
||||
new SearchFilterPipe<Attachment>({
|
||||
allowedFields: ['name', 'type', 'context'],
|
||||
allowedFields: ['name', 'type', 'resourceRef'],
|
||||
}),
|
||||
)
|
||||
filters?: TFilterQuery<Attachment>,
|
||||
@ -97,7 +97,7 @@ export class AttachmentController extends BaseController<Attachment> {
|
||||
@Query(PageQueryPipe) pageQuery: PageQueryDto<Attachment>,
|
||||
@Query(
|
||||
new SearchFilterPipe<Attachment>({
|
||||
allowedFields: ['name', 'type', 'context'],
|
||||
allowedFields: ['name', 'type', 'resourceRef'],
|
||||
}),
|
||||
)
|
||||
filters: TFilterQuery<Attachment>,
|
||||
@ -130,7 +130,8 @@ export class AttachmentController extends BaseController<Attachment> {
|
||||
async uploadFile(
|
||||
@UploadedFiles() files: { file: Express.Multer.File[] },
|
||||
@Req() req: Request,
|
||||
@Query() { context, access = 'public' }: AttachmentContextParamDto,
|
||||
@Query()
|
||||
{ resourceRef, access = 'public' }: AttachmentContextParamDto,
|
||||
): Promise<Attachment[]> {
|
||||
if (!files || !Array.isArray(files?.file) || files.file.length === 0) {
|
||||
throw new BadRequestException('No file was selected');
|
||||
@ -149,7 +150,7 @@ export class AttachmentController extends BaseController<Attachment> {
|
||||
name: file.originalname,
|
||||
size: file.size,
|
||||
type: file.mimetype,
|
||||
context,
|
||||
resourceRef,
|
||||
access,
|
||||
createdBy: userId,
|
||||
createdByRef: 'User',
|
||||
|
@ -25,11 +25,11 @@ import { IsObjectId } from '@/utils/validation-rules/is-object-id';
|
||||
|
||||
import {
|
||||
AttachmentAccess,
|
||||
AttachmentContext,
|
||||
AttachmentCreatedByRef,
|
||||
AttachmentResourceRef,
|
||||
TAttachmentAccess,
|
||||
TAttachmentContext,
|
||||
TAttachmentCreatedByRef,
|
||||
TAttachmentResourceRef,
|
||||
} from '../types';
|
||||
|
||||
export class AttachmentMetadataDto {
|
||||
@ -67,16 +67,16 @@ export class AttachmentMetadataDto {
|
||||
channel?: Partial<Record<ChannelName, any>>;
|
||||
|
||||
/**
|
||||
* Attachment context
|
||||
* Attachment resource reference
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: 'Attachment Context',
|
||||
enum: Object.values(AttachmentContext),
|
||||
description: 'Attachment Resource Ref',
|
||||
enum: Object.values(AttachmentResourceRef),
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsIn(Object.values(AttachmentContext))
|
||||
context: TAttachmentContext;
|
||||
@IsIn(Object.values(AttachmentResourceRef))
|
||||
resourceRef: TAttachmentResourceRef;
|
||||
|
||||
/**
|
||||
* Attachment Owner Type
|
||||
@ -141,13 +141,13 @@ export class AttachmentDownloadDto extends ObjectIdDto {
|
||||
|
||||
export class AttachmentContextParamDto {
|
||||
@ApiProperty({
|
||||
description: 'Attachment Context',
|
||||
enum: Object.values(AttachmentContext),
|
||||
description: 'Attachment Resource Reference',
|
||||
enum: Object.values(AttachmentResourceRef),
|
||||
})
|
||||
@IsString()
|
||||
@IsIn(Object.values(AttachmentContext))
|
||||
@IsIn(Object.values(AttachmentResourceRef))
|
||||
@IsNotEmpty()
|
||||
context: TAttachmentContext;
|
||||
resourceRef: TAttachmentResourceRef;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: 'Attachment Access',
|
||||
|
@ -53,9 +53,9 @@ describe('AttachmentGuard', () => {
|
||||
});
|
||||
|
||||
describe('canActivate', () => {
|
||||
it('should allow GET requests with valid context', async () => {
|
||||
it('should allow GET requests with valid ref', async () => {
|
||||
const mockUser = { roles: ['admin-id'] } as any;
|
||||
const mockContext = ['user_avatar'];
|
||||
const mockRef = ['user_avatar'];
|
||||
|
||||
jest.spyOn(modelService, 'findOne').mockImplementation((criteria) => {
|
||||
return typeof criteria === 'string' ||
|
||||
@ -84,7 +84,7 @@ describe('AttachmentGuard', () => {
|
||||
const mockExecutionContext = {
|
||||
switchToHttp: jest.fn().mockReturnValue({
|
||||
getRequest: jest.fn().mockReturnValue({
|
||||
query: { where: { context: mockContext } },
|
||||
query: { where: { resourceRef: mockRef } },
|
||||
method: 'GET',
|
||||
user: mockUser,
|
||||
}),
|
||||
@ -95,11 +95,11 @@ describe('AttachmentGuard', () => {
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException for GET requests with invalid context', async () => {
|
||||
it('should throw BadRequestException for GET requests with invalid ref', async () => {
|
||||
const mockExecutionContext = {
|
||||
switchToHttp: jest.fn().mockReturnValue({
|
||||
getRequest: jest.fn().mockReturnValue({
|
||||
query: { where: { context: 'invalid_context' } },
|
||||
query: { where: { resourceRef: 'invalid_ref' } },
|
||||
method: 'GET',
|
||||
}),
|
||||
}),
|
||||
@ -120,7 +120,7 @@ describe('AttachmentGuard', () => {
|
||||
? Promise.reject('Invalid ID')
|
||||
: Promise.resolve({
|
||||
id: '9'.repeat(24),
|
||||
context: `user_avatar`,
|
||||
resourceRef: `user_avatar`,
|
||||
} as Attachment);
|
||||
});
|
||||
|
||||
@ -162,7 +162,7 @@ describe('AttachmentGuard', () => {
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow POST requests with valid context', async () => {
|
||||
it('should allow POST requests with a valid ref', async () => {
|
||||
const mockUser = { roles: ['editor-id'] } as any;
|
||||
|
||||
jest.spyOn(modelService, 'findOne').mockImplementation((criteria) => {
|
||||
@ -191,7 +191,7 @@ describe('AttachmentGuard', () => {
|
||||
const mockExecutionContext = {
|
||||
switchToHttp: jest.fn().mockReturnValue({
|
||||
getRequest: jest.fn().mockReturnValue({
|
||||
query: { context: 'block_attachment' },
|
||||
query: { resourceRef: 'block_attachment' },
|
||||
method: 'POST',
|
||||
user: mockUser,
|
||||
}),
|
||||
|
@ -26,8 +26,11 @@ import { Action } from '@/user/types/action.type';
|
||||
import { TModel } from '@/user/types/model.type';
|
||||
|
||||
import { AttachmentService } from '../services/attachment.service';
|
||||
import { TAttachmentContext } from '../types';
|
||||
import { isAttachmentContext, isAttachmentContextArray } from '../utilities';
|
||||
import { TAttachmentResourceRef } from '../types';
|
||||
import {
|
||||
isAttachmentResourceRef,
|
||||
isAttachmentResourceRefArray,
|
||||
} from '../utilities';
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentGuard implements CanActivate {
|
||||
@ -39,9 +42,9 @@ export class AttachmentGuard implements CanActivate {
|
||||
|
||||
private permissionMap: Record<
|
||||
Action,
|
||||
Record<TAttachmentContext, [TModel, Action][]>
|
||||
Record<TAttachmentResourceRef, [TModel, Action][]>
|
||||
> = {
|
||||
// Read attachments by context
|
||||
// Read attachments by ref
|
||||
[Action.READ]: {
|
||||
setting_attachment: [
|
||||
['setting', Action.READ],
|
||||
@ -62,7 +65,7 @@ export class AttachmentGuard implements CanActivate {
|
||||
['attachment', Action.READ],
|
||||
],
|
||||
},
|
||||
// Create attachments by context
|
||||
// Create attachments by ref
|
||||
[Action.CREATE]: {
|
||||
setting_attachment: [
|
||||
['setting', Action.UPDATE],
|
||||
@ -88,7 +91,7 @@ export class AttachmentGuard implements CanActivate {
|
||||
['attachment', Action.CREATE],
|
||||
],
|
||||
},
|
||||
// Delete attachments by context
|
||||
// Delete attachments by ref
|
||||
[Action.DELETE]: {
|
||||
setting_attachment: [
|
||||
['setting', Action.UPDATE],
|
||||
@ -156,27 +159,27 @@ export class AttachmentGuard implements CanActivate {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user is authorized to perform a given action on a attachment based on its context and user roles.
|
||||
* Checks if the user is authorized to perform a given action on a attachment based on the resource reference and user roles.
|
||||
*
|
||||
* @param action - The action on the attachment.
|
||||
* @param user - The current user.
|
||||
* @param context - The context of the attachment (e.g., user_avatar, setting_attachment).
|
||||
* @param resourceRef - The resource ref of the attachment (e.g., user_avatar, setting_attachment).
|
||||
* @returns A promise that resolves to `true` if the user has the required upload permission, otherwise `false`.
|
||||
*/
|
||||
private async isAuthorized(
|
||||
action: Action,
|
||||
user: Express.User & User,
|
||||
context: TAttachmentContext,
|
||||
resourceRef: TAttachmentResourceRef,
|
||||
): Promise<boolean> {
|
||||
if (!action) {
|
||||
throw new TypeError('Invalid action');
|
||||
}
|
||||
|
||||
if (!context) {
|
||||
throw new TypeError('Invalid context');
|
||||
if (!resourceRef) {
|
||||
throw new TypeError('Invalid resource ref');
|
||||
}
|
||||
|
||||
const permissions = this.permissionMap[action][context];
|
||||
const permissions = this.permissionMap[action][resourceRef];
|
||||
|
||||
if (!permissions.length) {
|
||||
return false;
|
||||
@ -214,17 +217,21 @@ export class AttachmentGuard implements CanActivate {
|
||||
throw new NotFoundException('Attachment not found!');
|
||||
}
|
||||
|
||||
return await this.isAuthorized(Action.READ, user, attachment.context);
|
||||
return await this.isAuthorized(
|
||||
Action.READ,
|
||||
user,
|
||||
attachment.resourceRef,
|
||||
);
|
||||
} else if (query.where) {
|
||||
const { context = [] } = query.where as qs.ParsedQs;
|
||||
const { resourceRef = [] } = query.where as qs.ParsedQs;
|
||||
|
||||
if (!isAttachmentContextArray(context)) {
|
||||
throw new BadRequestException('Invalid context param');
|
||||
if (!isAttachmentResourceRefArray(resourceRef)) {
|
||||
throw new BadRequestException('Invalid resource ref');
|
||||
}
|
||||
|
||||
return (
|
||||
await Promise.all(
|
||||
context.map((c) => this.isAuthorized(Action.READ, user, c)),
|
||||
resourceRef.map((c) => this.isAuthorized(Action.READ, user, c)),
|
||||
)
|
||||
).every(Boolean);
|
||||
} else {
|
||||
@ -233,12 +240,12 @@ export class AttachmentGuard implements CanActivate {
|
||||
}
|
||||
// upload() endpoint
|
||||
case 'POST': {
|
||||
const { context = '' } = query;
|
||||
if (!isAttachmentContext(context)) {
|
||||
throw new BadRequestException('Invalid context param');
|
||||
const { resourceRef = '' } = query;
|
||||
if (!isAttachmentResourceRef(resourceRef)) {
|
||||
throw new BadRequestException('Invalid resource ref');
|
||||
}
|
||||
|
||||
return await this.isAuthorized(Action.CREATE, user, context);
|
||||
return await this.isAuthorized(Action.CREATE, user, resourceRef);
|
||||
}
|
||||
// deleteOne() endpoint
|
||||
case 'DELETE': {
|
||||
@ -252,7 +259,7 @@ export class AttachmentGuard implements CanActivate {
|
||||
return await this.isAuthorized(
|
||||
Action.DELETE,
|
||||
user,
|
||||
attachment.context,
|
||||
attachment.resourceRef,
|
||||
);
|
||||
} else {
|
||||
throw new BadRequestException('Invalid params');
|
||||
|
@ -16,7 +16,7 @@ export const attachment: Attachment = {
|
||||
size: 343370,
|
||||
location:
|
||||
'/Screenshot from 2022-03-11 08-41-27-2a9799a8b6109c88fd9a7a690c1101934c.png',
|
||||
context: 'block_attachment',
|
||||
resourceRef: 'block_attachment',
|
||||
access: 'public',
|
||||
id: '65940d115178607da65c82b6',
|
||||
createdAt: new Date(),
|
||||
@ -47,7 +47,7 @@ export const attachments: Attachment[] = [
|
||||
location:
|
||||
'/app/src/attachment/uploads/Screenshot from 2022-03-11 08-41-27-2a9799a8b6109c88fd9a7a690c1101934c.png',
|
||||
channel: { ['some-channel']: {} },
|
||||
context: 'block_attachment',
|
||||
resourceRef: 'block_attachment',
|
||||
access: 'public',
|
||||
id: '65940d115178607da65c82b7',
|
||||
createdAt: new Date(),
|
||||
@ -62,7 +62,7 @@ export const attachments: Attachment[] = [
|
||||
location:
|
||||
'/app/src/attachment/uploads/Screenshot from 2022-03-18 08-58-15-af61e7f71281f9fd3f1ad7ad10107741c.png',
|
||||
channel: { ['some-channel']: {} },
|
||||
context: 'block_attachment',
|
||||
resourceRef: 'block_attachment',
|
||||
access: 'public',
|
||||
id: '65940d115178607da65c82b8',
|
||||
createdAt: new Date(),
|
||||
|
@ -25,11 +25,11 @@ import {
|
||||
|
||||
import {
|
||||
AttachmentAccess,
|
||||
AttachmentContext,
|
||||
AttachmentCreatedByRef,
|
||||
AttachmentResourceRef,
|
||||
TAttachmentAccess,
|
||||
TAttachmentContext,
|
||||
TAttachmentCreatedByRef,
|
||||
TAttachmentResourceRef,
|
||||
} from '../types';
|
||||
import { MIME_REGEX } from '../utilities';
|
||||
|
||||
@ -97,13 +97,13 @@ export class AttachmentStub extends BaseSchema {
|
||||
createdByRef: TAttachmentCreatedByRef;
|
||||
|
||||
/**
|
||||
* Context of the attachment
|
||||
* Resource reference of the attachment
|
||||
*/
|
||||
@Prop({ type: String, enum: Object.values(AttachmentContext) })
|
||||
context: TAttachmentContext;
|
||||
@Prop({ type: String, enum: Object.values(AttachmentResourceRef) })
|
||||
resourceRef: TAttachmentResourceRef;
|
||||
|
||||
/**
|
||||
* Context of the attachment
|
||||
* Access level of the attachment
|
||||
*/
|
||||
@Prop({ type: String, enum: Object.values(AttachmentAccess) })
|
||||
access: TAttachmentAccess;
|
||||
|
@ -30,7 +30,7 @@ import { BaseService } from '@/utils/generics/base-service';
|
||||
import { AttachmentMetadataDto } from '../dto/attachment.dto';
|
||||
import { AttachmentRepository } from '../repositories/attachment.repository';
|
||||
import { Attachment } from '../schemas/attachment.schema';
|
||||
import { TAttachmentContext } from '../types';
|
||||
import { TAttachmentResourceRef } from '../types';
|
||||
import {
|
||||
fileExists,
|
||||
generateUniqueFilename,
|
||||
@ -158,13 +158,13 @@ export class AttachmentService extends BaseService<Attachment> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachment root directory given the context
|
||||
* Get the attachment root directory given the resource reference
|
||||
*
|
||||
* @param context The attachment context
|
||||
* @param ref The attachment resource reference
|
||||
* @returns The root directory path
|
||||
*/
|
||||
getRootDirByContext(context: TAttachmentContext) {
|
||||
return context === 'subscriber_avatar' || context === 'user_avatar'
|
||||
getRootDirByResourceRef(ref: TAttachmentResourceRef) {
|
||||
return ref === 'subscriber_avatar' || ref === 'user_avatar'
|
||||
? config.parameters.avatarDir
|
||||
: config.parameters.uploadDir;
|
||||
}
|
||||
@ -186,7 +186,7 @@ export class AttachmentService extends BaseService<Attachment> {
|
||||
const storedDto = await this.getStoragePlugin()?.store?.(file, metadata);
|
||||
return storedDto ? await this.create(storedDto) : null;
|
||||
} else {
|
||||
const rootDir = this.getRootDirByContext(metadata.context);
|
||||
const rootDir = this.getRootDirByResourceRef(metadata.resourceRef);
|
||||
const uniqueFilename = generateUniqueFilename(metadata.name);
|
||||
const filePath = resolve(join(rootDir, sanitizeFilename(uniqueFilename)));
|
||||
|
||||
@ -244,7 +244,7 @@ export class AttachmentService extends BaseService<Attachment> {
|
||||
|
||||
return streamableFile;
|
||||
} else {
|
||||
const rootDir = this.getRootDirByContext(attachment.context);
|
||||
const rootDir = this.getRootDirByResourceRef(attachment.resourceRef);
|
||||
const path = resolve(join(rootDir, attachment.location));
|
||||
|
||||
if (!fileExists(path)) {
|
||||
|
@ -20,10 +20,10 @@ export enum AttachmentCreatedByRef {
|
||||
export type TAttachmentCreatedByRef = `${AttachmentCreatedByRef}`;
|
||||
|
||||
/**
|
||||
* Defines the various contexts in which an attachment can exist.
|
||||
* These contexts influence how the attachment is uploaded, stored, and accessed:
|
||||
* Defines the various resource references in which an attachment can exist.
|
||||
* These resource references influence how the attachment is uploaded, stored, and accessed:
|
||||
*/
|
||||
export enum AttachmentContext {
|
||||
export enum AttachmentResourceRef {
|
||||
SettingAttachment = 'setting_attachment', // Attachments related to app settings, restricted to users with specific permissions.
|
||||
UserAvatar = 'user_avatar', // Avatar files for users, only the current user can upload, accessible to those with appropriate permissions.
|
||||
SubscriberAvatar = 'subscriber_avatar', // Avatar files for subscribers, uploaded programmatically, accessible to authorized users.
|
||||
@ -32,7 +32,7 @@ export enum AttachmentContext {
|
||||
MessageAttachment = 'message_attachment', // Files sent or received via messages, uploaded programmatically, accessible to users with inbox permissions.;
|
||||
}
|
||||
|
||||
export type TAttachmentContext = `${AttachmentContext}`;
|
||||
export type TAttachmentResourceRef = `${AttachmentResourceRef}`;
|
||||
|
||||
export enum AttachmentAccess {
|
||||
Public = 'public',
|
||||
|
@ -15,7 +15,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { config } from '@/config';
|
||||
|
||||
import { AttachmentContext, TAttachmentContext } from '../types';
|
||||
import { AttachmentResourceRef, TAttachmentResourceRef } from '../types';
|
||||
|
||||
export const MIME_REGEX = /^[a-z-]+\/[0-9a-z\-.]+$/gm;
|
||||
|
||||
@ -82,27 +82,30 @@ export const generateUniqueFilename = (originalname: string) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the given context is of type TAttachmentContext.
|
||||
* Checks if the given ref is of type TAttachmentResourceRef.
|
||||
*
|
||||
* @param ctx - The context to check.
|
||||
* @returns True if the context is of type TAttachmentContext, otherwise false.
|
||||
* @param ref - The ref to check.
|
||||
* @returns True if the ref is of type TAttachmentResourceRef, otherwise false.
|
||||
*/
|
||||
export const isAttachmentContext = (ctx: any): ctx is TAttachmentContext => {
|
||||
return Object.values(AttachmentContext).includes(ctx);
|
||||
export const isAttachmentResourceRef = (
|
||||
ref: any,
|
||||
): ref is TAttachmentResourceRef => {
|
||||
return Object.values(AttachmentResourceRef).includes(ref);
|
||||
};
|
||||
AttachmentResourceRef;
|
||||
|
||||
/**
|
||||
* Checks if the given list is an array of TAttachmentContext.
|
||||
* Checks if the given list is an array of TAttachmentResourceRef.
|
||||
*
|
||||
* @param ctxList - The list of contexts to check.
|
||||
* @returns True if all items in the list are of type TAttachmentContext, otherwise false.
|
||||
* @param refList - The list of resource references to check.
|
||||
* @returns True if all items in the list are of type TAttachmentResourceRef, otherwise false.
|
||||
*/
|
||||
export const isAttachmentContextArray = (
|
||||
ctxList: any,
|
||||
): ctxList is TAttachmentContext[] => {
|
||||
export const isAttachmentResourceRefArray = (
|
||||
refList: any,
|
||||
): refList is TAttachmentResourceRef[] => {
|
||||
return (
|
||||
Array.isArray(ctxList) &&
|
||||
ctxList.length > 0 &&
|
||||
ctxList.every(isAttachmentContext)
|
||||
Array.isArray(refList) &&
|
||||
refList.length > 0 &&
|
||||
refList.every(isAttachmentResourceRef)
|
||||
);
|
||||
};
|
||||
|
@ -258,7 +258,7 @@ export default abstract class ChannelHandler<
|
||||
name: `${name ? `${name}-` : ''}${uuidv4()}.${mime.extension(type)}`,
|
||||
type,
|
||||
size,
|
||||
context: 'message_attachment',
|
||||
resourceRef: 'message_attachment',
|
||||
access: 'private',
|
||||
createdByRef: 'Subscriber',
|
||||
createdBy: subscriber.id,
|
||||
|
@ -88,7 +88,7 @@ const attachment: Attachment = {
|
||||
id: 'any-channel-attachment-id',
|
||||
},
|
||||
},
|
||||
context: 'block_attachment',
|
||||
resourceRef: 'block_attachment',
|
||||
access: 'public',
|
||||
createdByRef: 'User',
|
||||
createdBy: null,
|
||||
|
@ -280,7 +280,7 @@ export class ChatService {
|
||||
name: `avatar-${uuidv4()}.${extension}`,
|
||||
size,
|
||||
type,
|
||||
context: 'subscriber_avatar',
|
||||
resourceRef: 'subscriber_avatar',
|
||||
access: 'private',
|
||||
createdByRef: 'Subscriber',
|
||||
createdBy: subscriber.id,
|
||||
|
@ -625,7 +625,7 @@ export default abstract class BaseWebChannelHandler<
|
||||
name: data.name,
|
||||
size: Buffer.byteLength(data.file),
|
||||
type: data.type,
|
||||
context: 'message_attachment',
|
||||
resourceRef: 'message_attachment',
|
||||
access: 'private',
|
||||
createdByRef: 'Subscriber',
|
||||
createdBy: req.session?.web?.profile?.id,
|
||||
@ -692,7 +692,7 @@ export default abstract class BaseWebChannelHandler<
|
||||
name: file.originalname,
|
||||
size: file.size,
|
||||
type: file.mimetype,
|
||||
context: 'message_attachment',
|
||||
resourceRef: 'message_attachment',
|
||||
access: 'private',
|
||||
createdByRef: 'Subscriber',
|
||||
createdBy: req.session.web.profile?.id,
|
||||
|
@ -15,7 +15,10 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import attachmentSchema, {
|
||||
Attachment,
|
||||
} from '@/attachment/schemas/attachment.schema';
|
||||
import { AttachmentContext, AttachmentCreatedByRef } from '@/attachment/types';
|
||||
import {
|
||||
AttachmentCreatedByRef,
|
||||
AttachmentResourceRef,
|
||||
} from '@/attachment/types';
|
||||
import blockSchema, { Block } from '@/chat/schemas/block.schema';
|
||||
import messageSchema, { Message } from '@/chat/schemas/message.schema';
|
||||
import subscriberSchema, { Subscriber } from '@/chat/schemas/subscriber.schema';
|
||||
@ -79,7 +82,7 @@ const populateBlockAttachments = async ({ logger }: MigrationServices) => {
|
||||
{ _id: attachmentId },
|
||||
{
|
||||
$set: {
|
||||
context: AttachmentContext.BlockAttachment,
|
||||
resourceRef: AttachmentResourceRef.BlockAttachment,
|
||||
access: 'public',
|
||||
createdByRef: AttachmentCreatedByRef.User,
|
||||
createdBy: user._id,
|
||||
@ -103,7 +106,7 @@ const populateBlockAttachments = async ({ logger }: MigrationServices) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates setting attachment documents to populate new attributes (context, createdBy, createdByRef)
|
||||
* Updates setting attachment documents to populate new attributes (resourceRef, createdBy, createdByRef)
|
||||
*
|
||||
* @returns Resolves when the migration process is complete.
|
||||
*/
|
||||
@ -130,7 +133,7 @@ const populateSettingAttachments = async ({ logger }: MigrationServices) => {
|
||||
{ _id: setting.value },
|
||||
{
|
||||
$set: {
|
||||
context: AttachmentContext.SettingAttachment,
|
||||
resourceRef: AttachmentResourceRef.SettingAttachment,
|
||||
access: 'public',
|
||||
createdByRef: AttachmentCreatedByRef.User,
|
||||
createdBy: user._id,
|
||||
@ -148,7 +151,7 @@ const populateSettingAttachments = async ({ logger }: MigrationServices) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates user attachment documents to populate new attributes (context, createdBy, createdByRef)
|
||||
* Updates user attachment documents to populate new attributes (resourceRef, createdBy, createdByRef)
|
||||
*
|
||||
* @returns Resolves when the migration process is complete.
|
||||
*/
|
||||
@ -169,7 +172,7 @@ const populateUserAvatars = async ({ logger }: MigrationServices) => {
|
||||
{ _id: user.avatar },
|
||||
{
|
||||
$set: {
|
||||
context: AttachmentContext.UserAvatar,
|
||||
resourceRef: AttachmentResourceRef.UserAvatar,
|
||||
access: 'private',
|
||||
createdByRef: AttachmentCreatedByRef.User,
|
||||
createdBy: user._id,
|
||||
@ -187,7 +190,7 @@ const populateUserAvatars = async ({ logger }: MigrationServices) => {
|
||||
|
||||
/**
|
||||
* Updates subscriber documents with their corresponding avatar attachments,
|
||||
* populate new attributes (context, createdBy, createdByRef) and moves avatar files to a new directory.
|
||||
* populate new attributes (resourceRef, createdBy, createdByRef) and moves avatar files to a new directory.
|
||||
*
|
||||
* @returns Resolves when the migration process is complete.
|
||||
*/
|
||||
@ -231,7 +234,7 @@ const populateSubscriberAvatars = async ({ logger }: MigrationServices) => {
|
||||
{ _id: attachment._id },
|
||||
{
|
||||
$set: {
|
||||
context: AttachmentContext.SubscriberAvatar,
|
||||
resourceRef: AttachmentResourceRef.SubscriberAvatar,
|
||||
access: 'private',
|
||||
createdByRef: AttachmentCreatedByRef.Subscriber,
|
||||
createdBy: subscriber._id,
|
||||
@ -353,18 +356,18 @@ const undoPopulateAttachments = async ({ logger }: MigrationServices) => {
|
||||
try {
|
||||
const result = await AttachmentModel.updateMany(
|
||||
{
|
||||
context: {
|
||||
resourceRef: {
|
||||
$in: [
|
||||
AttachmentContext.BlockAttachment,
|
||||
AttachmentContext.SettingAttachment,
|
||||
AttachmentContext.UserAvatar,
|
||||
AttachmentContext.SubscriberAvatar,
|
||||
AttachmentResourceRef.BlockAttachment,
|
||||
AttachmentResourceRef.SettingAttachment,
|
||||
AttachmentResourceRef.UserAvatar,
|
||||
AttachmentResourceRef.SubscriberAvatar,
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
$unset: {
|
||||
context: '',
|
||||
resourceRef: '',
|
||||
access: '',
|
||||
createdByRef: '',
|
||||
createdBy: '',
|
||||
@ -373,11 +376,11 @@ const undoPopulateAttachments = async ({ logger }: MigrationServices) => {
|
||||
);
|
||||
|
||||
logger.log(
|
||||
`Successfully reverted attributes for ${result.modifiedCount} attachments with context 'setting_attachment'`,
|
||||
`Successfully reverted attributes for ${result.modifiedCount} attachments with ref 'setting_attachment'`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to revert attributes for attachments with context 'setting_attachment': ${error.message}`,
|
||||
`Failed to revert attributes for attachments with ref 'setting_attachment': ${error.message}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -645,7 +648,7 @@ const migrateAndPopulateAttachmentMessages = async ({
|
||||
await attachmentService.updateOne(
|
||||
msg.message.attachment.payload.attachment_id as string,
|
||||
{
|
||||
context: 'message_attachment',
|
||||
resourceRef: 'message_attachment',
|
||||
access: 'private',
|
||||
createdByRef: msg.sender ? 'Subscriber' : 'User',
|
||||
createdBy: msg.sender ? msg.sender : adminUser.id,
|
||||
@ -678,7 +681,7 @@ const migrateAndPopulateAttachmentMessages = async ({
|
||||
size: fileBuffer.length,
|
||||
type: response.headers['content-type'],
|
||||
channel: {},
|
||||
context: 'message_attachment',
|
||||
resourceRef: 'message_attachment',
|
||||
access: msg.sender ? 'private' : 'public',
|
||||
createdBy: msg.sender ? msg.sender : adminUser.id,
|
||||
createdByRef: msg.sender ? 'Subscriber' : 'User',
|
||||
|
@ -294,7 +294,7 @@ export class ReadWriteUserController extends ReadOnlyUserController {
|
||||
name: avatarFile.originalname,
|
||||
size: avatarFile.size,
|
||||
type: avatarFile.mimetype,
|
||||
context: 'user_avatar',
|
||||
resourceRef: 'user_avatar',
|
||||
access: 'private',
|
||||
createdByRef: 'User',
|
||||
createdBy: req.user.id,
|
||||
|
4
api/src/utils/test/fixtures/attachment.ts
vendored
4
api/src/utils/test/fixtures/attachment.ts
vendored
@ -22,7 +22,7 @@ export const attachmentFixtures: AttachmentCreateDto[] = [
|
||||
id: '1',
|
||||
},
|
||||
},
|
||||
context: 'content_attachment',
|
||||
resourceRef: 'content_attachment',
|
||||
access: 'public',
|
||||
createdByRef: 'User',
|
||||
createdBy: '9'.repeat(24),
|
||||
@ -37,7 +37,7 @@ export const attachmentFixtures: AttachmentCreateDto[] = [
|
||||
id: '2',
|
||||
},
|
||||
},
|
||||
context: 'content_attachment',
|
||||
resourceRef: 'content_attachment',
|
||||
access: 'public',
|
||||
createdByRef: 'User',
|
||||
createdBy: '9'.repeat(24),
|
||||
|
@ -13,7 +13,7 @@ import { forwardRef } from "react";
|
||||
import { useGet } from "@/hooks/crud/useGet";
|
||||
import { useHasPermission } from "@/hooks/useHasPermission";
|
||||
import { EntityType } from "@/services/types";
|
||||
import { IAttachment, TAttachmentContext } from "@/types/attachment.types";
|
||||
import { IAttachment, TAttachmentResourceRef } from "@/types/attachment.types";
|
||||
import { PermissionAction } from "@/types/permission.types";
|
||||
|
||||
import AttachmentThumbnail from "./AttachmentThumbnail";
|
||||
@ -29,7 +29,7 @@ type AttachmentThumbnailProps = {
|
||||
onChange?: (id: string | null, mimeType: string | null) => void;
|
||||
error?: boolean;
|
||||
helperText?: string;
|
||||
context: TAttachmentContext;
|
||||
resourceRef: TAttachmentResourceRef;
|
||||
};
|
||||
|
||||
const AttachmentInput = forwardRef<HTMLDivElement, AttachmentThumbnailProps>(
|
||||
@ -44,7 +44,7 @@ const AttachmentInput = forwardRef<HTMLDivElement, AttachmentThumbnailProps>(
|
||||
onChange,
|
||||
error,
|
||||
helperText,
|
||||
context,
|
||||
resourceRef,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
@ -84,7 +84,7 @@ const AttachmentInput = forwardRef<HTMLDivElement, AttachmentThumbnailProps>(
|
||||
accept={accept}
|
||||
enableMediaLibrary={enableMediaLibrary}
|
||||
onChange={handleChange}
|
||||
context={context}
|
||||
resourceRef={resourceRef}
|
||||
/>
|
||||
) : null}
|
||||
{helperText ? (
|
||||
|
@ -17,7 +17,7 @@ import { getDisplayDialogs, useDialog } from "@/hooks/useDialog";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import { useTranslate } from "@/hooks/useTranslate";
|
||||
import { EntityType } from "@/services/types";
|
||||
import { IAttachment, TAttachmentContext } from "@/types/attachment.types";
|
||||
import { IAttachment, TAttachmentResourceRef } from "@/types/attachment.types";
|
||||
|
||||
import { AttachmentDialog } from "./AttachmentDialog";
|
||||
import AttachmentThumbnail from "./AttachmentThumbnail";
|
||||
@ -68,7 +68,7 @@ export type FileUploadProps = {
|
||||
enableMediaLibrary?: boolean;
|
||||
onChange?: (data?: IAttachment | null) => void;
|
||||
onUploadComplete?: () => void;
|
||||
context: TAttachmentContext;
|
||||
resourceRef: TAttachmentResourceRef;
|
||||
};
|
||||
|
||||
const AttachmentUploader: FC<FileUploadProps> = ({
|
||||
@ -76,7 +76,7 @@ const AttachmentUploader: FC<FileUploadProps> = ({
|
||||
enableMediaLibrary,
|
||||
onChange,
|
||||
onUploadComplete,
|
||||
context,
|
||||
resourceRef,
|
||||
}) => {
|
||||
const [attachment, setAttachment] = useState<IAttachment | undefined>(
|
||||
undefined,
|
||||
@ -119,7 +119,7 @@ const AttachmentUploader: FC<FileUploadProps> = ({
|
||||
|
||||
return;
|
||||
}
|
||||
uploadAttachment({ file, context });
|
||||
uploadAttachment({ file, resourceRef });
|
||||
}
|
||||
};
|
||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
|
@ -12,7 +12,7 @@ import { forwardRef, useState } from "react";
|
||||
|
||||
import { useHasPermission } from "@/hooks/useHasPermission";
|
||||
import { EntityType } from "@/services/types";
|
||||
import { IAttachment, TAttachmentContext } from "@/types/attachment.types";
|
||||
import { IAttachment, TAttachmentResourceRef } from "@/types/attachment.types";
|
||||
import { PermissionAction } from "@/types/permission.types";
|
||||
|
||||
import AttachmentThumbnail from "./AttachmentThumbnail";
|
||||
@ -28,7 +28,7 @@ type MultipleAttachmentInputProps = {
|
||||
onChange?: (ids: string[]) => void;
|
||||
error?: boolean;
|
||||
helperText?: string;
|
||||
context: TAttachmentContext;
|
||||
resourceRef: TAttachmentResourceRef;
|
||||
};
|
||||
|
||||
const MultipleAttachmentInput = forwardRef<
|
||||
@ -46,7 +46,7 @@ const MultipleAttachmentInput = forwardRef<
|
||||
onChange,
|
||||
error,
|
||||
helperText,
|
||||
context,
|
||||
resourceRef,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
@ -109,7 +109,7 @@ const MultipleAttachmentInput = forwardRef<
|
||||
accept={accept}
|
||||
enableMediaLibrary={enableMediaLibrary}
|
||||
onChange={(attachment) => handleChange(attachment)}
|
||||
context={context}
|
||||
resourceRef={resourceRef}
|
||||
/>
|
||||
)}
|
||||
{helperText && (
|
||||
|
@ -116,7 +116,7 @@ const ContentFieldInput: React.FC<ContentFieldInput> = ({
|
||||
value={field.value?.payload?.id}
|
||||
accept={MIME_TYPES["images"].join(",")}
|
||||
format="full"
|
||||
context="content_attachment"
|
||||
resourceRef="content_attachment"
|
||||
/>
|
||||
);
|
||||
default:
|
||||
|
@ -81,7 +81,7 @@ export const ContentImportDialog: FC<ContentImportDialogProps> = ({
|
||||
}}
|
||||
label=""
|
||||
value={attachmentId}
|
||||
context="content_attachment"
|
||||
resourceRef="content_attachment"
|
||||
/>
|
||||
</ContentItem>
|
||||
</ContentContainer>
|
||||
|
@ -186,7 +186,7 @@ const SettingInput: React.FC<RenderSettingInputProps> = ({
|
||||
accept={MIME_TYPES["images"].join(",")}
|
||||
format="full"
|
||||
size={128}
|
||||
context="setting_attachment"
|
||||
resourceRef="setting_attachment"
|
||||
/>
|
||||
);
|
||||
|
||||
@ -199,7 +199,7 @@ const SettingInput: React.FC<RenderSettingInputProps> = ({
|
||||
accept={MIME_TYPES["images"].join(",")}
|
||||
format="full"
|
||||
size={128}
|
||||
context="setting_attachment"
|
||||
resourceRef="setting_attachment"
|
||||
/>
|
||||
);
|
||||
default:
|
||||
|
@ -69,7 +69,7 @@ const AttachmentMessageForm = () => {
|
||||
},
|
||||
});
|
||||
}}
|
||||
context="block_attachment"
|
||||
resourceRef="block_attachment"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
@ -9,7 +9,7 @@
|
||||
import { useMutation, useQueryClient } from "react-query";
|
||||
|
||||
import { QueryType, TMutationOptions } from "@/services/types";
|
||||
import { TAttachmentContext } from "@/types/attachment.types";
|
||||
import { TAttachmentResourceRef } from "@/types/attachment.types";
|
||||
import { IBaseSchema, IDynamicProps, TType } from "@/types/base.types";
|
||||
|
||||
import { useEntityApiClient } from "../useApiClient";
|
||||
@ -27,7 +27,7 @@ export const useUpload = <
|
||||
TMutationOptions<
|
||||
TBasic,
|
||||
Error,
|
||||
{ file: File; context: TAttachmentContext },
|
||||
{ file: File; resourceRef: TAttachmentResourceRef },
|
||||
TBasic
|
||||
>,
|
||||
"mutationFn" | "mutationKey"
|
||||
@ -39,8 +39,8 @@ export const useUpload = <
|
||||
const { invalidate = true, ...otherOptions } = options || {};
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ file, context }) => {
|
||||
const data = await api.upload(file, context);
|
||||
mutationFn: async ({ file, resourceRef }) => {
|
||||
const data = await api.upload(file, resourceRef);
|
||||
const { entities, result } = normalizeAndCache(data);
|
||||
|
||||
// Invalidate all counts & collections
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
import { AxiosInstance, AxiosResponse } from "axios";
|
||||
|
||||
import { TAttachmentContext } from "@/types/attachment.types";
|
||||
import { TAttachmentResourceRef } from "@/types/attachment.types";
|
||||
import { ILoginAttributes } from "@/types/auth/login.types";
|
||||
import { IUserPermissions } from "@/types/auth/permission.types";
|
||||
import { StatsType } from "@/types/bot-stat.types";
|
||||
@ -302,7 +302,7 @@ export class EntityApiClient<TAttr, TBasic, TFull> extends ApiClient {
|
||||
return data;
|
||||
}
|
||||
|
||||
async upload(file: File, context?: TAttachmentContext) {
|
||||
async upload(file: File, resourceRef?: TAttachmentResourceRef) {
|
||||
const { _csrf } = await this.getCsrf();
|
||||
const formData = new FormData();
|
||||
|
||||
@ -314,7 +314,7 @@ export class EntityApiClient<TAttr, TBasic, TFull> extends ApiClient {
|
||||
FormData
|
||||
>(
|
||||
`${ROUTES[this.type]}/upload?_csrf=${_csrf}${
|
||||
context ? `&context=${context}` : ""
|
||||
resourceRef ? `&resourceRef=${resourceRef}` : ""
|
||||
}`,
|
||||
formData,
|
||||
{
|
||||
|
@ -25,10 +25,10 @@ export enum AttachmentCreatedByRef {
|
||||
export type TAttachmentCreatedByRef = `${AttachmentCreatedByRef}`;
|
||||
|
||||
/**
|
||||
* Defines the various contexts in which an attachment can exist.
|
||||
* These contexts influence how the attachment is uploaded, stored, and accessed:
|
||||
* Defines the various resource references in which an attachment can exist.
|
||||
* These references influence how the attachment is uploaded, stored, and accessed:
|
||||
*/
|
||||
export enum AttachmentContext {
|
||||
export enum AttachmentResourceRef {
|
||||
SettingAttachment = "setting_attachment", // Attachments related to app settings, restricted to users with specific permissions.
|
||||
UserAvatar = "user_avatar", // Avatar files for users, only the current user can upload, accessible to those with appropriate permissions.
|
||||
SubscriberAvatar = "subscriber_avatar", // Avatar files for subscribers, uploaded programmatically, accessible to authorized users.
|
||||
@ -37,7 +37,7 @@ export enum AttachmentContext {
|
||||
MessageAttachment = "message_attachment", // Files sent or received via messages, uploaded programmatically, accessible to users with inbox permissions.;
|
||||
}
|
||||
|
||||
export type TAttachmentContext = `${AttachmentContext}`;
|
||||
export type TAttachmentResourceRef = `${AttachmentResourceRef}`;
|
||||
|
||||
export interface IAttachmentAttributes {
|
||||
name: string;
|
||||
@ -46,7 +46,7 @@ export interface IAttachmentAttributes {
|
||||
location: string;
|
||||
url: string;
|
||||
channel?: Record<string, any>;
|
||||
context: TAttachmentContext;
|
||||
resourceRef: TAttachmentResourceRef;
|
||||
createdByRef: TAttachmentCreatedByRef;
|
||||
createdBy: string | null;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user