diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index a0bd1ba..76e6d93 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -1,6 +1,12 @@ import { FastifyReply, FastifyRequest } from "fastify"; -import { createUser, getUser } from "./user.service"; -import { CreateUserInput } from "./user.schema"; +import { + createUser, + deleteUser, + getUser, + listUsers, + updateUser, +} from "./user.service"; +import { CreateUserInput, UpdateUserInput } from "./user.schema"; export async function createUserHandler( req: FastifyRequest, @@ -9,8 +15,7 @@ export async function createUserHandler( const body = req.body as CreateUserInput; try { - const authUser = req.user; - const user = await createUser(body, authUser.tenantId); + const user = await createUser(body, req.user); return res.code(201).send(user); } catch (err) { return err; @@ -49,3 +54,46 @@ export async function getUserHandler(req: FastifyRequest, res: FastifyReply) { return err; } } + +export async function listUserHandler(req: FastifyRequest, res: FastifyReply) { + try { + const users = await listUsers(req.user.tenantId); + return res.code(200).send({ users: users }); + } catch (err) { + return err; + } +} + +export async function updateUserHandler( + req: FastifyRequest, + res: FastifyReply +) { + const input = req.body as UpdateUserInput; + const { userId } = req.params as { userId: string }; + + try { + const updatedUser = await updateUser(userId, input); + if (!updateUser) return res.code(404).send({ error: "resource not found" }); + + return res.code(200).send(updatedUser); + } catch (err) { + return err; + } +} + +export async function deleteUserHandler( + req: FastifyRequest, + res: FastifyReply +) { + const { userId } = req.params as { userId: string }; + + try { + const deleteResult = await deleteUser(userId, req.user.tenantId); + if (deleteResult.deletedCount == 0) + return res.code(404).send({ error: "resource not found" }); + + return res.code(204).send(); + } catch (err) { + return err; + } +} diff --git a/src/user/user.route.ts b/src/user/user.route.ts index 1e186f1..6e71456 100644 --- a/src/user/user.route.ts +++ b/src/user/user.route.ts @@ -1,8 +1,11 @@ import { FastifyInstance } from "fastify"; import { createUserHandler, + deleteUserHandler, getCurrentUserHandler, getUserHandler, + listUserHandler, + updateUserHandler, } from "./user.controller"; import { $user } from "./user.schema"; @@ -13,7 +16,7 @@ export default async function userRoutes(fastify: FastifyInstance) { schema: { body: $user("createUserInput"), response: { - 201: $user("createUserResponse"), + 201: $user("userResponse"), }, }, config: { requiredClaims: ["user:write"] }, @@ -27,7 +30,7 @@ export default async function userRoutes(fastify: FastifyInstance) { { schema: { response: { - 200: $user("createUserResponse"), + 200: $user("userResponse"), }, }, }, @@ -39,7 +42,7 @@ export default async function userRoutes(fastify: FastifyInstance) { { schema: { response: { - 200: $user("createUserResponse"), + 200: $user("userResponse"), }, }, config: { requiredClaims: ["user:read"] }, @@ -47,4 +50,34 @@ export default async function userRoutes(fastify: FastifyInstance) { }, getUserHandler ); + + fastify.get( + "/", + { + config: { requiredClaims: ["user:read"] }, + preHandler: [fastify.authorize], + }, + listUserHandler + ); + + fastify.patch( + "/:userId", + { + schema: { + body: $user("updateUserInput"), + }, + config: { requiredClaims: ["user:write"] }, + preHandler: [fastify.authorize], + }, + updateUserHandler + ); + + fastify.delete( + "/:userId", + { + config: { requiredClaims: ["user:delete"] }, + preHandler: [fastify.authorize], + }, + deleteUserHandler + ); } diff --git a/src/user/user.schema.ts b/src/user/user.schema.ts index e9adba0..12ffccd 100644 --- a/src/user/user.schema.ts +++ b/src/user/user.schema.ts @@ -80,11 +80,6 @@ const createUserInput = z } }); -const createUserResponse = z.object({ - pid: z.string().cuid2(), - ...userCore, -}); - const updateUserInput = z.object({ firstName: z.string().max(30).optional(), lastName: z.string().max(30).optional(), @@ -96,6 +91,23 @@ const updateUserInput = z.object({ .email() .optional(), avatar: z.string().url().optional(), + role: z.enum(roles).optional(), +}); + +const userResponse = z.object({ + _id: z.string(), + pid: z.string(), + orgId: z.string().optional(), + firstName: z.string().optional(), + lastName: z.string().optional(), + name: z.string().optional(), + email: z.string().optional(), + role: z.string().optional(), + avatar: z.string().optional(), + status: z.string().optional(), + createdAt: z.string().optional(), + createdBy: z.string().optional(), + lastLogin: z.string().optional(), }); export type CreateUserInput = z.infer; @@ -104,7 +116,8 @@ export type UpdateUserInput = z.infer; export const { schemas: userSchemas, $ref: $user } = buildJsonSchemas( { createUserInput, - createUserResponse, + updateUserInput, + userResponse, }, { $id: "user" } ); diff --git a/src/user/user.service.ts b/src/user/user.service.ts index 1dcef58..3fbc84a 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -2,19 +2,25 @@ import mongoose from "mongoose"; import { generateId, generateToken } from "../utils/id"; import { CreateUserInput, UpdateUserInput, userModel } from "./user.schema"; import { sendMail } from "../utils/mail"; +import { AuthenticatedUser } from "../auth"; -export async function createUser(input: CreateUserInput, tenantId: string) { +export async function createUser( + input: CreateUserInput, + user: AuthenticatedUser +) { const token = await generateToken(); - const user = await userModel.create({ - tenantId: tenantId, + const newUser = await userModel.create({ + tenantId: user.tenantId, pid: generateId(), name: input.firstName + " " + input.lastName, createdAt: new Date(), + createdBy: user.userId, token: { value: token, expiry: new Date(Date.now() + 3600 * 6 * 1000), }, + status: "invited", ...input, }); @@ -22,11 +28,15 @@ export async function createUser(input: CreateUserInput, tenantId: string) { input.email, "You have been invited to Quicker Permtis.", `Click here to register.` ); - return user; + return newUser; } export async function getUser(userId: string) { @@ -47,6 +57,26 @@ export async function getUserByEmail(email: string) { return await userModel.findOne({ email: email }); } -export async function updateUser(userId: string, input: UpdateUserInput) { - return await userModel.findOneAndUpdate({ pid: userId }, input); +export async function listUsers(tenantId: string) { + return await userModel + .find({ tenantId: tenantId }) + .select( + "_id pid orgId firstName lastName name email role avatar status createdAt createdBy lastLogin" + ); +} + +export async function updateUser(userId: string, input: UpdateUserInput) { + return await userModel + .findOneAndUpdate({ pid: userId }, input, { + new: true, + }) + .select( + "_id pid orgId firstName lastName name email role avatar status createdAt createdBy lastLogin" + ); +} + +export async function deleteUser(userId: string, tenantId: string) { + return await userModel.deleteOne({ + $and: [{ pid: userId }, { tenantId: tenantId }], + }); } diff --git a/src/webauthn/webauthn.route.ts b/src/webauthn/webauthn.route.ts index 29ee281..a38c343 100644 --- a/src/webauthn/webauthn.route.ts +++ b/src/webauthn/webauthn.route.ts @@ -112,6 +112,7 @@ export async function webAuthnRoutes(fastify: FastifyInstance) { return res.code(400).send({ error: "registration failed" }); } + userInDB.status = "active"; userInDB.passKeys.push({ credentialID: verification.registrationInfo.credential.id, credentialPublicKey: @@ -238,6 +239,9 @@ export async function webAuthnRoutes(fastify: FastifyInstance) { req.headers["user-agent"] ); + userInDB.lastLogin = new Date(); + await userInDB.save(); + res.send({ session_token: newSession.sid }); } catch (error) { res.code(400).send({ error: (error as Error).message });