Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions lib/core/users/usecase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ function isEmailValid(email: string) {
export const RESET_PASSWORD_TOKEN_BYTES_LENGTH = 256;
export const SHA256_HASH_BYTES_LENGTH = 32;

export interface UserSpaceSnapshot {
maxSpaceBytes: number;
totalUsedSpaceBytes: number;
}

export class UserAlreadyExistsError extends Error {
constructor() {
super('User already exists');
Expand Down Expand Up @@ -72,6 +77,19 @@ export class UsersUsecase {
private eventBus: EventBus
) {}

async getUserUsage(uuid: User['uuid']): Promise<UserSpaceSnapshot> {
const user = await this.usersRepository.findByUuid(uuid);

if (!user) {
throw new UserNotFoundError(uuid);
}

return {
maxSpaceBytes: user.maxSpaceBytes,
totalUsedSpaceBytes: user.totalUsedSpaceBytes,
};
}

async updateEmail(uuid: User['uuid'], newEmail: User['email']): Promise<void> {
const [maybeAlreadyExistentUser, userToUpdate] = await Promise.all([
this.usersRepository.findByEmail(newEmail),
Expand Down
33 changes: 32 additions & 1 deletion lib/server/http/gateway/controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Request, Response } from 'express';
import { validate as uuidValidate } from 'uuid';
import { Logger } from 'winston';
import { EmailIsAlreadyInUseError, InvalidDataFormatError, UserAlreadyExistsError, UserNotFoundError, UsersUsecase } from '../../../core';
import { EmailIsAlreadyInUseError, InvalidDataFormatError, UserAlreadyExistsError, UserNotFoundError, UserSpaceSnapshot, UsersUsecase } from '../../../core';
import { BucketEntriesUsecase } from '../../../core/bucketEntries/usecase';

import { GatewayUsecase } from '../../../core/gateway/Usecase';
Expand All @@ -22,6 +23,36 @@ export class HTTPGatewayController {
private eventBus: EventBus
) {}

async getUserUsage(
req: Request<{ uuid: string }, {}, {}, {}>,
res: Response<UserSpaceSnapshot | { message: string }>
) {
const { uuid } = req.params;

if (!uuid || !uuidValidate(uuid)) {
return res.status(400).send({ message: 'Missing or invalid uuid' });
}

try {
const usage = await this.usersUsecase.getUserUsage(uuid);

return res.status(200).send(usage);
} catch (err) {
if (err instanceof UserNotFoundError) {
return res.status(404).send({ message: err.message });
}

this.logger.error(
'[GATEWAY/GET_USAGE] Error getting usage of user %s: %s. %s',
uuid,
(err as Error).message,
(err as Error).stack || 'NO STACK'
);

return res.status(500).send({ message: 'Internal server error' });
}
}

async updateUserEmail(req: Request<{ uuid: string }, {}, { email?: string }, {}>, res: Response) {
if (!(req.body && req.body.email && req.params.uuid)) {
return res.status(400).send({ error: "Missing params" });
Expand Down
1 change: 1 addition & 0 deletions lib/server/http/gateway/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const createGatewayHTTPRouter = (
const router = Router();

router.post('/users', jwtMiddleware, controller.findOrCreateUser.bind(controller));
router.get('/users/:uuid/usage', jwtMiddleware, controller.getUserUsage.bind(controller));
router.patch('/users/:uuid', jwtMiddleware, controller.updateUserEmail.bind(controller));
router.put('/storage/users/:uuid', jwtMiddleware, controller.changeStorage.bind(controller));
router.delete('/storage/files', jwtMiddleware, controller.deleteFilesInBulk.bind(controller));
Expand Down
25 changes: 25 additions & 0 deletions tests/lib/core/users/usecase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,31 @@ const fakeUser: User = {
}

describe('Users usecases', () => {
describe('getUserUsage()', () => {
it('Should return the user space snapshot if the user exists', async () => {
const user = { ...fakeUser, maxSpaceBytes: 5000, totalUsedSpaceBytes: 1200 };
stub(usersRepository, 'findByUuid').resolves(user);

const usage = await usecase.getUserUsage(user.uuid);

expect(usage).toStrictEqual({
maxSpaceBytes: 5000,
totalUsedSpaceBytes: 1200
});
});

it('Should reject if the user does not exist', async () => {
stub(usersRepository, 'findByUuid').resolves(null);

try {
await usecase.getUserUsage('unknown-uuid');
expect(true).toBeFalsy();
} catch (err) {
expect(err).toBeInstanceOf(UserNotFoundError);
}
});
});

describe('createUser()', () => {
it(`Should work if input data is valid`, async () => {
stub(usersRepository, 'findByEmail').resolves(null);
Expand Down
54 changes: 54 additions & 0 deletions tests/lib/e2e/gateway/gateway-v2.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,60 @@ describe('Gateway V2 e2e tests', () => {
})
})

describe('Getting user usage', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a 401 test always, as the gateway controller is especially delicate in terms of security

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed

it('When getting the usage of an existing user, then it should return the space snapshot', async () => {
const testUser = await createTestUser()

const jwt = signRS256JWT(
'5m',
engine._config.gateway.SIGN_JWT_SECRET,
);

const response = await testServer
.get(`/v2/gateway/users/${testUser.uuid}/usage`)
.set('Authorization', `Bearer ${jwt}`)

expect(response.status).toBe(200)
expect(response.body).toStrictEqual({
maxSpaceBytes: testUser.maxSpaceBytes,
totalUsedSpaceBytes: testUser.totalUsedSpaceBytes,
})
})

it('When getting the usage of a user that does not exist, then it should return 404', async () => {
const jwt = signRS256JWT(
'5m',
engine._config.gateway.SIGN_JWT_SECRET,
);

const response = await testServer
.get(`/v2/gateway/users/${crypto.randomUUID()}/usage`)
.set('Authorization', `Bearer ${jwt}`)

expect(response.status).toBe(404)
})

it('When the uuid has an invalid format, then it should return 400', async () => {
const jwt = signRS256JWT(
'5m',
engine._config.gateway.SIGN_JWT_SECRET,
);

const response = await testServer
.get('/v2/gateway/users/not-a-valid-uuid/usage')
.set('Authorization', `Bearer ${jwt}`)

expect(response.status).toBe(400)
})

it('When the request is not authenticated, then it should return 401', async () => {
const response = await testServer
.get(`/v2/gateway/users/${crypto.randomUUID()}/usage`)

expect(response.status).toBe(401)
})
})

describe('Deleting user files', () => {
let axiosGetStub: sinon.SinonStub
const FAKE_UPLOAD_URL = 'http://fake-upload-url'
Expand Down
Loading