diff --git a/lib/core/users/usecase.ts b/lib/core/users/usecase.ts index e15f0b75..1da9c8d8 100644 --- a/lib/core/users/usecase.ts +++ b/lib/core/users/usecase.ts @@ -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'); @@ -72,6 +77,19 @@ export class UsersUsecase { private eventBus: EventBus ) {} + async getUserUsage(uuid: User['uuid']): Promise { + 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 { const [maybeAlreadyExistentUser, userToUpdate] = await Promise.all([ this.usersRepository.findByEmail(newEmail), diff --git a/lib/server/http/gateway/controller.ts b/lib/server/http/gateway/controller.ts index 3ac32bb5..fb5c139a 100644 --- a/lib/server/http/gateway/controller.ts +++ b/lib/server/http/gateway/controller.ts @@ -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'; @@ -22,6 +23,36 @@ export class HTTPGatewayController { private eventBus: EventBus ) {} + async getUserUsage( + req: Request<{ uuid: string }, {}, {}, {}>, + res: Response + ) { + 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" }); diff --git a/lib/server/http/gateway/index.ts b/lib/server/http/gateway/index.ts index a2483cd6..310c7900 100644 --- a/lib/server/http/gateway/index.ts +++ b/lib/server/http/gateway/index.ts @@ -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)); diff --git a/tests/lib/core/users/usecase.test.ts b/tests/lib/core/users/usecase.test.ts index 64e6f9e7..486a8b67 100644 --- a/tests/lib/core/users/usecase.test.ts +++ b/tests/lib/core/users/usecase.test.ts @@ -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); diff --git a/tests/lib/e2e/gateway/gateway-v2.e2e-spec.ts b/tests/lib/e2e/gateway/gateway-v2.e2e-spec.ts index e7bf1978..03a5a49a 100644 --- a/tests/lib/e2e/gateway/gateway-v2.e2e-spec.ts +++ b/tests/lib/e2e/gateway/gateway-v2.e2e-spec.ts @@ -44,6 +44,60 @@ describe('Gateway V2 e2e tests', () => { }) }) + describe('Getting user usage', () => { + 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'