Rename old UserService to AuthService, LoginRoutes to AuthRoutes

This commit is contained in:
Jerko Steiner 2019-09-05 10:19:24 +07:00
parent 0a1eabadd9
commit 5cf54d0be9
15 changed files with 128 additions and 114 deletions

View File

@ -20,7 +20,7 @@ export interface IAPIDef {
'/auth/logout': { '/auth/logout': {
'get': {} 'get': {}
} }
'/users/password': { '/auth/password': {
'post': { 'post': {
body: { body: {
oldPassword: string oldPassword: string

View File

@ -1,9 +1,9 @@
import {IUserService} from '../services' import {IAuthService} from '../services'
import {ITeamService} from '../team' import {ITeamService} from '../team'
import {IUserPermissions} from '../user' import {IUserPermissions} from '../user'
export interface IServices { export interface IServices {
userService: IUserService authService: IAuthService
teamService: ITeamService teamService: ITeamService
userPermissions: IUserPermissions userPermissions: IUserPermissions
} }

View File

@ -26,12 +26,12 @@ export const configureServer: ServerConfigurator = (config, database) => {
const logger = loggerFactory.getLogger('api') const logger = loggerFactory.getLogger('api')
const services: IServices = { const services: IServices = {
userService: new Services.UserService(database), authService: new Services.AuthService(database),
teamService: new Team.TeamService(database), teamService: new Team.TeamService(database),
userPermissions: new User.UserPermissions(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 transactionManager = database.transactionManager
const createTransactionalRouter = <T extends IRoutes>() => const createTransactionalRouter = <T extends IRoutes>() =>
@ -73,13 +73,13 @@ export const configureServer: ServerConfigurator = (config, database) => {
api: { api: {
path: '/api', path: '/api',
handle: [ handle: [
new routes.LoginRoutes( new routes.AuthRoutes(
services.userService, services.authService,
authenticator, authenticator,
createTransactionalRouter(), createTransactionalRouter(),
).handle, ).handle,
new routes.UserRoutes( new routes.UserRoutes(
services.userService, services.authService,
createTransactionalRouter(), createTransactionalRouter(),
).handle, ).handle,
new Team.TeamRoutes( new Team.TeamRoutes(

View File

@ -2,11 +2,11 @@ import express, {Application} from 'express'
import request from 'supertest' import request from 'supertest'
import {Authenticator} from './Authenticator' import {Authenticator} from './Authenticator'
import {ICredentials} from '@rondo.dev/common' import {ICredentials} from '@rondo.dev/common'
import {IUserService} from '../services' import {IAuthService} from '../services'
import {handlePromise} from './handlePromise' import {handlePromise} from './handlePromise'
import {urlencoded} from 'body-parser' import {urlencoded} from 'body-parser'
describe('passport.promise', () => { describe('Authenticator', () => {
let app: Application let app: Application
let loginMiddleware: any let loginMiddleware: any
@ -18,7 +18,7 @@ describe('passport.promise', () => {
firstName: 'test', firstName: 'test',
lastName: 'test', lastName: 'test',
} }
const userService = new (class implements IUserService { const authService = new (class implements IAuthService {
async createUser() { async createUser() {
return {id: 1, ...userInfo} return {id: 1, ...userInfo}
} }
@ -40,7 +40,7 @@ describe('passport.promise', () => {
return undefined return undefined
} }
})() })()
const authenticator = new Authenticator(userService) const authenticator = new Authenticator(authService)
app.use(urlencoded({ extended: false })) app.use(urlencoded({ extended: false }))
app.use(authenticator.handle) app.use(authenticator.handle)

View File

@ -1,5 +1,5 @@
import {Authenticator as A, Passport} from 'passport' import {Authenticator as A, Passport} from 'passport'
import {IUserService} from '../services' import {IAuthService} from '../services'
import {Strategy as LocalStrategy} from 'passport-local' import {Strategy as LocalStrategy} from 'passport-local'
import {THandler} from './THandler' import {THandler} from './THandler'
import {IMiddleware} from './IMiddleware' import {IMiddleware} from './IMiddleware'
@ -9,7 +9,7 @@ export class Authenticator implements IMiddleware {
protected readonly passport: A protected readonly passport: A
readonly handle: THandler[] readonly handle: THandler[]
constructor(protected readonly userService: IUserService) { constructor(protected readonly authService: IAuthService) {
this.passport = new Passport() as any this.passport = new Passport() as any
this.configurePassport() this.configurePassport()
@ -45,7 +45,7 @@ export class Authenticator implements IMiddleware {
protected deserializeUser = protected deserializeUser =
// TODO parametrize user type // TODO parametrize user type
(userId: number, done: (err?: Error, user?: any) => void) => { (userId: number, done: (err?: Error, user?: any) => void) => {
this.userService.findOne(userId) this.authService.findOne(userId)
.then(user => done(undefined, user)) .then(user => done(undefined, user))
.catch(done) .catch(done)
} }
@ -67,7 +67,7 @@ export class Authenticator implements IMiddleware {
password: string, password: string,
done: (err?: Error, user?: any) => void, done: (err?: Error, user?: any) => void,
) => { ) => {
this.userService.validateCredentials({ username, password }) this.authService.validateCredentials({ username, password })
.then(user => done(undefined, user)) .then(user => done(undefined, user))
.catch(done) .catch(done)
} }

View File

@ -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)
})
})
})

View File

@ -1,12 +1,13 @@
import {AsyncRouter} from '../router' import {AsyncRouter} from '../router'
import {BaseRoute} from './BaseRoute' import {BaseRoute} from './BaseRoute'
import {IAPIDef} from '@rondo.dev/common' import {IAPIDef} from '@rondo.dev/common'
import {IUserService} from '../services' import {IAuthService} from '../services'
import {Authenticator} from '../middleware' import {Authenticator} from '../middleware'
import {ensureLoggedInApi} from '../middleware'
export class LoginRoutes extends BaseRoute<IAPIDef> { export class AuthRoutes extends BaseRoute<IAPIDef> {
constructor( constructor(
protected readonly userService: IUserService, protected readonly authService: IAuthService,
protected readonly authenticator: Authenticator, protected readonly authenticator: Authenticator,
protected readonly t: AsyncRouter<IAPIDef>, protected readonly t: AsyncRouter<IAPIDef>,
) { ) {
@ -15,7 +16,7 @@ export class LoginRoutes extends BaseRoute<IAPIDef> {
setup(t: AsyncRouter<IAPIDef>) { setup(t: AsyncRouter<IAPIDef>) {
t.post('/auth/register', async (req, res) => { t.post('/auth/register', async (req, res) => {
const user = await this.userService.createUser({ const user = await this.authService.createUser({
username: req.body.username, username: req.body.username,
password: req.body.password, password: req.body.password,
firstName: req.body.firstName, firstName: req.body.firstName,
@ -37,6 +38,14 @@ export class LoginRoutes extends BaseRoute<IAPIDef> {
return user 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) => { t.get('/auth/logout', async (req, res) => {
req.logout() req.logout()
}) })

View File

@ -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)
})
})
})

View File

@ -15,31 +15,6 @@ describe('user', () => {
t.setHeaders({ cookie, 'x-csrf-token': token }) 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', () => { describe('GET /users/profile', () => {
it('fetches user profile', async () => { it('fetches user profile', async () => {
t.setHeaders({ cookie }) t.setHeaders({ cookie })

View File

@ -1,12 +1,12 @@
import {AsyncRouter} from '../router' import {AsyncRouter} from '../router'
import {BaseRoute} from './BaseRoute' import {BaseRoute} from './BaseRoute'
import {IAPIDef} from '@rondo.dev/common' import {IAPIDef} from '@rondo.dev/common'
import {IUserService} from '../services' import {IAuthService} from '../services'
import {ensureLoggedInApi} from '../middleware' import {ensureLoggedInApi} from '../middleware'
export class UserRoutes extends BaseRoute<IAPIDef> { export class UserRoutes extends BaseRoute<IAPIDef> {
constructor( constructor(
protected readonly userService: IUserService, protected readonly userService: IAuthService,
protected readonly t: AsyncRouter<IAPIDef>, protected readonly t: AsyncRouter<IAPIDef>,
) { ) {
super(t) super(t)
@ -15,14 +15,6 @@ export class UserRoutes extends BaseRoute<IAPIDef> {
setup(t: AsyncRouter<IAPIDef>) { setup(t: AsyncRouter<IAPIDef>) {
t.use('/users', ensureLoggedInApi) 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 => { t.get('/users/emails/:email', async req => {
return this.userService.findUserByEmail(req.params.email) return this.userService.findUserByEmail(req.params.email)
}) })

View File

@ -1,4 +1,4 @@
export * from './BaseRoute'
export * from './LoginRoutes'
export * from './UserRoutes'
export * from './application' export * from './application'
export * from './AuthRoutes'
export * from './BaseRoute'
export * from './UserRoutes'

View File

@ -1,17 +1,17 @@
import {test} from '../test' import {test} from '../test'
import {UserService} from './UserService' import {AuthService} from './AuthService'
describe('UserService', () => { describe('AuthService', () => {
test.withDatabase() test.withDatabase()
const username = test.username const username = test.username
const password = '1234567890' const password = '1234567890'
const userService = new UserService(test.bootstrap.database) const authService = new AuthService(test.bootstrap.database)
async function createUser(u = username, p = password) { async function createUser(u = username, p = password) {
return userService.createUser({ return authService.createUser({
username: u, username: u,
password: p, password: p,
firstName: 'test', firstName: 'test',
@ -23,7 +23,7 @@ describe('UserService', () => {
it('creates a new user with bcrypted password', async () => { it('creates a new user with bcrypted password', async () => {
const result = await createUser() const result = await createUser()
expect(result.id).toBeTruthy() expect(result.id).toBeTruthy()
const user = await userService.findOne(result.id) const user = await authService.findOne(result.id)
expect(user).toBeTruthy() expect(user).toBeTruthy()
expect(user).not.toHaveProperty('password') expect(user).not.toHaveProperty('password')
}) })
@ -48,7 +48,7 @@ describe('UserService', () => {
describe('findUserByMail', () => { describe('findUserByMail', () => {
it('returns user without password', async () => { it('returns user without password', async () => {
await createUser() await createUser()
const user = await userService.findUserByEmail(username) const user = await authService.findUserByEmail(username)
expect(user).toBeTruthy() expect(user).toBeTruthy()
expect(user).not.toHaveProperty('password') expect(user).not.toHaveProperty('password')
}) })
@ -57,7 +57,7 @@ describe('UserService', () => {
describe('getUserEmails', () => { describe('getUserEmails', () => {
it('returns user emails', async () => { it('returns user emails', async () => {
const {id} = await createUser() const {id} = await createUser()
const emails = await userService.findUserEmails(id) const emails = await authService.findUserEmails(id)
expect(emails).toEqual([{ expect(emails).toEqual([{
id: jasmine.any(Number), id: jasmine.any(Number),
userId: id, userId: id,
@ -71,24 +71,24 @@ describe('UserService', () => {
describe('validateCredentials', () => { describe('validateCredentials', () => {
it('returns user when password is valid', async () => { it('returns user when password is valid', async () => {
await createUser() await createUser()
expect(await userService.validateCredentials({ username, password })) expect(await authService.validateCredentials({ username, password }))
.toBeTruthy() .toBeTruthy()
}) })
it('returns undefined when no user', async () => { it('returns undefined when no user', async () => {
expect(await userService.validateCredentials({ username, password })) expect(await authService.validateCredentials({ username, password }))
.toBe(undefined) .toBe(undefined)
}) })
it('returns undefined when password is invalid', async () => { it('returns undefined when password is invalid', async () => {
await createUser() await createUser()
expect(await userService.validateCredentials({ username, password: 't' })) expect(await authService.validateCredentials({ username, password: 't' }))
.toBe(undefined) .toBe(undefined)
}) })
it('does not return a password', async () => { it('does not return a password', async () => {
await createUser() await createUser()
const user = await userService const user = await authService
.validateCredentials({ username, password }) .validateCredentials({ username, password })
expect(user).not.toHaveProperty('password') expect(user).not.toHaveProperty('password')
}) })

View File

@ -2,7 +2,7 @@ import createError from 'http-errors'
import {BaseService} from './BaseService' import {BaseService} from './BaseService'
import {IDatabase} from '../database/IDatabase' import {IDatabase} from '../database/IDatabase'
import {ICredentials, INewUser, IUser, trim} from '@rondo.dev/common' import {ICredentials, INewUser, IUser, trim} from '@rondo.dev/common'
import {IUserService} from './IUserService' import {IAuthService} from './IAuthService'
import {UserEmail} from '../entities/UserEmail' import {UserEmail} from '../entities/UserEmail'
import {User} from '../entities/User' import {User} from '../entities/User'
import {compare, hash} from 'bcrypt' import {compare, hash} from 'bcrypt'
@ -12,7 +12,7 @@ import {Validator} from '../validator'
const SALT_ROUNDS = 10 const SALT_ROUNDS = 10
const MIN_PASSWORD_LENGTH = 10 const MIN_PASSWORD_LENGTH = 10
export class UserService implements IUserService { export class AuthService implements IAuthService {
constructor(protected readonly db: IDatabase) {} constructor(protected readonly db: IDatabase) {}
async createUser(payload: INewUser): Promise<IUser> { async createUser(payload: INewUser): Promise<IUser> {

View File

@ -1,6 +1,6 @@
import {ICredentials, INewUser, IUser} from '@rondo.dev/common' import {ICredentials, INewUser, IUser} from '@rondo.dev/common'
export interface IUserService { export interface IAuthService {
createUser(credentials: INewUser): Promise<IUser> createUser(credentials: INewUser): Promise<IUser>
changePassword(params: { changePassword(params: {
userId: number, userId: number,

View File

@ -1,3 +1,3 @@
export * from './BaseService' export * from './BaseService'
export * from './IUserService' export * from './IAuthService'
export * from './UserService' export * from './AuthService'