Skip to content

Commit

Permalink
feat: don't require password when creating user
Browse files Browse the repository at this point in the history
* feat: don't require password when creating user
  • Loading branch information
blackmann authored Dec 28, 2023
1 parent 53ffd96 commit ab3acc5
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 24 deletions.
72 changes: 68 additions & 4 deletions base/src/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('setup', () => {
data: {
email: '[email protected]',
fullname: 'Mock User',
password: 'hello',
password: 'helloworld',
username: 'mock-1',
},
method: 'create',
Expand All @@ -60,7 +60,7 @@ describe('setup', () => {
const res = await app.api(
context({
data: {
password: 'hello',
password: 'helloworld',
username: 'mock',
},
method: 'create',
Expand All @@ -77,7 +77,7 @@ describe('setup', () => {
data: {
email: '[email protected]',
fullname: 'Mock User',
password: 'hello',
password: 'helloworld',
role: 'dev',
username: 'mock',
},
Expand Down Expand Up @@ -819,7 +819,7 @@ describe('hooks registry', () => {
'log-data',
'custom-code',
'restrict-method',
'auth-require-password',
'auth-collect-password',
'create-auth-credential',
'require-auth',
'assign-auth-user',
Expand All @@ -832,3 +832,67 @@ describe('hooks registry', () => {
)
})
})

describe('users/auth service', () => {
it('creates user', async () => {
const res = await app.api(
context({
data: {
email: '[email protected]',
fullname: 'Mock User',
password: 'helloworld',
username: 'mock-user-1',
},
method: 'create',
path: 'users',
})
)

expect(res.statusCode).toBe(201)
expect(res.result).toStrictEqual({
_id: expect.anything(),
created_at: expect.any(Date),
email: '[email protected]',
fullname: 'Mock User',
role: 'basic',
updated_at: expect.any(Date),
username: 'mock-user-1',
})
})

it('returns 400 when password is less than 8 characters', async () => {
const res = await app.api(
context({
data: {
email: '[email protected]',
fullname: 'Mock User',
password: 'hello',
username: 'mock-user-2',
},
method: 'create',
path: 'users',
})
)

expect(res.statusCode).toBe(400)
expect(res.result.error).toStrictEqual(
'`password` should be at least 8 characters long'
)
})

it('should create user without requiring password', async () => {
const res = await app.api(
context({
data: {
email: '[email protected]',
fullname: 'Mock User',
username: 'mock-user-3',
},
method: 'create',
path: 'users',
})
)

expect(res.statusCode).toBe(201)
})
})
6 changes: 3 additions & 3 deletions base/src/authentication.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe('authentication', () => {
data: {
email: '[email protected]',
fullname: 'Mock User',
password: 'hello',
password: 'helloworld',
username: 'mock-1',
},
method: 'create',
Expand All @@ -47,7 +47,7 @@ describe('authentication', () => {
const { result } = await app.api(
context({
data: {
password: 'hello',
password: 'helloworld',
username: 'mock-1',
},
method: 'create',
Expand All @@ -66,7 +66,7 @@ describe('authentication', () => {
context({
data: {
email: '[email protected]',
password: 'hello',
password: 'helloworld',
},
method: 'create',
path: 'login',
Expand Down
43 changes: 29 additions & 14 deletions base/src/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,25 @@ const authCredentialsSchema: SchemaDefinitions = {
user: { relation: 'users', required: true, type: 'id' },
}

const RequirePassword: Hook = {
description: 'Require password field. [Base auth]',
id: 'auth-require-password',
name: 'Require Password',
const CollectPassword: Hook = {
description:
'Collects password field if provided. This is used later by `CreatePasswordAuth`. [Base auth]',
id: 'auth-collect-password',
name: 'Collect Password',
run: async (ctx) => {
const password = ctx.data?.['password']
if (password === undefined || password === null) {
return ctx
}

if (typeof password !== 'string') {
throw new BadRequest('`password` field is required or should be a string')
}

if (password.length < 8) {
throw new BadRequest('`password` should be at least 8 characters long')
}

ctx.locals['password'] = password
delete ctx.data!['password']

Expand Down Expand Up @@ -70,6 +79,10 @@ const CreatePasswordAuthCredential: Hook = {
name: 'Create Password Auth Credential',
run: async (ctx, _, app) => {
const password = ctx.locals['password']
if (!password) {
return ctx
}

if (typeof password !== 'string') {
throw new BadRequest('`password` field is missing')
}
Expand Down Expand Up @@ -113,23 +126,23 @@ const CreatePasswordAuthCredential: Hook = {

/** This is the primary authentication mechanism */
async function baseAuthentication(app: App) {
const name = 'auth-credentials'
const collectionName = 'auth-credentials'
const secretKey = new TextEncoder().encode(process.env.SECRET_KEY!)

if (!(await app.manifest.collection(name))) {
if (!(await app.manifest.collection(collectionName))) {
const index = [{ fields: ['user'], options: { unique: true } }]
await app.manifest.collection(name, {
await app.manifest.collection(collectionName, {
indexes: index,
name,
name: collectionName,
readOnlySchema: true,
schema: authCredentialsSchema,
})

await app.database.addIndexes(name, index)
await app.database.addIndexes(collectionName, index)
}

app.hooksRegistry.register(
RequirePassword,
CollectPassword,
CreatePasswordAuthCredential,
RequireAuth,
AssignAuthUser
Expand Down Expand Up @@ -167,7 +180,9 @@ async function baseAuthentication(app: App) {
throw new BadRequest('Incorrect username/password combination')
}

const credentialService = app.service(unexposed(name)) as CollectionService
const credentialService = app.service(
unexposed(collectionName)
) as CollectionService

const {
data: [credential],
Expand Down Expand Up @@ -226,10 +241,10 @@ async function upsertHooks(app: App) {

if (
!usersHooks['before']['create'].find(
([hookId]) => hookId === RequirePassword.id
([hookId]) => hookId === CollectPassword.id
)
) {
usersHooks.before.create.push([RequirePassword.id])
usersHooks.before.create.push([CollectPassword.id])
dirty = true
}

Expand Down Expand Up @@ -312,4 +327,4 @@ function checkSecretKeyEnv() {
}
}

export { RequirePassword, baseAuthentication }
export { CollectPassword, baseAuthentication }
9 changes: 6 additions & 3 deletions base/src/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ const usersSchema: SchemaDefinitions = {
avatar: { type: 'string' },
email: { required: true, type: 'string', unique: true },
fullname: { required: true, type: 'string' },
// basic, dev
// [ ] Prevent anyone from just creating a 'dev' account. Use hook
role: { defaultValue: 'basic', required: true, type: 'string' },
role: {
defaultValue: 'basic',
enum: ['dev', 'basic'],
required: true,
type: 'string',
},
username: { required: true, type: 'string', unique: true },
verified: { type: 'boolean' },
}

// [ ] Make collection schema read-only
async function users(app: App) {
if (!(await app.manifest.collection('users'))) {
const indexes = [
Expand Down

0 comments on commit ab3acc5

Please sign in to comment.