Skip to content

Commit

Permalink
Merge pull request #357 from internxt/chore/workspaces-gateway-and-mi…
Browse files Browse the repository at this point in the history
…scelaneous-files

[_] chore: added mailer service, files/folders/gateway/workspaces dtos
  • Loading branch information
sg-gs authored Jul 11, 2024
2 parents f4476f8 + 57e44f3 commit e36a2c1
Show file tree
Hide file tree
Showing 31 changed files with 851 additions and 4 deletions.
81 changes: 81 additions & 0 deletions src/common/http-exception-filter-extended.exception.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { ArgumentsHost, HttpException, Logger } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { BaseExceptionFilter } from '@nestjs/core';
import { newUser } from '../../test/fixtures';
import { ExtendedHttpExceptionFilter } from './http-exception-filter-extended.exception';

describe('ExtendedHttpExceptionFilter', () => {
let filter: ExtendedHttpExceptionFilter;
let loggerErrorSpy: jest.SpyInstance;
let baseExceptionFilterCatchSpy: jest.SpyInstance;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ExtendedHttpExceptionFilter],
}).compile();

filter = module.get<ExtendedHttpExceptionFilter>(
ExtendedHttpExceptionFilter,
);
loggerErrorSpy = jest.spyOn(Logger.prototype, 'error').mockImplementation();
baseExceptionFilterCatchSpy = jest
.spyOn(BaseExceptionFilter.prototype, 'catch')
.mockImplementation();
});

afterEach(() => {
jest.clearAllMocks();
});

const createMockArgumentsHost = (url: string, method: string, user: any) =>
({
switchToHttp: jest.fn().mockReturnValue({
getRequest: jest.fn().mockReturnValue({
url,
method,
user,
}),
getResponse: jest.fn().mockReturnValue({
statusCode: 500,
headers: {},
getHeader: jest.fn(),
setHeader: jest.fn(),
isHeadersSent: false,
}),
getNext: jest.fn(),
}),
getArgs: jest.fn(),
getArgByIndex: jest.fn(),
switchToRpc: jest.fn(),
switchToWs: jest.fn(),
getType: jest.fn(),
}) as unknown as ArgumentsHost;

it('When non expected error are sent, then it should log details and call parent catch', () => {
const mockException = new Error('Unexpected error');
const user = newUser();
const mockHost = createMockArgumentsHost('/my-endpoint', 'GET', user);

filter.catch(mockException, mockHost);

expect(loggerErrorSpy).toHaveBeenCalled();
expect(baseExceptionFilterCatchSpy).toHaveBeenCalledWith(
mockException,
mockHost,
);
});

it('When expected exception is sent, then it should not log detailss and call parent catch', () => {
const mockException = new HttpException('This is an http error', 400);
const user = newUser();
const mockHost = createMockArgumentsHost('/my-endpoint', 'GET', user);

filter.catch(mockException, mockHost);

expect(loggerErrorSpy).not.toHaveBeenCalled();
expect(baseExceptionFilterCatchSpy).toHaveBeenCalledWith(
mockException,
mockHost,
);
});
});
30 changes: 30 additions & 0 deletions src/common/http-exception-filter-extended.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Catch, Logger, HttpException, ArgumentsHost } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';

@Catch()
export class ExtendedHttpExceptionFilter extends BaseExceptionFilter {
private readonly logger = new Logger(ExtendedHttpExceptionFilter.name);

catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();

const request = ctx.getRequest();

if (!(exception instanceof HttpException)) {
const errorResponse = {
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
message: (exception as Error)?.message,
stack: (exception as Error)?.stack,
user: { email: request?.user?.email, uuid: request?.user?.uuid },
};

this.logger.error(
`[UNEXPECTED_ERROR] - Details: ${JSON.stringify(errorResponse)}`,
);
}

super.catch(exception, host);
}
}
25 changes: 25 additions & 0 deletions src/externals/bridge/bridge.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,31 @@ export class BridgeService {
}
}

async setStorage(email: UserAttributes['email'], bytes: number) {
try {
const url = this.configService.get('apis.storage.url');
const username = this.configService.get('apis.storage.auth.username');
const password = this.configService.get('apis.storage.auth.password');

const params = {
headers: { 'Content-Type': 'application/json' },
auth: { username, password },
};

await this.httpClient.post(
`${url}/gateway/upgrade`,
{ email, bytes },
params,
);
} catch (error) {
Logger.error(`
[BRIDGESERVICE/SETSTORAGE]: There was an error while trying to set user storage space Error: ${JSON.stringify(
error,
)}
`);
}
}

