mirror of
				https://github.com/strapi/strapi.git
				synced 2025-10-31 01:47:13 +00:00 
			
		
		
		
	Add register route
Signed-off-by: Alexandre Bodin <bodin.alex@gmail.com>
This commit is contained in:
		
							parent
							
								
									01854f431f
								
							
						
					
					
						commit
						a841400f85
					
				| @ -69,6 +69,11 @@ | ||||
|       "path": "/registration-info", | ||||
|       "handler": "authentication.registrationInfo" | ||||
|     }, | ||||
|     { | ||||
|       "method": "POST", | ||||
|       "path": "/register", | ||||
|       "handler": "authentication.register" | ||||
|     }, | ||||
|     { | ||||
|       "method": "POST", | ||||
|       "path": "/auth/local/register", | ||||
|  | ||||
| @ -3,6 +3,8 @@ | ||||
| const passport = require('koa-passport'); | ||||
| const compose = require('koa-compose'); | ||||
| 
 | ||||
| const { validateRegistrationInput } = require('../validation/authentication'); | ||||
| 
 | ||||
| module.exports = { | ||||
|   login: compose([ | ||||
|     (ctx, next) => { | ||||
| @ -68,4 +70,23 @@ module.exports = { | ||||
| 
 | ||||
|     ctx.body = { data: registrationInfo }; | ||||
|   }, | ||||
| 
 | ||||
|   async register(ctx) { | ||||
|     const input = ctx.request.body; | ||||
| 
 | ||||
|     try { | ||||
|       await validateRegistrationInput(input); | ||||
|     } catch (err) { | ||||
|       return ctx.badRequest('ValidationError', err); | ||||
|     } | ||||
| 
 | ||||
|     const user = await strapi.admin.services.user.register(input); | ||||
| 
 | ||||
|     ctx.body = { | ||||
|       data: { | ||||
|         token: strapi.admin.services.token.createJwtToken(user), | ||||
|         user: strapi.admin.services.user.sanitizeUser(user), | ||||
|       }, | ||||
|     }; | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| @ -78,6 +78,27 @@ describe('User', () => { | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('update', () => { | ||||
|     test('Forwards call to the query layer', async () => { | ||||
|       const user = { | ||||
|         email: 'test@strapi.io', | ||||
|       }; | ||||
|       const update = jest.fn(() => Promise.resolve(user)); | ||||
| 
 | ||||
|       global.strapi = { | ||||
|         query() { | ||||
|           return { update }; | ||||
|         }, | ||||
|       }; | ||||
|       const params = { id: 1 }; | ||||
|       const input = { email: 'test@strapi.io' }; | ||||
|       const result = await userService.update(params, input); | ||||
| 
 | ||||
|       expect(update).toHaveBeenCalledWith(params, input); | ||||
|       expect(result).toBe(user); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('exists', () => { | ||||
|     test('Return true if the user already exists', async () => { | ||||
|       const count = jest.fn(() => Promise.resolve(1)); | ||||
| @ -148,4 +169,142 @@ describe('User', () => { | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('register', () => { | ||||
|     test('Fails if no matching user is found', async () => { | ||||
|       const findOne = jest.fn(() => Promise.resolve(undefined)); | ||||
| 
 | ||||
|       global.strapi = { | ||||
|         query() { | ||||
|           return { | ||||
|             findOne, | ||||
|           }; | ||||
|         }, | ||||
|         errors: { | ||||
|           badRequest(msg) { | ||||
|             throw new Error(msg); | ||||
|           }, | ||||
|         }, | ||||
|       }; | ||||
| 
 | ||||
|       const input = { | ||||
|         registrationToken: '123', | ||||
|         userInfo: { | ||||
|           firstname: 'test', | ||||
|           lastname: 'Strapi', | ||||
|           password: 'Test1234', | ||||
|         }, | ||||
|       }; | ||||
| 
 | ||||
|       expect(userService.register(input)).rejects.toThrowError('Invalid registration info'); | ||||
|     }); | ||||
| 
 | ||||
|     test('Create a password hash', async () => { | ||||
|       const findOne = jest.fn(() => Promise.resolve({ id: 1 })); | ||||
|       const update = jest.fn(user => Promise.resolve(user)); | ||||
|       const hashPassword = jest.fn(() => Promise.resolve('123456789')); | ||||
| 
 | ||||
|       global.strapi = { | ||||
|         query() { | ||||
|           return { | ||||
|             findOne, | ||||
|           }; | ||||
|         }, | ||||
|         admin: { | ||||
|           services: { | ||||
|             user: { update }, | ||||
|             auth: { hashPassword }, | ||||
|           }, | ||||
|         }, | ||||
|       }; | ||||
| 
 | ||||
|       const input = { | ||||
|         registrationToken: '123', | ||||
|         userInfo: { | ||||
|           firstname: 'test', | ||||
|           lastname: 'Strapi', | ||||
|           password: 'Test1234', | ||||
|         }, | ||||
|       }; | ||||
| 
 | ||||
|       await userService.register(input); | ||||
| 
 | ||||
|       expect(hashPassword).toHaveBeenCalledWith('Test1234'); | ||||
|       expect(update).toHaveBeenCalledWith( | ||||
|         { id: 1 }, | ||||
|         expect.objectContaining({ password: '123456789' }) | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     test('Set user firstname and lastname', async () => { | ||||
|       const findOne = jest.fn(() => Promise.resolve({ id: 1 })); | ||||
|       const update = jest.fn(user => Promise.resolve(user)); | ||||
|       const hashPassword = jest.fn(() => Promise.resolve('123456789')); | ||||
| 
 | ||||
|       global.strapi = { | ||||
|         query() { | ||||
|           return { | ||||
|             findOne, | ||||
|           }; | ||||
|         }, | ||||
|         admin: { | ||||
|           services: { | ||||
|             user: { update }, | ||||
|             auth: { hashPassword }, | ||||
|           }, | ||||
|         }, | ||||
|       }; | ||||
| 
 | ||||
|       const input = { | ||||
|         registrationToken: '123', | ||||
|         userInfo: { | ||||
|           firstname: 'test', | ||||
|           lastname: 'Strapi', | ||||
|           password: 'Test1234', | ||||
|         }, | ||||
|       }; | ||||
| 
 | ||||
|       await userService.register(input); | ||||
| 
 | ||||
|       expect(hashPassword).toHaveBeenCalledWith('Test1234'); | ||||
|       expect(update).toHaveBeenCalledWith( | ||||
|         { id: 1 }, | ||||
|         expect.objectContaining({ firstname: 'test', lastname: 'Strapi' }) | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     test('Set user to active', async () => { | ||||
|       const findOne = jest.fn(() => Promise.resolve({ id: 1 })); | ||||
|       const update = jest.fn(user => Promise.resolve(user)); | ||||
|       const hashPassword = jest.fn(() => Promise.resolve('123456789')); | ||||
| 
 | ||||
|       global.strapi = { | ||||
|         query() { | ||||
|           return { | ||||
|             findOne, | ||||
|           }; | ||||
|         }, | ||||
|         admin: { | ||||
|           services: { | ||||
|             user: { update }, | ||||
|             auth: { hashPassword }, | ||||
|           }, | ||||
|         }, | ||||
|       }; | ||||
| 
 | ||||
|       const input = { | ||||
|         registrationToken: '123', | ||||
|         userInfo: { | ||||
|           firstname: 'test', | ||||
|           lastname: 'Strapi', | ||||
|           password: 'Test1234', | ||||
|         }, | ||||
|       }; | ||||
| 
 | ||||
|       await userService.register(input); | ||||
| 
 | ||||
|       expect(hashPassword).toHaveBeenCalledWith('Test1234'); | ||||
|       expect(update).toHaveBeenCalledWith({ id: 1 }, expect.objectContaining({ isActive: true })); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @ -25,6 +25,16 @@ const create = async attributes => { | ||||
|   return strapi.query('user', 'admin').create(user); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Update a user in database | ||||
|  * @param params query params to find the user to update | ||||
|  * @param attributes A partial user object | ||||
|  * @returns {Promise<user>} | ||||
|  */ | ||||
| const update = async (params, attributes) => { | ||||
|   return strapi.query('user', 'admin').update(params, attributes); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Check if a user with specific attributes exists in the database | ||||
|  * @param attributes A partial user object | ||||
| @ -49,9 +59,38 @@ const findRegistrationInfo = async registrationToken => { | ||||
|   return _.pick(user, ['email', 'firstname', 'lastname']); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Registers a user based on a registrationToken and some informations to update | ||||
|  * @param {Object} params | ||||
|  * @param {Object} params.registrationInfo registration token | ||||
|  * @param {Object} params.userInfo user info | ||||
|  */ | ||||
| const register = async ({ registrationToken, userInfo }) => { | ||||
|   const matchingUser = await strapi.query('user', 'admin').findOne({ registrationToken }); | ||||
| 
 | ||||
|   if (!matchingUser) { | ||||
|     throw strapi.errors.badRequest('Invalid registration info'); | ||||
|   } | ||||
| 
 | ||||
|   const hashedPassword = await strapi.admin.services.auth.hashPassword(userInfo.password); | ||||
| 
 | ||||
|   return strapi.admin.services.user.update( | ||||
|     { id: matchingUser.id }, | ||||
|     { | ||||
|       password: hashedPassword, | ||||
|       firstname: userInfo.firstname, | ||||
|       lastname: userInfo.lastname, | ||||
|       registrationToken: null, | ||||
|       isActive: true, | ||||
|     } | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| module.exports = { | ||||
|   sanitizeUser, | ||||
|   create, | ||||
|   update, | ||||
|   exists, | ||||
|   findRegistrationInfo, | ||||
|   register, | ||||
| }; | ||||
|  | ||||
| @ -234,4 +234,105 @@ describe('Admin Auth End to End', () => { | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('GET /register', () => { | ||||
|     test('Fails on missing payload', async () => { | ||||
|       const res = await rq({ | ||||
|         url: '/admin/register', | ||||
|         method: 'POST', | ||||
|         body: { | ||||
|           userInfo: {}, | ||||
|         }, | ||||
|       }); | ||||
| 
 | ||||
|       expect(res.statusCode).toBe(400); | ||||
|       expect(res.body).toEqual({ | ||||
|         statusCode: 400, | ||||
|         error: 'Bad Request', | ||||
|         message: 'ValidationError', | ||||
|         data: { | ||||
|           registrationToken: ['registrationToken is a required field'], | ||||
| 
 | ||||
|           'userInfo.firstname': ['userInfo.firstname is a required field'], | ||||
|           'userInfo.lastname': ['userInfo.lastname is a required field'], | ||||
|           'userInfo.password': ['userInfo.password is a required field'], | ||||
|         }, | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     test('Fails on invalid password', async () => { | ||||
|       const user = { | ||||
|         email: 'test1@strapi.io', // FIXME: Have to increment emails until we can delete the users after each test
 | ||||
|         firstname: 'test', | ||||
|         lastname: 'strapi', | ||||
|       }; | ||||
|       const createRes = await createUser(user); | ||||
| 
 | ||||
|       const registrationToken = createRes.body.data.registrationToken; | ||||
| 
 | ||||
|       const res = await rq({ | ||||
|         url: '/admin/register', | ||||
|         method: 'POST', | ||||
|         body: { | ||||
|           registrationToken, | ||||
|           userInfo: { | ||||
|             firstname: 'test', | ||||
|             lastname: 'Strapi', | ||||
|             password: '123', | ||||
|           }, | ||||
|         }, | ||||
|       }); | ||||
| 
 | ||||
|       console.log(res.body.data); | ||||
| 
 | ||||
|       expect(res.statusCode).toBe(400); | ||||
|       expect(res.body).toEqual({ | ||||
|         statusCode: 400, | ||||
|         error: 'Bad Request', | ||||
|         message: 'ValidationError', | ||||
|         data: { | ||||
|           'userInfo.password': ['userInfo.password must contain at least one uppercase character'], | ||||
|         }, | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     test('Registers user correctly', async () => { | ||||
|       const user = { | ||||
|         email: 'test2@strapi.io', // FIXME: Have to increment emails until we can delete the users after each test
 | ||||
|         firstname: 'test', | ||||
|         lastname: 'strapi', | ||||
|       }; | ||||
|       const createRes = await createUser(user); | ||||
| 
 | ||||
|       const registrationToken = createRes.body.data.registrationToken; | ||||
| 
 | ||||
|       const userInfo = { | ||||
|         firstname: 'test', | ||||
|         lastname: 'Strapi', | ||||
|         password: '1Test2azda3', | ||||
|       }; | ||||
| 
 | ||||
|       const res = await rq({ | ||||
|         url: '/admin/register', | ||||
|         method: 'POST', | ||||
|         body: { | ||||
|           registrationToken, | ||||
|           userInfo, | ||||
|         }, | ||||
|       }); | ||||
| 
 | ||||
|       expect(res.statusCode).toBe(200); | ||||
|       expect(res.body.data).toEqual({ | ||||
|         token: expect.any(String), | ||||
|         user: { | ||||
|           email: user.email, | ||||
|           firstname: expect.any(String), | ||||
|           lastname: expect.any(String), | ||||
|           password: expect.any(String), | ||||
|         }, | ||||
|       }); | ||||
| 
 | ||||
|       expect(res.body.data.user.password === userInfo.password).toBe(false); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
							
								
								
									
										41
									
								
								packages/strapi-admin/validation/authentication.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								packages/strapi-admin/validation/authentication.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const { yup, formatYupErrors } = require('strapi-utils'); | ||||
| 
 | ||||
| const registrationSchema = yup | ||||
|   .object() | ||||
|   .shape({ | ||||
|     registrationToken: yup.string().required(), | ||||
|     userInfo: yup | ||||
|       .object() | ||||
|       .shape({ | ||||
|         firstname: yup | ||||
|           .string() | ||||
|           .min(1) | ||||
|           .required(), | ||||
|         lastname: yup | ||||
|           .string() | ||||
|           .min(1) | ||||
|           .required(), | ||||
|         password: yup | ||||
|           .string() | ||||
|           .min(8) | ||||
|           .matches(/[a-z]/, '${path} must contain at least one lowercase character') | ||||
|           .matches(/[A-Z]/, '${path} must contain at least one uppercase character') | ||||
|           .matches(/\d/, '${path} must contain at least one number') | ||||
|           .required(), | ||||
|       }) | ||||
|       .required() | ||||
|       .noUnknown(), | ||||
|   }) | ||||
|   .noUnknown(); | ||||
| 
 | ||||
| const validateRegistrationInput = data => { | ||||
|   return registrationSchema | ||||
|     .validate(data, { strict: true, abortEarly: false }) | ||||
|     .catch(error => Promise.reject(formatYupErrors(error))); | ||||
| }; | ||||
| 
 | ||||
| module.exports = { | ||||
|   validateRegistrationInput, | ||||
| }; | ||||
| @ -4,7 +4,9 @@ const { yup, formatYupErrors } = require('strapi-utils'); | ||||
| 
 | ||||
| const handleReject = error => Promise.reject(formatYupErrors(error)); | ||||
| 
 | ||||
| const userCreationSchema = yup.object().shape({ | ||||
| const userCreationSchema = yup | ||||
|   .object() | ||||
|   .shape({ | ||||
|     email: yup | ||||
|       .string() | ||||
|       .email() | ||||
| @ -21,7 +23,8 @@ const userCreationSchema = yup.object().shape({ | ||||
|       .array() | ||||
|       .min(1) | ||||
|       .required(), | ||||
| }); | ||||
|   }) | ||||
|   .noUnknown(); | ||||
| 
 | ||||
| const validateUserCreationInput = data => { | ||||
|   return userCreationSchema.validate(data, { strict: true, abortEarly: false }).catch(handleReject); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Alexandre Bodin
						Alexandre Bodin