Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 131 additions & 29 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,9 @@
"@fastify/swagger-ui": "^5.2.4",
"bson": "^6.10.4",
"fastify-raw-body": "^5.0.0",
"flowerbase": "file:flowerbase-1.0.0.tgz",
"jsonwebtoken": "^9.0.3",
"node-cron": "^4.2.1",
"undici": "^7.18.2"
}
}
}
8 changes: 7 additions & 1 deletion packages/demo/packages/backend/functions/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,11 @@
"private": false,
"run_as_system": true,
"disable_arg_logs": true
},
{
"name": "onUserCreation",
"private": false,
"run_as_system": true,
"disable_arg_logs": true
}
]
]
8 changes: 8 additions & 0 deletions packages/demo/packages/backend/functions/onUserCreation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = async function (payload) {
console.log("User created with payload:", payload)
// you can do whatever you want with the user object, for example:
// - send a welcome email
// - add the user to a mailing list
// - create a profile for the user in another collection
// - etc.
};
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export const Registration = () => {
await app.emailPasswordAuth.registerUser({
email: data.email,
password: data.password,
payload: {
tryingToAddCustomData: true,
}
});
} catch (err) {
alert("Error during registration");
Expand Down
33 changes: 33 additions & 0 deletions packages/flowerbase-client/src/__tests__/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,37 @@ describe('flowerbase-client auth', () => {
})
)
})

it('sends payload when registering a user', async () => {
global.fetch = jest.fn().mockResolvedValue({
ok: true,
text: async () => JSON.stringify({ status: 'ok' })
}) as unknown as typeof fetch

const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })

await app.emailPasswordAuth.registerUser({
email: 'john@doe.com',
password: 'secret123',
payload: {
tryingToAddCustomData: true,
role: 'student'
}
})

