diff --git a/packages/backend/src/controllers/api/v1/access-tokens/create-access-token.js b/packages/backend/src/controllers/api/v1/access-tokens/create-access-token.js new file mode 100644 index 00000000..5fe162a9 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/access-tokens/create-access-token.js @@ -0,0 +1,13 @@ +import User from '../../../../models/user.js'; +import { renderObject, renderError } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const { email, password } = request.body; + const token = await User.authenticate(email, password); + + if (token) { + return renderObject(response, { token }); + } + + renderError(response, [{ general: ['Incorrect email or password.'] }]); +}; diff --git a/packages/backend/src/controllers/api/v1/access-tokens/create-access-token.test.js b/packages/backend/src/controllers/api/v1/access-tokens/create-access-token.test.js new file mode 100644 index 00000000..cc54b094 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/access-tokens/create-access-token.test.js @@ -0,0 +1,39 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import { createUser } from '../../../../../test/factories/user'; + +describe('POST /api/v1/access-tokens', () => { + beforeEach(async () => { + await createUser({ + email: 'user@automatisch.io', + password: 'password', + }); + }); + + it('should return the token data with correct credentials', async () => { + const response = await request(app) + .post('/api/v1/access-tokens') + .send({ + email: 'user@automatisch.io', + password: 'password', + }) + .expect(200); + + expect(response.body.data.token.length).toBeGreaterThan(0); + }); + + it('should return error with incorrect credentials', async () => { + const response = await request(app) + .post('/api/v1/access-tokens') + .send({ + email: 'incorrect@email.com', + password: 'incorrectpassword', + }) + .expect(422); + + expect(response.body.errors.general).toEqual([ + 'Incorrect email or password.', + ]); + }); +}); diff --git a/packages/backend/src/helpers/renderer.js b/packages/backend/src/helpers/renderer.js index 91ea259a..c4329a00 100644 --- a/packages/backend/src/helpers/renderer.js +++ b/packages/backend/src/helpers/renderer.js @@ -44,4 +44,22 @@ const renderObject = (response, object, options) => { return response.json(computedPayload); }; -export { renderObject }; +const renderError = (response, errors, status, type) => { + const errorStatus = status || 422; + const errorType = type || 'ValidationError'; + + const payload = { + errors: errors.reduce((acc, error) => { + const key = Object.keys(error)[0]; + acc[key] = error[key]; + return acc; + }, {}), + meta: { + type: errorType, + }, + }; + + return response.status(errorStatus).send(payload); +}; + +export { renderObject, renderError }; diff --git a/packages/backend/src/models/user.js b/packages/backend/src/models/user.js index dfc68d1c..f67e1a63 100644 --- a/packages/backend/src/models/user.js +++ b/packages/backend/src/models/user.js @@ -5,6 +5,7 @@ import crypto from 'node:crypto'; import appConfig from '../config/app.js'; import { hasValidLicense } from '../helpers/license.ee.js'; import userAbility from '../helpers/user-ability.js'; +import createAuthTokenByUserId from '../helpers/create-auth-token-by-user-id.js'; import Base from './base.js'; import Connection from './connection.js'; import Execution from './execution.js'; @@ -161,6 +162,17 @@ class User extends Base { : Execution.query(); } + static async authenticate(email, password) { + const user = await User.query().findOne({ + email: email?.toLowerCase() || null, + }); + + if (user && (await user.login(password))) { + const token = createAuthTokenByUserId(user.id); + return token; + } + } + login(password) { return bcrypt.compare(password, this.password); } diff --git a/packages/backend/src/routes/api/v1/access-tokens.js b/packages/backend/src/routes/api/v1/access-tokens.js new file mode 100644 index 00000000..8d7ed14c --- /dev/null +++ b/packages/backend/src/routes/api/v1/access-tokens.js @@ -0,0 +1,9 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import createAccessTokenAction from '../../../controllers/api/v1/access-tokens/create-access-token.js'; + +const router = Router(); + +router.post('/', asyncHandler(createAccessTokenAction)); + +export default router; diff --git a/packages/backend/src/routes/index.js b/packages/backend/src/routes/index.js index 6a6a5e21..cb141691 100644 --- a/packages/backend/src/routes/index.js +++ b/packages/backend/src/routes/index.js @@ -4,6 +4,7 @@ import webhooksRouter from './webhooks.js'; import paddleRouter from './paddle.ee.js'; import healthcheckRouter from './healthcheck.js'; import automatischRouter from './api/v1/automatisch.js'; +import accessTokensRouter from './api/v1/access-tokens.js'; import usersRouter from './api/v1/users.js'; import paymentRouter from './api/v1/payment.ee.js'; import appAuthClientsRouter from './api/v1/app-auth-clients.js'; @@ -26,6 +27,7 @@ router.use('/webhooks', webhooksRouter); router.use('/paddle', paddleRouter); router.use('/healthcheck', healthcheckRouter); router.use('/api/v1/automatisch', automatischRouter); +router.use('/api/v1/access-tokens', accessTokensRouter); router.use('/api/v1/users', usersRouter); router.use('/api/v1/payment', paymentRouter); router.use('/api/v1/app-auth-clients', appAuthClientsRouter);