diff --git a/src/organization/organization.controller.ts b/src/organization/organization.controller.ts index ca109a5..5834203 100644 --- a/src/organization/organization.controller.ts +++ b/src/organization/organization.controller.ts @@ -1,6 +1,13 @@ import { FastifyRequest, FastifyReply } from "fastify"; -import { CreateOrgInput } from "./organization.schema"; -import { createOrg, getOrg } from "./organization.service"; +import { CreateOrgInput, UpdateOrgInput } from "./organization.schema"; +import { + createOrg, + deleteOrg, + getOrg, + listOrgs, + updateOrg, +} from "./organization.service"; +import { PageQueryParams } from "../pagination"; export async function createOrgHandler(req: FastifyRequest, res: FastifyReply) { const input = req.body as CreateOrgInput; @@ -28,3 +35,45 @@ export async function getOrgHandler(req: FastifyRequest, res: FastifyReply) { return err; } } + +export async function listOrgsHandler(req: FastifyRequest, res: FastifyReply) { + const queryParams = req.query as PageQueryParams; + + try { + const authUser = req.user; + const orgList = await listOrgs(queryParams, authUser.tenantId); + return res.code(200).send(orgList); + } catch (err) { + return err; + } +} + +export async function updateOrgHandler(req: FastifyRequest, res: FastifyReply) { + const input = req.body as UpdateOrgInput; + const { orgId } = req.params as { orgId: string }; + + try { + const authUser = req.user; + const updatedOrg = await updateOrg(input, orgId, authUser.tenantId); + if (!updatedOrg) return res.code(404).send({ error: "resource not found" }); + + return res.code(200).send(updatedOrg); + } catch (err) { + return err; + } +} + +export async function deleteOrgHandler(req: FastifyRequest, res: FastifyReply) { + const { orgId } = req.params as { orgId: string }; + + try { + const authUser = req.user; + const deleteResult = await deleteOrg(orgId, authUser.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/organization/organization.route.ts b/src/organization/organization.route.ts index c373e1d..647d329 100644 --- a/src/organization/organization.route.ts +++ b/src/organization/organization.route.ts @@ -1,6 +1,12 @@ import { FastifyInstance } from "fastify"; import { $org } from "./organization.schema"; -import { createOrgHandler, getOrgHandler } from "./organization.controller"; +import { + createOrgHandler, + deleteOrgHandler, + getOrgHandler, + listOrgsHandler, + updateOrgHandler, +} from "./organization.controller"; export default function organizationRoutes(fastify: FastifyInstance) { fastify.post( @@ -33,4 +39,43 @@ export default function organizationRoutes(fastify: FastifyInstance) { }, getOrgHandler ); + + fastify.get( + "/", + { + schema: { + querystring: $org("pageQueryParams"), + response: { 200: $org("listOrgResponse") }, + }, + config: { requiredClaims: ["org:read"] }, + preHandler: [fastify.authorize], + }, + listOrgsHandler + ); + + fastify.patch( + "/:orgId", + { + schema: { + params: { type: "object", properties: { orgId: { type: "string" } } }, + body: $org("updateOrgInput"), + response: { + 200: $org("createOrgResponse"), + }, + }, + }, + updateOrgHandler + ); + + fastify.delete( + "/:orgId", + { + schema: { + params: { type: "object", properties: { orgId: { type: "string" } } }, + }, + config: { requiredClaims: ["org:delete"] }, + preHandler: [fastify.authorize], + }, + deleteOrgHandler + ); } diff --git a/src/organization/organization.schema.ts b/src/organization/organization.schema.ts index 63f3d07..c42a5ff 100644 --- a/src/organization/organization.schema.ts +++ b/src/organization/organization.schema.ts @@ -1,6 +1,7 @@ import { buildJsonSchemas } from "fastify-zod"; import mongoose from "mongoose"; import { z } from "zod"; +import { pageMetadata, pageQueryParams } from "../pagination"; export const orgModel = mongoose.model( "organization", @@ -19,10 +20,12 @@ export const orgModel = mongoose.model( }, avatar: String, type: String, + isClient: Boolean, status: String, createdAt: Date, createdBy: mongoose.Types.ObjectId, - }) + updatedAt: Date, + }).index({ tenantId: 1, domain: 1 }, { unique: true }) ); const orgCore = { @@ -32,6 +35,7 @@ const orgCore = { type: z.enum(["county", "builder"], { message: "Must be county or builder", }), + isClient: z.boolean().optional(), }; const createOrgInput = z.object({ @@ -43,12 +47,33 @@ const createOrgResponse = z.object({ ...orgCore, }); +const listOrgResponse = z.object({ + orgs: z.array(createOrgResponse), + metadata: pageMetadata, +}); + +const updateOrgInput = z.object({ + name: z.string().max(30).optional(), + domain: z.string().max(30).optional(), + avatar: z.string().url().optional(), + type: z + .enum(["county", "builder"], { + message: "Must be county or builder", + }) + .optional(), + isClient: z.boolean().optional(), +}); + export type CreateOrgInput = z.infer; +export type UpdateOrgInput = z.infer; export const { schemas: orgSchemas, $ref: $org } = buildJsonSchemas( { createOrgInput, createOrgResponse, + listOrgResponse, + updateOrgInput, + pageQueryParams, }, { $id: "org" } ); diff --git a/src/organization/organization.service.ts b/src/organization/organization.service.ts index f29c09c..5f7ba25 100644 --- a/src/organization/organization.service.ts +++ b/src/organization/organization.service.ts @@ -1,5 +1,10 @@ +import { PageQueryParams } from "../pagination"; import { generateId } from "../utils/id"; -import { CreateOrgInput, orgModel } from "./organization.schema"; +import { + CreateOrgInput, + orgModel, + UpdateOrgInput, +} from "./organization.schema"; export async function createOrg(input: CreateOrgInput, tenantId: string) { const org = await orgModel.create({ @@ -17,3 +22,49 @@ export async function getOrg(orgId: string, tenantId: string) { $and: [{ tenantId: tenantId }, { pid: orgId }], }); } + +export async function listOrgs(params: PageQueryParams, tenantId: string) { + const page = params.page || 1; + const pageSize = params.pageSize || 10; + + const orgs = await orgModel.aggregate([ + { $match: { $and: [{ tenantId: tenantId }] } }, + { + $facet: { + metadata: [{ $count: "count" }], + data: [{ $skip: (page - 1) * pageSize }, { $limit: pageSize }], + }, + }, + ]); + + return { + orgs: orgs[0].data, + metadata: { + count: orgs[0].metadata[0].count, + page, + pageSize, + }, + }; +} + +export async function updateOrg( + input: UpdateOrgInput, + orgId: string, + tenantId: string +) { + const updateOrgResult = await orgModel.findOneAndUpdate( + { + $and: [{ tenantId: tenantId }, { pid: orgId }], + }, + { ...input, updatedAt: new Date() }, + { new: true } + ); + + return updateOrgResult; +} + +export async function deleteOrg(orgId: string, tenantId: string) { + return await orgModel.deleteOne({ + $and: [{ tenantId: tenantId }, { pid: orgId }], + }); +} diff --git a/src/pagination.ts b/src/pagination.ts new file mode 100644 index 0000000..e738aa6 --- /dev/null +++ b/src/pagination.ts @@ -0,0 +1,14 @@ +import { z } from "zod"; + +export const pageMetadata = z.object({ + count: z.number(), + page: z.number(), + pageSize: z.number(), +}); + +export const pageQueryParams = z.object({ + page: z.number().optional(), + pageSize: z.number().optional(), +}); + +export type PageQueryParams = z.infer; diff --git a/src/tokens/token.service.ts b/src/tokens/token.service.ts index 9e1e720..25e6b95 100644 --- a/src/tokens/token.service.ts +++ b/src/tokens/token.service.ts @@ -5,7 +5,7 @@ import { CreateTokenInput, tokenModel } from "./token.schema"; export async function createToken(input: CreateTokenInput, tenantId: string) { const tokenId = generateId(); const newToken = await generateToken(); - const tokenHash = await bcrypt.hash(newToken, 10); + const tokenHash = await bcrypt.hash(newToken, 5); const tokenInDb = await tokenModel.create({ tenantId: tenantId,