diff --git a/packages/client/src/components/Input.tsx b/packages/client/src/components/Input.tsx index f2c9949..bbae07d 100644 --- a/packages/client/src/components/Input.tsx +++ b/packages/client/src/components/Input.tsx @@ -11,6 +11,7 @@ export interface IInputProps { readOnly?: boolean label: string Icon?: IconType + required?: boolean } export class Input extends React.PureComponent { @@ -33,6 +34,7 @@ export class Input extends React.PureComponent { onChange={this.handleChange} placeholder={this.props.placeholder} readOnly={!!this.props.readOnly} + required={this.props.required} /> {Icon && diff --git a/packages/client/src/login/LoginActions.ts b/packages/client/src/login/LoginActions.ts index 825d45b..b5261f2 100644 --- a/packages/client/src/login/LoginActions.ts +++ b/packages/client/src/login/LoginActions.ts @@ -1,5 +1,5 @@ import {IAction, IErrorAction, ActionTypes} from '../actions' -import {IAPIDef, ICredentials, IUser} from '@rondo/common' +import {IAPIDef, ICredentials, INewUser, IUser} from '@rondo/common' import {IHTTPClient} from '../http/IHTTPClient' export enum LoginActionKeys { @@ -44,7 +44,7 @@ export class LoginActions { } } - register = (profile: ICredentials): + register = (profile: INewUser): IAction => { return { payload: this.http.post('/auth/register', profile), diff --git a/packages/client/src/login/RegisterForm.tsx b/packages/client/src/login/RegisterForm.tsx index 3a6a98a..bb02701 100644 --- a/packages/client/src/login/RegisterForm.tsx +++ b/packages/client/src/login/RegisterForm.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {ICredentials, IUser} from '@rondo/common' +import {INewUser, IUser} from '@rondo/common' import {Input} from '../components/Input' import {Redirect} from '../components/Redirect' @@ -7,7 +7,7 @@ export interface IRegisterFormProps { error?: string onSubmit: () => void onChange: (name: string, value: string) => void - data: ICredentials + data: INewUser user?: IUser redirectTo: string } @@ -21,12 +21,13 @@ export class RegisterForm extends React.PureComponent {

{this.props.error}

{ onChange={this.props.onChange} value={this.props.data.password} placeholder='Password' + required + /> + + { beforeEach(() => { app = express() + const userInfo = { + username: 'test@user.com', + firstName: 'test', + lastName: 'test', + } const userService = new (class implements IUserService { async createUser() { - return {id: 1} + return {id: 1, ...userInfo} } async changePassword() {/* empty */} async findOne(id: number) { - return {id} + return {id, ...userInfo} } async validateCredentials({username, password}: ICredentials) { if (username === 'test' && password === 'pass') { - return {id: 1} + return {id: 1, ...userInfo} return } if (username === 'error') { diff --git a/packages/server/src/routes/LoginRoutes.ts b/packages/server/src/routes/LoginRoutes.ts index b3c28e9..9b27c99 100644 --- a/packages/server/src/routes/LoginRoutes.ts +++ b/packages/server/src/routes/LoginRoutes.ts @@ -18,6 +18,8 @@ export class LoginRoutes extends BaseRoute { const user = await this.userService.createUser({ username: req.body.username, password: req.body.password, + firstName: req.body.firstName, + lastName: req.body.lastName, }) await req.logInPromise(user) return user diff --git a/packages/server/src/services/IUserService.ts b/packages/server/src/services/IUserService.ts index 42dcc21..cc2e334 100644 --- a/packages/server/src/services/IUserService.ts +++ b/packages/server/src/services/IUserService.ts @@ -1,8 +1,7 @@ -import {ICredentials} from '@rondo/common' -import {IUser} from '@rondo/common' +import {ICredentials, INewUser, IUser} from '@rondo/common' export interface IUserService { - createUser(credentials: ICredentials): Promise + createUser(credentials: INewUser): Promise changePassword(params: { userId: number, oldPassword: string, diff --git a/packages/server/src/services/UserService.test.ts b/packages/server/src/services/UserService.test.ts index bbb9c26..efecbaf 100644 --- a/packages/server/src/services/UserService.test.ts +++ b/packages/server/src/services/UserService.test.ts @@ -14,6 +14,8 @@ describe('UserService', () => { return userService.createUser({ username: u, password: p, + firstName: 'test', + lastName: 'test', }) } @@ -23,7 +25,7 @@ describe('UserService', () => { expect(result.id).toBeTruthy() const user = await userService.findOne(result.id) expect(user).toBeTruthy() - expect(user!.password).toBe(undefined) + expect(user).not.toHaveProperty('password') }) it('throws when username is not an email', async () => { @@ -88,7 +90,7 @@ describe('UserService', () => { await createUser() const user = await userService .validateCredentials({ username, password }) - expect(user!.password).toBe(undefined) + expect(user).not.toHaveProperty('password') }) }) diff --git a/packages/server/src/services/UserService.ts b/packages/server/src/services/UserService.ts index e6b0c2a..22b09cc 100644 --- a/packages/server/src/services/UserService.ts +++ b/packages/server/src/services/UserService.ts @@ -1,38 +1,68 @@ import createError from 'http-errors' import {BaseService} from './BaseService' -import {ICredentials} from '@rondo/common' +import {ICredentials, INewUser, IUser} from '@rondo/common' import {IUserService} from './IUserService' import {UserEmail} from '../entities/UserEmail' import {User} from '../entities/User' import {compare, hash} from 'bcrypt' import {validate as validateEmail} from 'email-validator' +import {Validator, trim} from '../validator' const SALT_ROUNDS = 10 const MIN_PASSWORD_LENGTH = 10 export class UserService extends BaseService implements IUserService { - async createUser(payload: ICredentials): Promise { - const username = payload.username - if (!validateEmail(username)) { + async createUser(payload: INewUser): Promise { + const newUser = { + username: trim(payload.username), + firstName: trim(payload.firstName), + lastName: trim(payload.lastName), + } + + if (!validateEmail(newUser.username)) { throw createError(400, 'Username is not a valid e-mail') } if (payload.password.length < MIN_PASSWORD_LENGTH) { throw createError(400, `Password must be at least ${MIN_PASSWORD_LENGTH} characters long`) } + + new Validator(newUser) + .ensure('username') + .ensure('firstName') + .ensure('lastName') + .throw() + const password = await this.hash(payload.password) const user = await this.getRepository(User).save({ + ...newUser, password, }) await this.getRepository(UserEmail).save({ - email: username, + email: newUser.username, userId: user.id, }) - return user + return { + id: user.id, + ...newUser, + } } async findOne(id: number) { - return this.getRepository(User).findOne(id) + const user = await this.getRepository(User).findOne(id, { + relations: ['emails'], + }) + + if (!user) { + return undefined + } + + return { + id: user.id, + username: user.emails[0] ? user.emails[0].email : '', + firstName: user.firstName, + lastName: user.lastName, + } } async findUserByEmail(email: string) { @@ -71,6 +101,7 @@ export class UserService extends BaseService implements IUserService { .createQueryBuilder('user') .select('user') .addSelect('user.password') + .addSelect('emails') .innerJoin('user.emails', 'emails', 'emails.email = :email', { email: username, }) @@ -78,8 +109,12 @@ export class UserService extends BaseService implements IUserService { const isValid = await compare(password, user ? user.password! : '') if (user && isValid) { - delete user.password - return user + return { + id: user.id, + username: user.emails[0].email, + firstName: user.firstName, + lastName: user.lastName, + } } } diff --git a/packages/server/src/test-utils/TestUtils.ts b/packages/server/src/test-utils/TestUtils.ts index 8b8e1f6..43763db 100644 --- a/packages/server/src/test-utils/TestUtils.ts +++ b/packages/server/src/test-utils/TestUtils.ts @@ -75,7 +75,7 @@ export class TestUtils { const response = await supertest(this.app) .get(`${context}/app`) .expect(200) - const cookie = response.header['set-cookie'] as string + const cookie = this.getCookies(response.header['set-cookie']) const token = this.getCsrfToken(response.text) expect(cookie).toBeTruthy() expect(token).toBeTruthy() @@ -101,11 +101,17 @@ export class TestUtils { const response = await supertest(this.app) .post(`${context}/api/auth/register`) .set('cookie', cookie) - .send(this.getLoginBody(token)) + .send({ + firstName: 'test', + lastName: 'test', + ...this.getLoginBody(token), + }) .expect(200) + const cookies = this.getCookies(response.header['set-cookie']) + return { - cookie: response.header['set-cookie'] as string, + cookie: [cookies, cookie].join('; '), userId: response.body.id, token, } @@ -121,7 +127,12 @@ export class TestUtils { .send({username, password, _csrf: token}) .expect(200) - return {cookie: response.header['set-cookie'] as string, token} + const cookies = this.getCookies(response.header['set-cookie']) + + return { + cookie: [cookies, cookie].join('; '), + token, + } } request = (baseUrl = '') => { @@ -130,4 +141,8 @@ export class TestUtils { `${this.bootstrap.config.app.baseUrl.path!}${baseUrl}`) } + private getCookies(setCookiesString: string[]): string { + return setCookiesString.map(c => c.split('; ')[0]).join('; ') + } + } diff --git a/packages/server/src/validator/index.ts b/packages/server/src/validator/index.ts index 31baf0f..e66861d 100644 --- a/packages/server/src/validator/index.ts +++ b/packages/server/src/validator/index.ts @@ -1,3 +1,4 @@ export * from './IValidationMessage' export * from './ValidationError' export * from './Validator' +export * from './trim' diff --git a/packages/server/src/validator/trim.test.ts b/packages/server/src/validator/trim.test.ts new file mode 100644 index 0000000..dfdcd99 --- /dev/null +++ b/packages/server/src/validator/trim.test.ts @@ -0,0 +1,9 @@ +import {trim} from './trim' + +describe('trim', () => { + it('trims string', () => { + expect(trim('test')).toEqual('test') + expect(trim(' test ')).toEqual('test') + expect(trim(undefined)).toEqual('') + }) +}) diff --git a/packages/server/src/validator/trim.ts b/packages/server/src/validator/trim.ts new file mode 100644 index 0000000..fe46baa --- /dev/null +++ b/packages/server/src/validator/trim.ts @@ -0,0 +1,6 @@ +export function trim(str?: string) { + if (!str) { + return '' + } + return str.trim() +}