expect(global.fetch).toHaveBeenCalledWith(
'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/register',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
email: 'john@doe.com',
password: 'secret123',
payload: {
tryingToAddCustomData: true,
role: 'student'
}
})
})
)
})
})
6 changes: 3 additions & 3 deletions packages/flowerbase-client/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class App {
private readonly listeners = new Set<() => void>()

emailPasswordAuth: {
registerUser: (input: { email: string; password: string }) => Promise<unknown>
registerUser: (input: { email: string; password: string; payload?: Record<string, unknown> }) => Promise<unknown>
confirmUser: (input: { token: string; tokenId: string }) => Promise<unknown>
resendConfirmationEmail: (input: { email: string }) => Promise<unknown>
retryCustomConfirmation: (input: { email: string }) => Promise<unknown>
Expand Down Expand Up @@ -69,8 +69,8 @@ export class App {
this.sessionBootstrapPromise = this.bootstrapSessionOnLoad()

this.emailPasswordAuth = {
registerUser: ({ email, password }) =>
this.postProvider('/local-userpass/register', { email, password }),
registerUser: ({ email, password, payload }) =>
this.postProvider('/local-userpass/register', { email, password, payload }),
confirmUser: ({ token, tokenId }) =>
this.postProvider('/local-userpass/confirm', { token, tokenId }),
resendConfirmationEmail: ({ email }) =>
Expand Down
51 changes: 51 additions & 0 deletions packages/flowerbase/src/__test__/constants.customUserData.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const loadConstantsWithCustomUserData = async (
customUserDataConfig: Record<string, unknown>
) => {
jest.resetModules()

jest.doMock('../auth/utils', () => ({
loadAuthConfig: jest.fn(() => ({
auth_collection: 'auth_users',
auth_database: 'auth-db',
'local-userpass': {
disabled: false,
config: {}
}
})),
loadCustomUserData: jest.fn(() => customUserDataConfig)
}))

return import('../constants')
}

describe('AUTH_CONFIG custom user data config', () => {
it('uses custom user collection when custom_user_data.enabled is true', async () => {
const { AUTH_CONFIG, DB_NAME } = await loadConstantsWithCustomUserData({
enabled: true,
database_name: 'main',
collection_name: 'users',
user_id_field: 'id',
on_user_creation_function_name: 'onCreateUser'
})

expect(DB_NAME).toBe('main')
expect(AUTH_CONFIG.userCollection).toBe('users')
expect(AUTH_CONFIG.user_id_field).toBe('id')
expect(AUTH_CONFIG.on_user_creation_function_name).toBe('onCreateUser')
})

it('disables custom user collection when custom_user_data.enabled is false', async () => {
const { AUTH_CONFIG, DB_NAME } = await loadConstantsWithCustomUserData({
enabled: false,
database_name: 'main',
collection_name: 'users',
user_id_field: 'id',
on_user_creation_function_name: 'onCreateUser'
})

expect(DB_NAME).toBe('main')
expect(AUTH_CONFIG.userCollection).toBeUndefined()
expect(AUTH_CONFIG.user_id_field).toBeUndefined()
expect(AUTH_CONFIG.on_user_creation_function_name).toBeUndefined()
})
})
209 changes: 209 additions & 0 deletions packages/flowerbase/src/auth/__tests__/controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,102 @@ jest.mock('../../constants', () => ({
}
}))

const loadAuthControllerWithConfig = async (
authConfigOverrides: Record<string, unknown>
) => {
jest.resetModules()

jest.doMock('../../constants', () => ({
AUTH_CONFIG: {
authCollection: 'auth_users',
refreshTokensCollection: 'refresh_tokens',
userCollection: 'users',
user_id_field: 'id',
...authConfigOverrides
},
AUTH_DB_NAME: 'test-auth-db',
DB_NAME: 'test-db',
DEFAULT_CONFIG: {
REFRESH_TOKEN_TTL_DAYS: 1
}
}))

jest.doMock('../../state', () => ({
StateManager: {
select: jest.fn((key: string) => {
if (key === 'projectId') return 'test-project'
return undefined
})
}
}))

return import('../controller')
}

const buildProfileTestApp = ({
authUser,
customUser
}: {
authUser: Record<string, unknown>
customUser?: Record<string, unknown> | null
}) => {
let profileHandler: ((req: any) => Promise<unknown>) | undefined

const authCollection = {
createIndex: jest.fn().mockResolvedValue('ok'),
findOne: jest.fn().mockResolvedValue(authUser)
}

const refreshCollection = {
createIndex: jest.fn().mockResolvedValue('ok')
}

const usersCollection = {
findOne: jest.fn().mockResolvedValue(customUser ?? null)
}

const db = {
collection: jest.fn((name: string) => {
if (name === 'auth_users') return authCollection
if (name === 'refresh_tokens') return refreshCollection
if (name === 'users') return usersCollection
return {
createIndex: jest.fn().mockResolvedValue('ok'),
findOne: jest.fn().mockResolvedValue(null)
}
})
}

const app = {
mongo: {
client: {
db: jest.fn().mockReturnValue(db)
}
},
addHook: jest.fn(),
get: jest.fn(
(
path: string,
_opts: unknown,
handler: (req: any) => Promise<unknown>
) => {
if (path === '/profile') {
profileHandler = handler
}
}
),
post: jest.fn(),
delete: jest.fn(),
jwtAuthentication: jest.fn()
}

return {
app,
usersCollection,
getProfileHandler: () => profileHandler
}
}

describe('authController', () => {
it('creates a unique email index on the auth collection', async () => {
const authCollection = {
Expand Down Expand Up @@ -113,4 +209,117 @@ describe('authController', () => {
}
})
})

it('returns custom_data from linked custom user collection when custom user data is enabled', async () => {
const authUserId = '697349de5dc2c5850198cc06'

const { authController } = await loadAuthControllerWithConfig({
userCollection: 'users',
user_id_field: 'id'
})

const { app, usersCollection, getProfileHandler } = buildProfileTestApp({
authUser: {
_id: { toString: () => authUserId },
email: 'enabled@example.com',
identities: [{ provider_type: 'local-userpass' }],
custom_data: {
role: 'from-auth-users'
}
},
customUser: {
_id: 'custom-user-document-id',
id: authUserId,
name: 'Mario Rossi',
role: 'from-users-collection'
}
})

await authController(app as never)

const result = await getProfileHandler()?.({
user: {
typ: 'access',
id: authUserId
},
params: {
appId: 'flowerbase-e2e'
}
})

expect(usersCollection.findOne).toHaveBeenCalledWith({
id: authUserId
})

expect(result).toEqual({
user_id: authUserId,
domain_id: 'flowerbase-e2e',
identities: [{ provider_type: 'local-userpass' }],
custom_data: {
_id: 'custom-user-document-id',
id: authUserId,
name: 'Mario Rossi',
role: 'from-users-collection'
},
type: 'normal',
data: {
email: 'enabled@example.com'
}
})
})

it('returns auth_users.custom_data when custom user data is disabled', async () => {
const authUserId = '697349de5dc2c5850198cc06'

const { authController } = await loadAuthControllerWithConfig({
userCollection: undefined,
user_id_field: undefined
})

const { app, usersCollection, getProfileHandler } = buildProfileTestApp({
authUser: {
_id: { toString: () => authUserId },
email: 'disabled@example.com',
identities: [{ provider_type: 'local-userpass' }],
custom_data: {
role: 'student',
tenantId: 'tenant-1',
tryingToAddCustomData: true
}
},
customUser: {
id: authUserId,
role: 'this-should-not-be-used'
}
})

await authController(app as never)

const result = await getProfileHandler()?.({
user: {
typ: 'access',
id: authUserId
},
params: {
appId: 'flowerbase-e2e'
}
})

expect(usersCollection.findOne).not.toHaveBeenCalled()

expect(result).toEqual({
user_id: authUserId,
domain_id: 'flowerbase-e2e',
identities: [{ provider_type: 'local-userpass' }],
custom_data: {
role: 'student',
tenantId: 'tenant-1',
tryingToAddCustomData: true
},
type: 'normal',
data: {
email: 'disabled@example.com'
}
})
})
})
4 changes: 2 additions & 2 deletions packages/flowerbase/src/auth/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export async function authController(app: FastifyInstance) {
? await customUserDb
.collection<Record<string, unknown>>(userCollection)
.findOne({ [AUTH_CONFIG.user_id_field]: req.user.id })
: null
: authUser?.custom_data

const params = (req as unknown as { params?: { appId?: string } }).params
const stateProjectId = StateManager.select('projectId')
Expand Down Expand Up @@ -139,7 +139,7 @@ export async function authController(app: FastifyInstance) {

const user = userCollection && AUTH_CONFIG.user_id_field
? (await customUserDb.collection(userCollection).findOne({ [AUTH_CONFIG.user_id_field]: req.user.sub }))
: {}
: auth_user.custom_data ?? {}

res.status(201)
return {
Expand Down
Loading