diff --git a/packages/common/src/IAPIDef.ts b/packages/common/src/IAPIDef.ts index 49f2858..14bd49f 100644 --- a/packages/common/src/IAPIDef.ts +++ b/packages/common/src/IAPIDef.ts @@ -20,7 +20,7 @@ export interface IAPIDef { '/auth/logout': { 'get': {} } - '/users/password': { + '/auth/password': { 'post': { body: { oldPassword: string diff --git a/packages/server/src/application/IServices.ts b/packages/server/src/application/IServices.ts index 8b3fbfa..a85e4da 100644 --- a/packages/server/src/application/IServices.ts +++ b/packages/server/src/application/IServices.ts @@ -1,9 +1,9 @@ -import {IUserService} from '../services' +import {IAuthService} from '../services' import {ITeamService} from '../team' import {IUserPermissions} from '../user' export interface IServices { - userService: IUserService + authService: IAuthService teamService: ITeamService userPermissions: IUserPermissions } diff --git a/packages/server/src/application/configureServer.ts b/packages/server/src/application/configureServer.ts index ddb7ddb..60d6bf7 100644 --- a/packages/server/src/application/configureServer.ts +++ b/packages/server/src/application/configureServer.ts @@ -26,12 +26,12 @@ export const configureServer: ServerConfigurator = (config, database) => { const logger = loggerFactory.getLogger('api') const services: IServices = { - userService: new Services.UserService(database), + authService: new Services.AuthService(database), teamService: new Team.TeamService(database), userPermissions: new User.UserPermissions(database), } - const authenticator = new Middleware.Authenticator(services.userService) + const authenticator = new Middleware.Authenticator(services.authService) const transactionManager = database.transactionManager const createTransactionalRouter = () => @@ -73,13 +73,13 @@ export const configureServer: ServerConfigurator = (config, database) => { api: { path: '/api', handle: [ - new routes.LoginRoutes( - services.userService, + new routes.AuthRoutes( + services.authService, authenticator, createTransactionalRouter(), ).handle, new routes.UserRoutes( - services.userService, + services.authService, createTransactionalRouter(), ).handle, new Team.TeamRoutes( diff --git a/packages/server/src/middleware/Authenticator.test.ts b/packages/server/src/middleware/Authenticator.test.ts index 373dc31..7db406d 100644 --- a/packages/server/src/middleware/Authenticator.test.ts +++ b/packages/server/src/middleware/Authenticator.test.ts @@ -2,11 +2,11 @@ import express, {Application} from 'express' import request from 'supertest' import {Authenticator} from './Authenticator' import {ICredentials} from '@rondo.dev/common' -import {IUserService} from '../services' +import {IAuthService} from '../services' import {handlePromise} from './handlePromise' import {urlencoded} from 'body-parser' -describe('passport.promise', () => { +describe('Authenticator', () => { let app: Application let loginMiddleware: any @@ -18,7 +18,7 @@ describe('passport.promise', () => { firstName: 'test', lastName: 'test', } - const userService = new (class implements IUserService { + const authService = new (class implements IAuthService { async createUser() { return {id: 1, ...userInfo} } @@ -40,7 +40,7 @@ describe('passport.promise', () => { return undefined } })() - const authenticator = new Authenticator(userService) + const authenticator = new Authenticator(authService) app.use(urlencoded({ extended: false })) app.use(authenticator.handle) diff --git a/packages/server/src/middleware/Authenticator.ts b/packages/server/src/middleware/Authenticator.ts index 4575f08..b24173b 100644 --- a/packages/server/src/middleware/Authenticator.ts +++ b/packages/server/src/middleware/Authenticator.ts @@ -1,5 +1,5 @@ import {Authenticator as A, Passport} from 'passport' -import {IUserService} from '../services' +import {IAuthService} from '../services' import {Strategy as LocalStrategy} from 'passport-local' import {THandler} from './THandler' import {IMiddleware} from './IMiddleware' @@ -9,7 +9,7 @@ export class Authenticator implements IMiddleware { protected readonly passport: A readonly handle: THandler[] - constructor(protected readonly userService: IUserService) { + constructor(protected readonly authService: IAuthService) { this.passport = new Passport() as any this.configurePassport() @@ -45,7 +45,7 @@ export class Authenticator implements IMiddleware { protected deserializeUser = // TODO parametrize user type (userId: number, done: (err?: Error, user?: any) => void) => { - this.userService.findOne(userId) + this.authService.findOne(userId) .then(user => done(undefined, user)) .catch(done) } @@ -67,7 +67,7 @@ export class Authenticator implements IMiddleware { password: string, done: (err?: Error, user?: any) => void, ) => { - this.userService.validateCredentials({ username, password }) + this.authService.validateCredentials({ username, password }) .then(user => done(undefined, user)) .catch(done) } diff --git a/packages/server/src/routes/AuthRoutes.test.ts b/packages/server/src/routes/AuthRoutes.test.ts new file mode 100644 index 0000000..d445477 --- /dev/null +++ b/packages/server/src/routes/AuthRoutes.test.ts @@ -0,0 +1,78 @@ +import {test} from '../test' + +describe('/auth', () => { + + test.withDatabase() + + describe('/register', () => { + it('should create a new user account', async () => { + await test.registerAccount() + }) + }) + + describe('/login', () => { + + beforeEach(async () => { + await test.registerAccount() + }) + + it('should log in the newly created user', async () => { + await test.login() + }) + }) + + describe('/auth/password', () => { + + const t = test.request('/api') + beforeEach(async () => { + const session = await test.registerAccount() + const token = session.token + const cookie = session.cookie + t.setHeaders({cookie, 'x-csrf-token': token}) + }) + + it('should prevent access when user not logged in', async () => { + const {cookie, token} = await test.getCsrf() + await t + .setHeaders({'cookie': cookie, 'x-csrf-token': token}) + .post('/auth/password') + .expect(401) + }) + + describe('POST /users/password', () => { + it('changes user password when passwords match', async () => { + await t + .post('/auth/password') + .send({ oldPassword: test.password, newPassword: 'newPass' }) + .expect(200) + + await test.login(test.username, 'newPass') + }) + + it('returns 400 when passwords do not match', async () => { + await t + .post('/auth/password') + .send({ oldPassword: 'invalid-password', newPassword: 'newPass' }) + .expect(400) + }) + }) + + }) + + describe('/logout', () => { + + let cookie!: string + beforeEach(async () => { + await test.registerAccount() + cookie = (await test.login()).cookie + }) + + it('should log out the user', async () => { + await test.request('/api') + .get('/auth/logout') + .set('cookie', cookie) + .expect(200) + }) + }) + +}) diff --git a/packages/server/src/routes/LoginRoutes.ts b/packages/server/src/routes/AuthRoutes.ts similarity index 66% rename from packages/server/src/routes/LoginRoutes.ts rename to packages/server/src/routes/AuthRoutes.ts index e0cc954..9d72a34 100644 --- a/packages/server/src/routes/LoginRoutes.ts +++ b/packages/server/src/routes/AuthRoutes.ts @@ -1,12 +1,13 @@ import {AsyncRouter} from '../router' import {BaseRoute} from './BaseRoute' import {IAPIDef} from '@rondo.dev/common' -import {IUserService} from '../services' +import {IAuthService} from '../services' import {Authenticator} from '../middleware' +import {ensureLoggedInApi} from '../middleware' -export class LoginRoutes extends BaseRoute { +export class AuthRoutes extends BaseRoute { constructor( - protected readonly userService: IUserService, + protected readonly authService: IAuthService, protected readonly authenticator: Authenticator, protected readonly t: AsyncRouter, ) { @@ -15,7 +16,7 @@ export class LoginRoutes extends BaseRoute { setup(t: AsyncRouter) { t.post('/auth/register', async (req, res) => { - const user = await this.userService.createUser({ + const user = await this.authService.createUser({ username: req.body.username, password: req.body.password, firstName: req.body.firstName, @@ -37,6 +38,14 @@ export class LoginRoutes extends BaseRoute { return user }) + t.post('/auth/password', [ensureLoggedInApi], async req => { + await this.authService.changePassword({ + userId: req.user!.id, + oldPassword: req.body.oldPassword, + newPassword: req.body.newPassword, + }) + }) + t.get('/auth/logout', async (req, res) => { req.logout() }) diff --git a/packages/server/src/routes/LoginRoutes.test.ts b/packages/server/src/routes/LoginRoutes.test.ts deleted file mode 100644 index 52a9ef8..0000000 --- a/packages/server/src/routes/LoginRoutes.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import {test} from '../test' - -describe('login', () => { - - test.withDatabase() - - describe('/register', () => { - it('should create a new user account', async () => { - await test.registerAccount() - }) - }) - - describe('/login', () => { - - beforeEach(async () => { - await test.registerAccount() - }) - - it('should log in the newly created user', async () => { - await test.login() - }) - }) - - describe('/logout', () => { - - let cookie!: string - beforeEach(async () => { - await test.registerAccount() - cookie = (await test.login()).cookie - }) - - it('should log out the user', async () => { - await test.request('/api') - .get('/auth/logout') - .set('cookie', cookie) - .expect(200) - }) - }) - -}) diff --git a/packages/server/src/routes/UserRoutes.test.ts b/packages/server/src/routes/UserRoutes.test.ts index b07a9ab..44311da 100644 --- a/packages/server/src/routes/UserRoutes.test.ts +++ b/packages/server/src/routes/UserRoutes.test.ts @@ -15,31 +15,6 @@ describe('user', () => { t.setHeaders({ cookie, 'x-csrf-token': token }) }) - it('should prevent access when user not logged in', async () => { - await t - .setHeaders({ token }) - .get(`/users/password`) - .expect(401) - }) - - describe('POST /users/password', () => { - it('changes user password when passwords match', async () => { - await t - .post('/users/password') - .send({ oldPassword: test.password, newPassword: 'newPass' }) - .expect(200) - - await test.login(test.username, 'newPass') - }) - - it('returns 400 when passwords do not match', async () => { - await t - .post('/users/password') - .send({ oldPassword: 'invalid-password', newPassword: 'newPass' }) - .expect(400) - }) - }) - describe('GET /users/profile', () => { it('fetches user profile', async () => { t.setHeaders({ cookie }) diff --git a/packages/server/src/routes/UserRoutes.ts b/packages/server/src/routes/UserRoutes.ts index 1a7ff75..215f658 100644 --- a/packages/server/src/routes/UserRoutes.ts +++ b/packages/server/src/routes/UserRoutes.ts @@ -1,12 +1,12 @@ import {AsyncRouter} from '../router' import {BaseRoute} from './BaseRoute' import {IAPIDef} from '@rondo.dev/common' -import {IUserService} from '../services' +import {IAuthService} from '../services' import {ensureLoggedInApi} from '../middleware' export class UserRoutes extends BaseRoute { constructor( - protected readonly userService: IUserService, + protected readonly userService: IAuthService, protected readonly t: AsyncRouter, ) { super(t) @@ -15,14 +15,6 @@ export class UserRoutes extends BaseRoute { setup(t: AsyncRouter) { t.use('/users', ensureLoggedInApi) - t.post('/users/password', async req => { - await this.userService.changePassword({ - userId: req.user!.id, - oldPassword: req.body.oldPassword, - newPassword: req.body.newPassword, - }) - }) - t.get('/users/emails/:email', async req => { return this.userService.findUserByEmail(req.params.email) }) diff --git a/packages/server/src/routes/index.ts b/packages/server/src/routes/index.ts index a908140..ce5e7fc 100644 --- a/packages/server/src/routes/index.ts +++ b/packages/server/src/routes/index.ts @@ -1,4 +1,4 @@ -export * from './BaseRoute' -export * from './LoginRoutes' -export * from './UserRoutes' export * from './application' +export * from './AuthRoutes' +export * from './BaseRoute' +export * from './UserRoutes' diff --git a/packages/server/src/services/UserService.test.ts b/packages/server/src/services/AuthService.test.ts similarity index 79% rename from packages/server/src/services/UserService.test.ts rename to packages/server/src/services/AuthService.test.ts index b369ebf..a964eb6 100644 --- a/packages/server/src/services/UserService.test.ts +++ b/packages/server/src/services/AuthService.test.ts @@ -1,17 +1,17 @@ import {test} from '../test' -import {UserService} from './UserService' +import {AuthService} from './AuthService' -describe('UserService', () => { +describe('AuthService', () => { test.withDatabase() const username = test.username const password = '1234567890' - const userService = new UserService(test.bootstrap.database) + const authService = new AuthService(test.bootstrap.database) async function createUser(u = username, p = password) { - return userService.createUser({ + return authService.createUser({ username: u, password: p, firstName: 'test', @@ -23,7 +23,7 @@ describe('UserService', () => { it('creates a new user with bcrypted password', async () => { const result = await createUser() expect(result.id).toBeTruthy() - const user = await userService.findOne(result.id) + const user = await authService.findOne(result.id) expect(user).toBeTruthy() expect(user).not.toHaveProperty('password') }) @@ -48,7 +48,7 @@ describe('UserService', () => { describe('findUserByMail', () => { it('returns user without password', async () => { await createUser() - const user = await userService.findUserByEmail(username) + const user = await authService.findUserByEmail(username) expect(user).toBeTruthy() expect(user).not.toHaveProperty('password') }) @@ -57,7 +57,7 @@ describe('UserService', () => { describe('getUserEmails', () => { it('returns user emails', async () => { const {id} = await createUser() - const emails = await userService.findUserEmails(id) + const emails = await authService.findUserEmails(id) expect(emails).toEqual([{ id: jasmine.any(Number), userId: id, @@ -71,24 +71,24 @@ describe('UserService', () => { describe('validateCredentials', () => { it('returns user when password is valid', async () => { await createUser() - expect(await userService.validateCredentials({ username, password })) + expect(await authService.validateCredentials({ username, password })) .toBeTruthy() }) it('returns undefined when no user', async () => { - expect(await userService.validateCredentials({ username, password })) + expect(await authService.validateCredentials({ username, password })) .toBe(undefined) }) it('returns undefined when password is invalid', async () => { await createUser() - expect(await userService.validateCredentials({ username, password: 't' })) + expect(await authService.validateCredentials({ username, password: 't' })) .toBe(undefined) }) it('does not return a password', async () => { await createUser() - const user = await userService + const user = await authService .validateCredentials({ username, password }) expect(user).not.toHaveProperty('password') }) diff --git a/packages/server/src/services/UserService.ts b/packages/server/src/services/AuthService.ts similarity index 97% rename from packages/server/src/services/UserService.ts rename to packages/server/src/services/AuthService.ts index b2d006d..017af9d 100644 --- a/packages/server/src/services/UserService.ts +++ b/packages/server/src/services/AuthService.ts @@ -2,7 +2,7 @@ import createError from 'http-errors' import {BaseService} from './BaseService' import {IDatabase} from '../database/IDatabase' import {ICredentials, INewUser, IUser, trim} from '@rondo.dev/common' -import {IUserService} from './IUserService' +import {IAuthService} from './IAuthService' import {UserEmail} from '../entities/UserEmail' import {User} from '../entities/User' import {compare, hash} from 'bcrypt' @@ -12,7 +12,7 @@ import {Validator} from '../validator' const SALT_ROUNDS = 10 const MIN_PASSWORD_LENGTH = 10 -export class UserService implements IUserService { +export class AuthService implements IAuthService { constructor(protected readonly db: IDatabase) {} async createUser(payload: INewUser): Promise { diff --git a/packages/server/src/services/IUserService.ts b/packages/server/src/services/IAuthService.ts similarity index 92% rename from packages/server/src/services/IUserService.ts rename to packages/server/src/services/IAuthService.ts index cb07827..f92c02e 100644 --- a/packages/server/src/services/IUserService.ts +++ b/packages/server/src/services/IAuthService.ts @@ -1,6 +1,6 @@ import {ICredentials, INewUser, IUser} from '@rondo.dev/common' -export interface IUserService { +export interface IAuthService { createUser(credentials: INewUser): Promise changePassword(params: { userId: number, diff --git a/packages/server/src/services/index.ts b/packages/server/src/services/index.ts index c0b0049..7c10df8 100644 --- a/packages/server/src/services/index.ts +++ b/packages/server/src/services/index.ts @@ -1,3 +1,3 @@ export * from './BaseService' -export * from './IUserService' -export * from './UserService' +export * from './IAuthService' +export * from './AuthService'