diff --git a/packages/backend/src/config/cors-options.js b/packages/backend/src/config/cors-options.js index c96a2d4a..b4386b1c 100644 --- a/packages/backend/src/config/cors-options.js +++ b/packages/backend/src/config/cors-options.js @@ -2,7 +2,7 @@ import appConfig from './app.js'; const corsOptions = { origin: appConfig.webAppUrl, - methods: 'POST', + methods: 'GET,HEAD,POST,DELETE', credentials: true, optionsSuccessStatus: 200, }; diff --git a/packages/backend/src/controllers/api/v1/access-tokens/revoke-access-token.js b/packages/backend/src/controllers/api/v1/access-tokens/revoke-access-token.js new file mode 100644 index 00000000..95c7ffbc --- /dev/null +++ b/packages/backend/src/controllers/api/v1/access-tokens/revoke-access-token.js @@ -0,0 +1,15 @@ +export default async (request, response) => { + const token = request.params.token; + + const accessToken = await request.currentUser + .$relatedQuery('accessTokens') + .findOne({ + token, + revoked_at: null, + }) + .throwIfNotFound(); + + await accessToken.revoke(); + + response.status(204).send(); +}; diff --git a/packages/backend/src/controllers/api/v1/access-tokens/revoke-access-token.test.js b/packages/backend/src/controllers/api/v1/access-tokens/revoke-access-token.test.js new file mode 100644 index 00000000..1651418a --- /dev/null +++ b/packages/backend/src/controllers/api/v1/access-tokens/revoke-access-token.test.js @@ -0,0 +1,54 @@ +import { expect, describe, it, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../test/factories/user.js'; +import AccessToken from '../../../../models/access-token.js'; + +describe('DELETE /api/v1/access-tokens/:token', () => { + let token; + + beforeEach(async () => { + const currentUser = await createUser({ + email: 'user@automatisch.io', + password: 'password', + }); + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should respond with HTTP 204 with correct token', async () => { + await request(app) + .delete(`/api/v1/access-tokens/${token}`) + .set('Authorization', token) + .expect(204); + + const revokedToken = await AccessToken.query().findOne({ token }); + + expect(revokedToken).toBeDefined(); + expect(revokedToken.revokedAt).not.toBeNull(); + }); + + it('should respond with HTTP 401 with incorrect credentials', async () => { + await request(app) + .delete(`/api/v1/access-tokens/${token}`) + .set('Authorization', 'wrong-token') + .expect(401); + + const unrevokedToken = await AccessToken.query().findOne({ token }); + + expect(unrevokedToken).toBeDefined(); + expect(unrevokedToken.revokedAt).toBeNull(); + }); + + it('should respond with HTTP 404 with correct credentials, but non-valid token', async () => { + await request(app) + .delete('/api/v1/access-tokens/wrong-token') + .set('Authorization', token) + .expect(404); + + const unrevokedToken = await AccessToken.query().findOne({ token }); + + expect(unrevokedToken).toBeDefined(); + expect(unrevokedToken.revokedAt).toBeNull(); + }); +}); diff --git a/packages/backend/src/models/access-token.js b/packages/backend/src/models/access-token.js index 7afb4776..f40a185f 100644 --- a/packages/backend/src/models/access-token.js +++ b/packages/backend/src/models/access-token.js @@ -27,6 +27,10 @@ class AccessToken extends Base { }, }, }); + + async revoke() { + return await this.$query().patch({ revokedAt: new Date().toISOString() }); + } } export default AccessToken; diff --git a/packages/backend/src/routes/api/v1/access-tokens.js b/packages/backend/src/routes/api/v1/access-tokens.js index 8d7ed14c..b41c3e0f 100644 --- a/packages/backend/src/routes/api/v1/access-tokens.js +++ b/packages/backend/src/routes/api/v1/access-tokens.js @@ -1,9 +1,16 @@ import { Router } from 'express'; import asyncHandler from 'express-async-handler'; import createAccessTokenAction from '../../../controllers/api/v1/access-tokens/create-access-token.js'; - +import revokeAccessTokenAction from '../../../controllers/api/v1/access-tokens/revoke-access-token.js'; +import { authenticateUser } from '../../../helpers/authentication.js'; const router = Router(); router.post('/', asyncHandler(createAccessTokenAction)); +router.delete( + '/:token', + authenticateUser, + asyncHandler(revokeAccessTokenAction) +); + export default router;