User management events
This commit is contained in:
parent
957e90fe86
commit
d99f579bf6
|
@ -3,7 +3,6 @@ exports.Events = {
|
|||
USER_CREATED: "user:created",
|
||||
USER_UPDATED: "user:updated",
|
||||
USER_DELETED: "user:deleted",
|
||||
USER_PASSWORD_FORCE_RESET: "user:password:force:reset",
|
||||
|
||||
// USER / PERMISSIONS
|
||||
USER_PERMISSION_ADMIN_ASSIGNED: "user:admin:assigned",
|
||||
|
@ -15,9 +14,9 @@ exports.Events = {
|
|||
USER_INVITED: "user:invited",
|
||||
USER_INVITED_ACCEPTED: "user:invite:accepted",
|
||||
|
||||
// USER / SELF
|
||||
USER_SELF_UPDATED: "user:self:updated",
|
||||
USER_SELF_PASSWORD_UPDATED: "user:self:password:updated",
|
||||
// USER / PASSWORD
|
||||
USER_PASSWORD_FORCE_RESET: "user:password:force:reset",
|
||||
USER_PASSWORD_UPDATED: "user:password:updated",
|
||||
USER_PASSWORD_RESET_REQUESTED: "user:password:reset:requested",
|
||||
USER_PASSWORD_RESET: "user:password:reset",
|
||||
|
||||
|
|
|
@ -18,12 +18,6 @@ exports.deleted = user => {
|
|||
events.processEvent(Events.USER_DELETED, properties)
|
||||
}
|
||||
|
||||
// TODO
|
||||
exports.passwordForceReset = user => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.USER_PASSWORD_FORCE_RESET, properties)
|
||||
}
|
||||
|
||||
// PERMISSIONS
|
||||
|
||||
exports.permissionAdminAssigned = user => {
|
||||
|
@ -48,39 +42,33 @@ exports.permissionBuilderRemoved = user => {
|
|||
|
||||
// INVITE
|
||||
|
||||
// TODO
|
||||
exports.invited = user => {
|
||||
exports.invited = userInfo => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.USER_INVITED, properties)
|
||||
}
|
||||
|
||||
// TODO
|
||||
exports.inviteAccepted = user => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.USER_INVITED_ACCEPTED, properties)
|
||||
}
|
||||
|
||||
// SELF
|
||||
// PASSWORD
|
||||
|
||||
// TODO
|
||||
exports.selfUpdated = user => {
|
||||
exports.passwordForceReset = user => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.USER_SELF_UPDATED, properties)
|
||||
events.processEvent(Events.USER_PASSWORD_FORCE_RESET, properties)
|
||||
}
|
||||
|
||||
// TODO
|
||||
exports.selfPasswordUpdated = user => {
|
||||
exports.passwordUpdated = user => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.USER_SELF_PASSWORD_UPDATED, properties)
|
||||
events.processEvent(Events.USER_PASSWORD_UPDATED, properties)
|
||||
}
|
||||
|
||||
// TODO
|
||||
exports.passwordResetRequested = user => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.USER_PASSWORD_RESET_REQUESTED, properties)
|
||||
}
|
||||
|
||||
// TODO
|
||||
exports.passwordReset = user => {
|
||||
const properties = {}
|
||||
events.processEvent(Events.USER_PASSWORD_RESET, properties)
|
||||
|
|
|
@ -80,15 +80,14 @@ jest.mock("../../../events", () => {
|
|||
created: jest.fn(),
|
||||
updated: jest.fn(),
|
||||
deleted: jest.fn(),
|
||||
passwordForceReset: jest.fn(),
|
||||
permissionAdminAssigned: jest.fn(),
|
||||
permissionAdminRemoved: jest.fn(),
|
||||
permissionBuilderAssigned: jest.fn(),
|
||||
permissionBuilderRemoved: jest.fn(),
|
||||
invited: jest.fn(),
|
||||
inviteAccepted: jest.fn(),
|
||||
selfUpdated: jest.fn(),
|
||||
selfPasswordUpdated: jest.fn(),
|
||||
passwordForceReset: jest.fn(),
|
||||
passwordUpdated: jest.fn(),
|
||||
passwordResetRequested: jest.fn(),
|
||||
passwordReset: jest.fn(),
|
||||
},
|
||||
|
|
|
@ -12,8 +12,20 @@ exports.getGlobalUserByEmail = async email => {
|
|||
throw "Must supply an email address to view"
|
||||
}
|
||||
|
||||
return queryGlobalView(ViewNames.USER_BY_EMAIL, {
|
||||
const response = await queryGlobalView(ViewNames.USER_BY_EMAIL, {
|
||||
key: email.toLowerCase(),
|
||||
include_docs: true,
|
||||
})
|
||||
|
||||
if (response) {
|
||||
if (Array.isArray(response)) {
|
||||
for (let user of response) {
|
||||
delete user.password
|
||||
}
|
||||
} else {
|
||||
delete response.password
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
|
|
@ -112,6 +112,7 @@ export const reset = async (ctx: any) => {
|
|||
user,
|
||||
subject: "{{ company }} platform password reset",
|
||||
})
|
||||
events.user.passwordResetRequested(user)
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
|
@ -136,6 +137,9 @@ export const resetUpdate = async (ctx: any) => {
|
|||
ctx.body = {
|
||||
message: "password reset successfully.",
|
||||
}
|
||||
// remove password from the user before sending events
|
||||
delete user.password
|
||||
events.user.passwordReset(user)
|
||||
} catch (err) {
|
||||
ctx.throw(400, "Cannot reset password.")
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ const { encrypt } = require("@budibase/backend-core/encryption")
|
|||
const { newid } = require("@budibase/backend-core/utils")
|
||||
const { users } = require("../../../sdk")
|
||||
const { Cookies } = require("@budibase/backend-core/constants")
|
||||
const { events } = require("@budibase/backend-core")
|
||||
|
||||
function newApiKey() {
|
||||
return encrypt(`${getTenantId()}${SEPARATOR}${newid()}`)
|
||||
|
@ -110,8 +111,10 @@ exports.getSelf = async ctx => {
|
|||
exports.updateSelf = async ctx => {
|
||||
const db = getGlobalDB()
|
||||
const user = await db.get(ctx.user._id)
|
||||
let passwordChange = false
|
||||
if (ctx.request.body.password) {
|
||||
// changing password
|
||||
passwordChange = true
|
||||
ctx.request.body.password = await hash(ctx.request.body.password)
|
||||
// Log all other sessions out apart from the current one
|
||||
await platformLogout({
|
||||
|
@ -134,4 +137,11 @@ exports.updateSelf = async ctx => {
|
|||
_id: response.id,
|
||||
_rev: response.rev,
|
||||
}
|
||||
|
||||
// remove the old password from the user before sending events
|
||||
delete user.password
|
||||
events.user.updated(user)
|
||||
if (passwordChange) {
|
||||
events.user.passwordUpdated(user)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ const {
|
|||
users: usersCore,
|
||||
tenancy,
|
||||
db: dbUtils,
|
||||
events,
|
||||
} = require("@budibase/backend-core")
|
||||
|
||||
export const save = async (ctx: any) => {
|
||||
|
@ -121,6 +122,7 @@ export const invite = async (ctx: any) => {
|
|||
ctx.body = {
|
||||
message: "Invitation has been sent.",
|
||||
}
|
||||
events.user.invited(userInfo)
|
||||
}
|
||||
|
||||
export const inviteAccept = async (ctx: any) => {
|
||||
|
@ -128,14 +130,16 @@ export const inviteAccept = async (ctx: any) => {
|
|||
try {
|
||||
// info is an extension of the user object that was stored by global
|
||||
const { email, info }: any = await checkInviteCode(inviteCode)
|
||||
ctx.body = await tenancy.doInTenant(info.tenantId, () => {
|
||||
return users.save({
|
||||
ctx.body = await tenancy.doInTenant(info.tenantId, async () => {
|
||||
const user = await users.save({
|
||||
firstName,
|
||||
lastName,
|
||||
password,
|
||||
email,
|
||||
...info,
|
||||
})
|
||||
events.user.inviteAccepted(user)
|
||||
return user
|
||||
})
|
||||
} catch (err: any) {
|
||||
if (err.code === errors.codes.USAGE_LIMIT_EXCEEDED) {
|
||||
|
|
|
@ -6,7 +6,6 @@ const { events } = require("@budibase/backend-core")
|
|||
const TENANT_ID = structures.TENANT_ID
|
||||
|
||||
describe("/api/global/auth", () => {
|
||||
let code
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.beforeAll()
|
||||
|
@ -20,16 +19,7 @@ describe("/api/global/auth", () => {
|
|||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should logout", async () => {
|
||||
await request
|
||||
.post("/api/global/auth/logout")
|
||||
.set(config.defaultHeaders())
|
||||
.expect(200)
|
||||
expect(events.auth.logout.mock.calls.length).toBe(1)
|
||||
})
|
||||
|
||||
it("should be able to generate password reset email", async () => {
|
||||
// initially configure settings
|
||||
const requestPasswordReset = async () => {
|
||||
await config.saveSmtpConfig()
|
||||
await config.saveSettingsConfig()
|
||||
await config.createUser()
|
||||
|
@ -40,16 +30,36 @@ describe("/api/global/auth", () => {
|
|||
})
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
const emailCall = sendMailMock.mock.calls[0][0]
|
||||
const parts = emailCall.html.split(`http://localhost:10000/builder/auth/reset?code=`)
|
||||
const code = parts[1].split("\"")[0].split("&")[0]
|
||||
return { code, res }
|
||||
}
|
||||
|
||||
it("should logout", async () => {
|
||||
await request
|
||||
.post("/api/global/auth/logout")
|
||||
.set(config.defaultHeaders())
|
||||
.expect(200)
|
||||
expect(events.auth.logout.mock.calls.length).toBe(1)
|
||||
})
|
||||
|
||||
it("should be able to generate password reset email", async () => {
|
||||
const { res, code } = await requestPasswordReset()
|
||||
const user = await config.getUser("test@test.com")
|
||||
|
||||
expect(res.body).toEqual({ message: "Please check your email for a reset link." })
|
||||
expect(sendMailMock).toHaveBeenCalled()
|
||||
const emailCall = sendMailMock.mock.calls[0][0]
|
||||
// after this URL there should be a code
|
||||
const parts = emailCall.html.split(`http://localhost:10000/builder/auth/reset?code=`)
|
||||
code = parts[1].split("\"")[0].split("&")[0]
|
||||
|
||||
expect(code).toBeDefined()
|
||||
expect(events.user.passwordResetRequested).toBeCalledTimes(1)
|
||||
expect(events.user.passwordResetRequested).toBeCalledWith(user)
|
||||
})
|
||||
|
||||
it("should allow resetting user password with code", async () => {
|
||||
const { code } = await requestPasswordReset()
|
||||
const user = await config.getUser("test@test.com")
|
||||
|
||||
const res = await request
|
||||
.post(`/api/global/auth/${TENANT_ID}/reset/update`)
|
||||
.send({
|
||||
|
@ -59,6 +69,8 @@ describe("/api/global/auth", () => {
|
|||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(res.body).toEqual({ message: "password reset successfully." })
|
||||
expect(events.user.passwordReset).toBeCalledTimes(1)
|
||||
expect(events.user.passwordReset).toBeCalledWith(user)
|
||||
})
|
||||
|
||||
describe("oidc", () => {
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
jest.mock("nodemailer")
|
||||
const { config, request } = require("../../../tests")
|
||||
const { events, utils } = require("@budibase/backend-core")
|
||||
|
||||
describe("/api/global/self", () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.beforeAll()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await config.afterAll()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
const updateSelf = async (user) => {
|
||||
const res = await request
|
||||
.post(`/api/global/self`)
|
||||
.send(user)
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
return res
|
||||
}
|
||||
|
||||
describe("update", () => {
|
||||
|
||||
it("should update self", async () => {
|
||||
const user = await config.createUser()
|
||||
|
||||
const res = await updateSelf(user)
|
||||
|
||||
delete user.password
|
||||
expect(res.body._id).toBe(user._id)
|
||||
expect(events.user.updated).toBeCalledTimes(1)
|
||||
expect(events.user.updated).toBeCalledWith(user)
|
||||
expect(events.user.passwordUpdated).not.toBeCalled()
|
||||
})
|
||||
|
||||
it("should update password", async () => {
|
||||
const user = await config.createUser()
|
||||
const password = "newPassword"
|
||||
user.password = password
|
||||
|
||||
const res = await updateSelf(user)
|
||||
|
||||
delete user.password
|
||||
expect(res.body._id).toBe(user._id)
|
||||
expect(events.user.updated).toBeCalledTimes(1)
|
||||
expect(events.user.updated).toBeCalledWith(user)
|
||||
expect(events.user.passwordUpdated).toBeCalledTimes(1)
|
||||
expect(events.user.passwordUpdated).toBeCalledWith(user)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -4,7 +4,6 @@ const sendMailMock = mocks.email.mock()
|
|||
const { events } = require("@budibase/backend-core")
|
||||
|
||||
describe("/api/global/users", () => {
|
||||
let code
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.beforeAll()
|
||||
|
@ -14,8 +13,7 @@ describe("/api/global/users", () => {
|
|||
await config.afterAll()
|
||||
})
|
||||
|
||||
it("should be able to generate an invitation", async () => {
|
||||
// initially configure settings
|
||||
const sendUserInvite = async () => {
|
||||
await config.saveSmtpConfig()
|
||||
await config.saveSettingsConfig()
|
||||
const res = await request
|
||||
|
@ -26,16 +24,27 @@ describe("/api/global/users", () => {
|
|||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(res.body).toEqual({ message: "Invitation has been sent." })
|
||||
expect(sendMailMock).toHaveBeenCalled()
|
||||
|
||||
const emailCall = sendMailMock.mock.calls[0][0]
|
||||
// after this URL there should be a code
|
||||
const parts = emailCall.html.split("http://localhost:10000/builder/invite?code=")
|
||||
code = parts[1].split("\"")[0].split("&")[0]
|
||||
const code = parts[1].split("\"")[0].split("&")[0]
|
||||
return { code, res }
|
||||
}
|
||||
|
||||
it("should be able to generate an invitation", async () => {
|
||||
const { code, res } = await sendUserInvite()
|
||||
|
||||
expect(res.body).toEqual({ message: "Invitation has been sent." })
|
||||
expect(sendMailMock).toHaveBeenCalled()
|
||||
expect(code).toBeDefined()
|
||||
expect(events.user.invited).toBeCalledTimes(1)
|
||||
expect(events.user.invited).toBeCalledWith({ tenantId: structures.TENANT_ID })
|
||||
})
|
||||
|
||||
it("should be able to create new user from invite", async () => {
|
||||
const { code } = await sendUserInvite()
|
||||
|
||||
const res = await request
|
||||
.post(`/api/global/users/invite/accept`)
|
||||
.send({
|
||||
|
@ -48,6 +57,8 @@ describe("/api/global/users", () => {
|
|||
const user = await config.getUser("invite@test.com")
|
||||
expect(user).toBeDefined()
|
||||
expect(user._id).toEqual(res.body._id)
|
||||
expect(events.user.inviteAccepted).toBeCalledTimes(1)
|
||||
expect(events.user.inviteAccepted).toBeCalledWith(res.body)
|
||||
})
|
||||
|
||||
const createUser = async (user) => {
|
||||
|
@ -70,7 +81,7 @@ describe("/api/global/users", () => {
|
|||
.send(user)
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
// .expect(200)
|
||||
.expect(200)
|
||||
return res.body
|
||||
}
|
||||
|
||||
|
@ -149,6 +160,23 @@ describe("/api/global/users", () => {
|
|||
expect(events.user.updated).toBeCalledTimes(1)
|
||||
expect(events.user.permissionBuilderAssigned).not.toBeCalled()
|
||||
expect(events.user.permissionAdminAssigned).not.toBeCalled()
|
||||
expect(events.user.passwordForceReset).not.toBeCalled()
|
||||
})
|
||||
|
||||
it("should be able to force reset password", async () => {
|
||||
let user = structures.users.user({ email: "basic-password-update@test.com" })
|
||||
await createUser(user)
|
||||
jest.clearAllMocks()
|
||||
|
||||
user.forceResetPassword = true
|
||||
user.password = "tempPassword"
|
||||
await updateUser(user)
|
||||
|
||||
expect(events.user.created).not.toBeCalled()
|
||||
expect(events.user.updated).toBeCalledTimes(1)
|
||||
expect(events.user.permissionBuilderAssigned).not.toBeCalled()
|
||||
expect(events.user.permissionAdminAssigned).not.toBeCalled()
|
||||
expect(events.user.passwordForceReset).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should be able to update a basic user to an admin user", async () => {
|
||||
|
|
|
@ -52,6 +52,14 @@ export const handleSaveEvents = (user: any, existingUser: any) => {
|
|||
if (isRemovingAdmin(user, existingUser)) {
|
||||
events.user.permissionAdminRemoved(user)
|
||||
}
|
||||
|
||||
if (
|
||||
!existingUser.forceResetPassword &&
|
||||
user.forceResetPassword &&
|
||||
user.password
|
||||
) {
|
||||
events.user.passwordForceReset(user)
|
||||
}
|
||||
} else {
|
||||
events.user.created(user)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import env from "../../environment"
|
||||
import { quotas } from "@budibase/pro"
|
||||
import * as apps from "../../utilities/appService"
|
||||
const { events } = require("@budibase/backend-core")
|
||||
import * as eventHelpers from "./events"
|
||||
|
||||
const {
|
||||
|
@ -125,7 +124,7 @@ export const save = async (
|
|||
const putUserFn = () => {
|
||||
return db.put(user)
|
||||
}
|
||||
if (await eventHelpers.isAddingBuilder(user, dbUser)) {
|
||||
if (eventHelpers.isAddingBuilder(user, dbUser)) {
|
||||
response = await quotas.addDeveloper(putUserFn)
|
||||
} else {
|
||||
response = await putUserFn()
|
||||
|
|
Loading…
Reference in New Issue