async getLimit(
networkUser: UserAttributes['bridgeUser'],
networkPass: UserAttributes['userId'],
Expand Down
65 changes: 65 additions & 0 deletions src/externals/mailer/mailer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import sendgrid from '@sendgrid/mail';
import { User } from '../../modules/user/user.domain';
import { Folder } from '../../modules/folder/folder.domain';
import { File } from '../../modules/file/file.domain';
import { Workspace } from '../../modules/workspaces/domains/workspaces.domain';

type SendInvitationToSharingContext = {
notification_message: string;
Expand Down Expand Up @@ -116,6 +117,70 @@ export class MailerService {
);
}

async sendWorkspaceUserInvitation(
senderName: User['name'],
invitedUserEmail: User['email'],
workspaceName: Workspace['name'],
mailInfo: {
acceptUrl: string;
declineUrl: string;
},
optionals: {
avatar?: {
pictureUrl: string;
initials: string;
};
message?: string;
},
): Promise<void> {
const context = {
sender_name: senderName,
workspace_name: workspaceName,
avatar: {
picture_url: optionals?.avatar?.pictureUrl,
initials: optionals?.avatar?.initials,
},
accept_url: mailInfo.acceptUrl,
decline_url: mailInfo.declineUrl,
message: optionals?.message,
};
await this.send(
invitedUserEmail,
this.configService.get('mailer.templates.invitationToWorkspaceUser'),
context,
);
}

async sendWorkspaceUserExternalInvitation(
senderName: User['name'],
invitedUserEmail: User['email'],
workspaceName: Workspace['name'],
signUpUrl: string,
optionals: {
avatar?: {
pictureUrl: string;
initials: string;
};
message?: string;
},
): Promise<void> {
const context = {
sender_name: senderName,
workspace_name: workspaceName,
avatar: {
picture_url: optionals?.avatar?.pictureUrl,
initials: optionals?.avatar?.initials,
},
signup_url: signUpUrl,
message: optionals?.message,
};
await this.send(
invitedUserEmail,
this.configService.get('mailer.templates.invitationToWorkspaceGuestUser'),
context,
);
}

async sendRemovedFromSharingEmail(
userRemovedFromSharingEmail: User['email'],
itemName: File['plainName'] | Folder['plainName'],
Expand Down
3 changes: 3 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ async function bootstrap() {
'internxt-mnemonic',
'x-share-password',
'X-Internxt-Captcha',
'x-internxt-workspace',
'internxt-resources-token',
],
exposedHeaders: ['sessionId'],
origin: '*',
Expand Down Expand Up @@ -68,6 +70,7 @@ async function bootstrap() {
.setDescription('Drive API')
.setVersion('1.0')
.addBearerAuth()
.addBearerAuth(undefined, 'gateway')
.build();

const document = SwaggerModule.createDocument(app, swaggerConfig);
Expand Down
12 changes: 8 additions & 4 deletions src/modules/auth/auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ export class AuthGuard extends PassportAuthGuard([JwtStrategy.id]) {
}

canActivate(context: ExecutionContext) {
const isPublic = this.reflector.get<boolean>(
'isPublic',
context.getHandler(),
const handlerContext = context.getHandler();
const classContext = context.getClass();

const isPublic = this.reflector.get<boolean>('isPublic', handlerContext);
const disableGlobalAuth = this.reflector.getAllAndOverride<boolean>(
'disableGlobalAuth',
[handlerContext, classContext],
);

if (isPublic) {
if (isPublic || disableGlobalAuth) {
return true;
}

Expand Down
2 changes: 2 additions & 0 deletions src/modules/auth/decorators/disable-global-auth.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { SetMetadata } from '@nestjs/common';
export const DisableGlobalAuth = () => SetMetadata('disableGlobalAuth', true);
28 changes: 28 additions & 0 deletions src/modules/auth/gateway-rs256jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { PassportStrategy } from '@nestjs/passport';
import { ConfigService } from '@nestjs/config';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Injectable } from '@nestjs/common';

const strategyId = 'gateway.jwt.rs256';
@Injectable()
export class GatewayRS256JwtStrategy extends PassportStrategy(
Strategy,
strategyId,
) {
static id = strategyId;
constructor(configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: Buffer.from(
configService.get('secrets.driveGateway') as string,
'base64',
).toString('utf8'),
algorithms: ['RS256'],
});
}

async validate(): Promise<boolean> {
return true;
}
}
17 changes: 17 additions & 0 deletions src/modules/auth/gateway.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard as PassportAuthGuard } from '@nestjs/passport';
import { GatewayRS256JwtStrategy } from './gateway-rs256jwt.strategy';

@Injectable()
export class GatewayGuard extends PassportAuthGuard(
GatewayRS256JwtStrategy.id,
) {
constructor(private readonly reflector: Reflector) {
super();
}

canActivate(context: ExecutionContext) {
return super.canActivate(context);
}
}
Loading

0 comments on commit e36a2c1

Please sign in to comment.