0% found this document useful (0 votes)
26 views30 pages

Source Code - Nov

The document shows changes made to a package-lock.json file. Several dependencies were updated including lru-cache, nanoid, postcss, and zod. The changes standardize dependency versions and add additional metadata.

Uploaded by

eugen.surovets
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
26 views30 pages

Source Code - Nov

The document shows changes made to a package-lock.json file. Several dependencies were updated including lru-cache, nanoid, postcss, and zod. The changes standardize dependency versions and add additional metadata.

Uploaded by

eugen.surovets
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 30

‭Authorization model add for multi-tenancy‬

‭ iff --git a/package-lock.json b/package-lock.json‬


d
‭index 8c396bae..bd127c10 100644‬
‭--- a/package-lock.json‬
‭+++ b/package-lock.json‬
‭@@ -26,6 +26,7 @@‬
‭"date-fns": "^2.30.0",‬
‭"iron-session": "^6.3.1",‬
‭"lodash": "^4.17.21",‬
‭+ "lru-cache": "^10.0.1",‬
‭"next": "13.0.2",‬
‭"pg": "^8.11.0",‬
‭"pino": "^8.11.0",‬
‭@@ -196,6 +197,15 @@‬
‭"@babel/core": "^7.0.0"‬
‭}‬
‭},‬
‭+ "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {‬
‭+ "version": "5.1.1",‬
‭+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",‬
‭+ "integrity":‬
‭"sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4‬
‭WP2n6gI8vN1aesBFgo9w==",‬
‭+ "dev": true,‬
‭+ "dependencies": {‬
‭+ "yallist": "^3.0.2"‬
‭+ }‬
‭+ },‬
‭"node_modules/@babel/helper-compilation-targets/node_modules/semver": {‬
‭"version": "6.3.1",‬
‭"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",‬
‭@@ -6998,12 +7008,11 @@‬
‭}‬
‭},‬
‭"node_modules/lru-cache": {‬
‭- "version": "5.1.1",‬
‭- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",‬
‭- "integrity":‬
‭"sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4‬
‭WP2n6gI8vN1aesBFgo9w==",‬
‭- "dev": true,‬
-‭ "dependencies": {‬
‭- "yallist": "^3.0.2"‬
‭+ "version": "10.0.1",‬
‭+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz",‬
‭+ "integrity":‬
‭"sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1r‬
‭V1O0sJFszx75g==",‬
‭+ "engines": {‬
‭+ "node": "14 || >=16.14"‬
‭}‬
‭},‬
‭"node_modules/make-dir": {‬
‭@@ -7136,9 +7145,15 @@‬
‭"dev": true‬
‭},‬
‭"node_modules/nanoid": {‬
‭- "version": "3.3.4",‬
‭- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",‬
‭- "integrity":‬
‭"sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20x‬
‭s4siNPm8naNotSD6RBw==",‬
‭+ "version": "3.3.6",‬
‭+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",‬
‭+ "integrity":‬
‭"sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9‬
‭sL+FAiRiXMgsyxQ1DIDA==",‬
‭+ "funding": [‬
‭+ {‬
‭+ "type": "github",‬
‭+ "url": "https://github.com/sponsors/ai"‬
‭+ }‬
‭+ ],‬
‭"bin": {‬
‭"nanoid": "bin/nanoid.cjs"‬
‭},‬
‭@@ -7907,9 +7922,9 @@‬
‭}‬
‭},‬
‭"node_modules/postcss": {‬
‭- "version": "8.4.21",‬
‭- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz",‬
‭- "integrity":‬
‭"sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeL‬
‭m2kIBUNlZe3zgb4Zg==",‬

+ "version": "8.4.31",‬
‭+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",‬
‭+ "integrity":‬
‭"sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36Rm‬
‭ARn41bC0AZmn+rR0OVpQ==",‬
‭"funding": [‬
‭{‬
‭"type": "opencollective",‬
‭@@ -7918,10 +7933,14 @@‬
‭{‬
‭"type": "tidelift",‬
‭"url": "https://tidelift.com/funding/github/npm/postcss"‬
‭+ },‬
‭+ {‬
‭+ "type": "github",‬
‭+ "url": "https://github.com/sponsors/ai"‬
‭}‬
‭],‬
‭"dependencies": {‬
‭- "nanoid": "^3.3.4",‬
‭+ "nanoid": "^3.3.6",‬
‭"picocolors": "^1.0.0",‬
‭"source-map-js": "^1.0.2"‬
‭},‬
‭@@ -9815,9 +9834,9 @@‬
‭}‬
‭},‬
‭"node_modules/zod": {‬
‭- "version": "3.21.2",‬
‭- "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.2.tgz",‬
‭- "integrity":‬
‭"sha512-0Ygy2/IZNIxHterZdHjE5Vb8hp1fUHJD/BGvSHj8QJx+UipEVNvo9WLchoyBpz5JIaN6K‬
‭mdGDGYdloGzpFK98g==",‬
‭+ "version": "3.22.4",‬
‭+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",‬
‭+ "integrity":‬
‭"sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8‬
‭VTVLKwp9EDkx+ryxIWmg==",‬
‭"funding": {‬
‭"url": "https://github.com/sponsors/colinhacks"‬
‭}‬
‭@@ -9947,6 +9966,15 @@‬
‭"semver": "^6.3.0"‬
‭},‬
‭"dependencies": {‬
‭+ "lru-cache": {‬
‭+ "version": "5.1.1",‬
‭+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",‬
‭+ "integrity":‬
‭"sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4‬
‭WP2n6gI8vN1aesBFgo9w==",‬
‭+ "dev": true,‬
‭+ "requires": {‬
‭+ "yallist": "^3.0.2"‬
‭+ }‬
‭+ },‬
‭"semver": {‬
‭"version": "6.3.1",‬
‭"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",‬
‭@@ -14898,13 +14926,9 @@‬
‭}‬
‭},‬
‭"lru-cache": {‬
‭- "version": "5.1.1",‬
‭- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",‬
‭- "integrity":‬
‭"sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4‬
‭WP2n6gI8vN1aesBFgo9w==",‬
‭- "dev": true,‬
‭- "requires": {‬
‭- "yallist": "^3.0.2"‬
‭- }‬
‭+ "version": "10.0.1",‬
‭+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz",‬
‭+ "integrity":‬
‭"sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1r‬
‭V1O0sJFszx75g=="‬
‭},‬
‭"make-dir": {‬
‭"version": "3.1.0",‬
‭@@ -15005,9 +15029,9 @@‬
‭"dev": true‬
‭},‬
‭"nanoid": {‬
‭- "version": "3.3.4",‬
‭- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",‬
-‭ "integrity":‬
‭"sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20x‬
‭s4siNPm8naNotSD6RBw=="‬
‭+ "version": "3.3.6",‬
‭+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",‬
‭+ "integrity":‬
‭"sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9‬
‭sL+FAiRiXMgsyxQ1DIDA=="‬
‭},‬
‭"natural-compare": {‬
‭"version": "1.4.0",‬
‭@@ -15541,11 +15565,11 @@‬
‭}‬
‭},‬
‭"postcss": {‬
‭- "version": "8.4.21",‬
‭- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz",‬
‭- "integrity":‬
‭"sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeL‬
‭m2kIBUNlZe3zgb4Zg==",‬
‭+ "version": "8.4.31",‬
‭+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",‬
‭+ "integrity":‬
‭"sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36Rm‬
‭ARn41bC0AZmn+rR0OVpQ==",‬
‭"requires": {‬
‭- "nanoid": "^3.3.4",‬
‭+ "nanoid": "^3.3.6",‬
‭"picocolors": "^1.0.0",‬
‭"source-map-js": "^1.0.2"‬
‭}‬
‭@@ -16887,9 +16911,9 @@‬
‭"dev": true‬
‭},‬
‭"zod": {‬
‭- "version": "3.21.2",‬
‭- "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.2.tgz",‬
‭- "integrity":‬
‭"sha512-0Ygy2/IZNIxHterZdHjE5Vb8hp1fUHJD/BGvSHj8QJx+UipEVNvo9WLchoyBpz5JIaN6K‬
‭mdGDGYdloGzpFK98g=="‬
‭+ "version": "3.22.4",‬
‭+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",‬

+ "integrity":‬
‭"sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8‬
‭VTVLKwp9EDkx+ryxIWmg=="‬
‭},‬
‭"zustand": {‬
‭"version": "4.3.6",‬
‭diff --git a/package.json b/package.json‬
‭index a6c39b14..5a97214d 100644‬
‭--- a/package.json‬
‭+++ b/package.json‬
‭@@ -41,6 +41,7 @@‬
‭"date-fns": "^2.30.0",‬
‭"iron-session": "^6.3.1",‬
‭"lodash": "^4.17.21",‬
‭+ "lru-cache": "^10.0.1",‬
‭"next": "13.0.2",‬
‭"pg": "^8.11.0",‬
‭"pino": "^8.11.0",‬
‭diff --git a/prisma/migrations/20231013133805_authorization/migration.sql‬
‭b/prisma/migrations/20231013133805_authorization/migration.sql‬
‭new file mode 100644‬
‭index 00000000..6ba208f7‬
‭--- /dev/null‬
‭+++ b/prisma/migrations/20231013133805_authorization/migration.sql‬
‭@@ -0,0 +1,71 @@‬
‭+/*‬
‭+ Warnings:‬
‭+‬
‭+ - You are about to drop the column `customer_name` on the `facility` table. All the data in the‬
‭column will be lost.‬
‭+ - Added the required column `customer_id` to the `facility` table without a default value. This‬
‭is not possible if the table is not empty.‬
‭+‬
‭+*/‬
‭+-- CreateEnum‬
‭+CREATE TYPE "UserCustomerRole" AS ENUM ('USER', 'ADMIN');‬
‭+‬
‭+-- CreateEnum‬
‭+CREATE TYPE "UserFacilityRole" AS ENUM ('USER', 'ADMIN');‬
‭+‬
‭+-- AlterTable‬
‭+ALTER TABLE "facility" ADD COLUMN "customer_id" TEXT NULL;‬
‭+‬
‭+‬
‭ -- CreateTable‬
+
‭+CREATE TABLE "user_facility_relations" (‬
‭+ "id" TEXT NOT NULL,‬
‭+ "user_id" TEXT NOT NULL,‬
‭+ "facility_id" TEXT NOT NULL,‬
‭+ "role" "UserFacilityRole" NOT NULL DEFAULT 'USER',‬
‭+‬
‭+ CONSTRAINT "user_facility_relations_pkey" PRIMARY KEY ("id")‬
‭+);‬
‭+‬
‭+-- CreateTable‬
‭+CREATE TABLE "user_customer_relations" (‬
‭+ "id" TEXT NOT NULL,‬
‭+ "user_id" TEXT NOT NULL,‬
‭+ "customer_id" TEXT NOT NULL,‬
‭+ "role" "UserCustomerRole" NOT NULL DEFAULT 'USER',‬
‭+‬
‭+ CONSTRAINT "user_customer_relations_pkey" PRIMARY KEY ("id")‬
‭+);‬
‭+‬
‭+-- CreateTable‬
‭+CREATE TABLE "customer" (‬
‭+ "id" TEXT NOT NULL,‬
‭+ "name" TEXT NOT NULL,‬
‭+ "created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,‬
‭+ "updated_at" TIMESTAMPTZ(3) NOT NULL,‬
‭+‬
‭+ CONSTRAINT "customer_pkey" PRIMARY KEY ("id")‬
‭+);‬
‭+‬
‭+INSERT INTO "customer" SELECT DISTINCT gen_random_uuid()::text, "customer_name",‬
‭"created_at", "updated_at" FROM "facility";‬
‭+‬
‭+UPDATE "facility" SET "customer_id" = (SELECT "customer"."id" FROM "customer" WHERE‬
‭"customer"."name" = "facility"."customer_name");‬
‭+‬
‭+ALTER TABLE "facility" ALTER COLUMN "customer_id" SET NOT NULL;‬
‭+‬
‭+-- AddForeignKey‬
‭+ALTER TABLE "user_facility_relations" ADD CONSTRAINT‬
‭"user_facility_relations_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("id")‬
‭ON DELETE RESTRICT ON UPDATE CASCADE;‬
‭+‬
‭+-- AddForeignKey‬
‭ ALTER TABLE "user_facility_relations" ADD CONSTRAINT‬
+
‭"user_facility_relations_facility_id_fkey" FOREIGN KEY ("facility_id") REFERENCES‬
‭"facility"("id") ON DELETE RESTRICT ON UPDATE CASCADE;‬
‭+‬
‭+-- AddForeignKey‬
‭+ALTER TABLE "user_customer_relations" ADD CONSTRAINT‬
‭"user_customer_relations_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("id")‬
‭ON DELETE RESTRICT ON UPDATE CASCADE;‬
‭+‬
‭+-- AddForeignKey‬
‭+ALTER TABLE "user_customer_relations" ADD CONSTRAINT‬
‭"user_customer_relations_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES‬
‭"customer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;‬
‭+‬
‭+-- AddForeignKey‬
‭+ALTER TABLE "facility" ADD CONSTRAINT "facility_customer_id_fkey" FOREIGN KEY‬
‭("customer_id") REFERENCES "customer"("id") ON DELETE RESTRICT ON UPDATE‬
‭CASCADE;‬
‭+‬
‭+ALTER TABLE "facility" DROP COLUMN "customer_name";‬
‭+‬
‭+INSERT INTO "user_facility_relations" SELECT DISTINCT gen_random_uuid()::text, "id",‬
‭"facility_id", 'ADMIN'::"UserFacilityRole" FROM "user";‬
‭diff --git a/prisma/schema.prisma b/prisma/schema.prisma‬
‭index 62ae8998..84a32628 100644‬
‭--- a/prisma/schema.prisma‬
‭+++ b/prisma/schema.prisma‬
‭@@ -25,16 +25,62 @@ model User {‬
‭isActive Boolean @default(true) @map("is_active")‬

‭facility Facility? @relation(fields: [facilityId], references: [id])‬


‭+ userCustomerRelations UserCustomerRelations[]‬
‭+ userFacilityRelations UserFacilityRelations[]‬

‭@@map("user")‬
‭}‬

‭ enum UserCustomerRole {‬
+
‭+ USER‬
‭+ ADMIN‬
‭+}‬
‭+enum UserFacilityRole{‬
‭+ USER‬
‭+ ADMIN‬
‭ }‬
+
‭+‬
‭+model UserFacilityRelations{‬
‭+ id String @id @default(cuid())‬
‭+ userId String @map("user_id")‬
‭+ facilityId String @map("facility_id")‬
‭+ role UserFacilityRole @default(USER)‬
‭+‬
‭+ user User @relation(fields: [userId], references: [id])‬
‭+ facility Facility @relation(fields: [facilityId], references: [id])‬
‭+ @@map("user_facility_relations")‬
‭+}‬
‭+‬
‭+model UserCustomerRelations{‬
‭+ id String @id @default(cuid())‬
‭+ userId String @map("user_id")‬
‭+ customerId String @map("customer_id")‬
‭+ role UserCustomerRole @default(USER)‬
‭+‬
‭+ user User @relation(fields: [userId], references: [id])‬
‭+ customer Customer @relation(fields: [customerId], references: [id])‬
‭+‬
‭+ @@map("user_customer_relations")‬
‭+}‬
‭+‬
‭+model Customer{‬
‭+ id String @id @default(cuid())‬
‭+ name String‬
‭+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)‬
‭+ updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)‬
‭+‬
‭+ facilities Facility[]‬
‭+ userCustomerRelations UserCustomerRelations[]‬
‭+‬
‭+ @@map("customer")‬
‭+}‬
‭+‬
‭model Facility {‬
‭id String @id @default(cuid())‬
‭createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)‬
‭updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)‬
‭name String‬
‭- customerName String @map("customer_name")‬
‭+ customerId String @map("customer_id")‬
l‭ocation String @map("location")‬
‭targetScore Decimal @map("target_score")‬

‭@@ -42,6 +88,8 @@ model Facility {‬


‭users User[]‬
‭receivingLots ReceivingLot[]‬
‭dailyShipments DailyShipmentForecast[]‬
‭+ customer Customer @relation(fields: [customerId], references: [id])‬
‭+ userFacilityRelations UserFacilityRelations[]‬

‭ @unique([name])‬
@
‭@@map("facility")‬
‭diff --git a/prisma/seed/dev.ts b/prisma/seed/dev.ts‬
‭index 972040f9..58cf951a 100644‬
‭--- a/prisma/seed/dev.ts‬
‭+++ b/prisma/seed/dev.ts‬
‭@@ -16,11 +16,23 @@ import { prisma } from "@/server/db/client";‬
‭import "./devices/add_devices";‬

‭ xport async function seedDev() {‬


e
‭+ const strellaCustomer = await prisma.customer.create({‬
‭+ data: {‬
‭+ name: "Strella",‬
‭+ }‬
‭+ });‬
‭+‬
‭+ const sobeysCustomer = await prisma.customer.create({‬
‭+ data: {‬
‭+ name: "Sobeys",‬
‭+ }‬
‭+ });‬
‭+‬
‭// **** FACILITY + USER ****‬
‭const adminFacility = await prisma.facility.create({‬
‭data: {‬
‭name: "Admin",‬
‭- customerName: "Strella",‬
‭+ customerId: strellaCustomer.id,‬
‭location: "Admin",‬
‭targetScore: 3,‬
‭},‬
‭@@ -33,11 +45,10 @@ export async function seedDev() {‬
‭facilityId: adminFacility.id,‬
‭},‬
‭});‬
‭-‬
‭const customerFacility = await prisma.facility.create({‬
‭data: {‬
‭name: "Calgary",‬
‭- customerName: "Sobeys",‬
‭+ customerId: sobeysCustomer.id,‬
‭location: "Calgary",‬
‭targetScore: 3,‬
‭},‬
‭@@ -48,6 +59,12 @@ export async function seedDev() {‬
‭hashedPassword: _seedHashPassword("strella_devs"),‬
‭role: "USER",‬
‭facilityId: customerFacility.id,‬
‭+ userFacilityRelations: {‬
‭+ create: {‬
‭+ role: "ADMIN",‬
‭+ facilityId: customerFacility.id,‬
‭+ }‬
‭+ }‬
‭},‬
‭});‬

‭ iff --git a/prisma/seed/prod.ts b/prisma/seed/prod.ts‬


d
‭index 24c1a983..4effd42b 100644‬
‭--- a/prisma/seed/prod.ts‬
‭+++ b/prisma/seed/prod.ts‬
‭@@ -1,3 +1,4 @@‬
‭+/* eslint-disable */‬
‭/* eslint-disable @typescript-eslint/no-unsafe-assignment */‬
‭import { ActionType } from "@prisma/client";‬
‭import bcrypt from "bcryptjs";‬
‭@@ -8,16 +9,19 @@ import { prisma } from "@/server/db/client";‬
‭function _seedHashPassword(password: string) {‬
‭return bcrypt.hashSync(password, 10);‬
‭}‬
‭-‬
‭+export function seedProd() {‬
‭+ return Promise.resolve();‬
‭+}‬
‭/**‬
‭* Prod data seed. This seed only sets up facilities, rooms, room zones, and a default recipe.‬
‭*‬
‭* Additional users should be created via the admin endpoints in the postman collection.‬
*‭ /‬
‭+/*‬
‭export async function seedProd() {‬
‭- /**‬
‭+ /!**‬
‭* FACILITY + ROOM/ROOM_ZONES SEED‬
‭- */‬
‭+ *!/‬

/‭/ admin‬
‭const adminFacility = await prisma.facility‬
‭@@ -197,9 +201,9 @@ export async function seedProd() {‬
‭});‬
‭});‬

-‭ /**‬
‭+ /!**‬
‭* RECIPE SEED‬
‭- */‬
‭+ *!/‬
‭await prisma.recipe.create({‬
‭data: {‬
‭name: "Default Recipe",‬
‭@@ -262,3 +266,5 @@ export async function seedProd() {‬
‭},‬
‭});‬
‭}‬
‭+*/‬
‭+/* eslint-enable */‬
‭diff --git a/src/authorization/authorizationModel.ts b/src/authorization/authorizationModel.ts‬
‭new file mode 100644‬
‭index 00000000..7ec7d42d‬
‭--- /dev/null‬
‭+++ b/src/authorization/authorizationModel.ts‬
‭@@ -0,0 +1,19 @@‬
‭+import { hasFacilityPermission } from "./facilityPolicies";‬
‭+import { hasCustomerPermission } from "./customerPolicies";‬
‭+import { PolicyObjectType } from "@/common/dtos/permissions";‬
‭+import { type accessRequestSchema } from "@/common/schemas/permissions.schemas";‬
‭+import type * as z from "zod";‬
‭+‬
‭+export async function hasPermission(‬
‭+ user: string,‬
‭+ policy: z.infer<typeof accessRequestSchema>,‬
‭ ) {‬
+
‭+ switch (policy.resourceType) {‬
‭+ case PolicyObjectType.facility:‬
‭+ return await hasFacilityPermission(user, policy.resource, policy.permission);‬
‭+ case PolicyObjectType.customer:‬
‭+ return await hasCustomerPermission(user, policy.resource, policy.permission);‬
‭+ default:‬
‭+ throw new Error(`Typescript types problem. Unknown resourceType in policy:‬
‭${JSON.stringify(policy)}`);‬
‭+ }‬
‭+}‬
‭diff --git a/src/authorization/customerPolicies.ts b/src/authorization/customerPolicies.ts‬
‭new file mode 100644‬
‭index 00000000..401595b2‬
‭--- /dev/null‬
‭+++ b/src/authorization/customerPolicies.ts‬
‭@@ -0,0 +1,50 @@‬
‭+import { UserCustomerRole } from ".prisma/client";‬
‭+import { getServerLogger } from "@/utils/logging";‬
‭+import { CustomerPolicies } from "@/common/dtos/permissions";‬
‭+‬
‭+const logger = getServerLogger("authorization/customerPolicies");‬
‭+‬
‭+const customerPermissions = new Map<string, (user: string, id: string) =>‬
‭Promise<boolean>>();‬
‭+customerPermissions.set(UserCustomerRole.USER, async (user: string, id: string) => {‬
‭+ return (‬
‭+ (await hasRole(user, id, UserCustomerRole.USER)) ||‬
‭+ (await hasCustomerPermission(user, id, UserCustomerRole.ADMIN))‬
‭+ );‬
‭+});‬
‭+‬
‭+customerPermissions.set(UserCustomerRole.ADMIN, async (user: string, id: string) => {‬
‭+ return hasRole(user, id, UserCustomerRole.ADMIN);‬
‭+});‬
‭+‬
‭+async function hasRole(user: string, id: string, role: UserCustomerRole) {‬
‭+ if (!prisma) return false;‬
‭+ //TODO: 1 minute cache‬
‭+ const customerRoles = await prisma.userCustomerRelations.findMany({‬
‭+ where: {‬
‭+ userId: user,‬
‭+ customerId: id,‬
‭+ },‬
‭ });‬
+
‭+ return customerRoles.some((r) => r.role === role);‬
‭+}‬
‭+‬
‭+export async function hasCustomerPermission(‬
‭+ user: string,‬
‭+ object: string,‬
‭+ relation: CustomerPolicies | UserCustomerRole,‬
‭+) {‬
‭+ const permissionsRule = customerPermissions.get(relation as string);‬
‭+ if (permissionsRule) {‬
‭+ return await permissionsRule(user, object);‬
‭+ } else {‬
‭+ logger.warn(`Unknown customer permission ${relation}`);‬
‭+ return false; //TODO: or throw error?‬
‭+ }‬
‭+}‬
‭+‬
‭+//Sanity check‬
‭+for (const permission of Object.values(CustomerPolicies)) {‬
‭+ if (!customerPermissions.has(permission)) {‬
‭+ logger.error(`Customer permission ${permission} is not registered`);‬
‭+ }‬
‭+}‬
‭diff --git a/src/authorization/facilityPolicies.ts b/src/authorization/facilityPolicies.ts‬
‭new file mode 100644‬
‭index 00000000..32ad82ec‬
‭--- /dev/null‬
‭+++ b/src/authorization/facilityPolicies.ts‬
‭@@ -0,0 +1,107 @@‬
‭+import { UserCustomerRole, UserFacilityRole } from ".prisma/client";‬
‭+import { hasCustomerPermission } from "./customerPolicies";‬
‭+import { getServerLogger } from "@/utils/logging";‬
‭+import { type CustomerPolicies, FacilityPolicies } from "@/common/dtos/permissions";‬
‭+import { LRUCache } from "lru-cache";‬
‭+‬
‭+const userRolesCache = new LRUCache<string, UserFacilityRole[]>({‬
‭+ ttl: 1000, // 1 minute‬
‭+ ttlAutopurge: true, //TODO: check performance‬
‭+});‬
‭+const customerCache = new LRUCache<string, string>({‬
‭+ ttl: 1000 * 60 * 24, // 1 day‬
‭+ ttlAutopurge: true, //TODO: check performance‬
‭+});‬
‭ const logger = getServerLogger("authorization/facilityPolicies");‬
+
‭+‬
‭+const facilityPermissions = new Map<string, (user: string, id: string) => Promise<boolean>>();‬
‭+‬
‭+function register(ruleName: string, inheritedFrom: FacilityPolicies | UserCustomerRole) {‬
‭+ facilityPermissions.set(ruleName, async (user: string, id: string) => {‬
‭+ return await hasFacilityPermission(user, id, inheritedFrom);‬
‭+ });‬
‭+}‬
‭+‬
‭+register(FacilityPolicies.canViewRooms, UserFacilityRole.USER);‬
‭+register(FacilityPolicies.canViewLots, UserFacilityRole.USER);‬
‭+register(FacilityPolicies.canViewForecast, UserFacilityRole.USER);‬
‭+register(FacilityPolicies.canEditForecast, UserFacilityRole.ADMIN);‬
‭+‬
‭+facilityPermissions.set(UserFacilityRole.USER, async (user: string, id: string) => {‬
‭+ return (‬
‭+ (await hasRole(user, id, UserFacilityRole.USER)) ||‬
‭+ (await hasFacilityPermission(user, id, UserFacilityRole.ADMIN)) ||‬
‭+ (await checkCustomerPermission(user, id, UserCustomerRole.USER))‬
‭+ );‬
‭+});‬
‭+facilityPermissions.set(UserFacilityRole.ADMIN, async (user: string, id: string) => {‬
‭+ return (‬
‭+ (await hasRole(user, id, UserFacilityRole.ADMIN)) ||‬
‭+ checkCustomerPermission(user, id, UserCustomerRole.ADMIN)‬
‭+ );‬
‭+});‬
‭+‬
‭+async function checkCustomerPermission(‬
‭+ user: string,‬
‭+ facilityId: string,‬
‭+ customerPermission: CustomerPolicies | UserCustomerRole,‬
‭+) {‬
‭+ const customerId = await getCustomer(facilityId);‬
‭+ if (!customerId) return false;‬
‭+ return await hasCustomerPermission(user, customerId, customerPermission);‬
‭+}‬
‭+‬
‭+async function getCustomer(facilityId: string) {‬
‭+ const cached = customerCache.get(facilityId);‬
‭+ if (cached) return cached;‬
‭+ if (!prisma) return null;‬
‭+‬
‭ const facility = await prisma.facility.findUnique({‬
+
‭+ where: {‬
‭+ id: facilityId,‬
‭+ },‬
‭+ });‬
‭+ if (!facility) {‬
‭+ logger.error(`Permission was requested for non-existing facility ${facilityId}`);‬
‭+ return null;‬
‭+ }‬
‭+ customerCache.set(facilityId, facility.customerId);‬
‭+ return facility.customerId;‬
‭+}‬
‭+‬
‭+async function hasRole(user: string, id: string, role: UserFacilityRole) {‬
‭+ let facilityRoles = userRolesCache.get(user);‬
‭+ if (!prisma) return false;‬
‭+‬
‭+ if(!facilityRoles) {‬
‭+ facilityRoles = (await prisma.userFacilityRelations.findMany({‬
‭+ where: {‬
‭+ userId: user,‬
‭+ facilityId: id,‬
‭+ },‬
‭+ })).map(x => x.role);‬
‭+ userRolesCache.set(user, facilityRoles);‬
‭+ }‬
‭+ return facilityRoles.some((r) => r === role);‬
‭+}‬
‭+‬
‭+export async function hasFacilityPermission(‬
‭+ user: string,‬
‭+ object: string,‬
‭+ relation: FacilityPolicies | UserFacilityRole,‬
‭+) {‬
‭+ const permissionsRule = facilityPermissions.get(relation);‬
‭+ if (permissionsRule) {‬
‭+ return await permissionsRule(user, object);‬
‭+ } else {‬
‭+ logger.warn(`Unknown facility permission ${relation}`);‬
‭+ return false; //TODO: or throw error?‬
‭+ }‬
‭+}‬
‭+‬
‭+//Sanity check‬
‭ for (const permission of Object.values(FacilityPolicies)) {‬
+
‭+ if (!facilityPermissions.has(permission)) {‬
‭+ logger.error(`Facility permission ${permission} is not registered`);‬
‭+ }‬
‭+}‬
‭diff --git a/src/common/dtos/permissions.ts b/src/common/dtos/permissions.ts‬
‭new file mode 100644‬
‭index 00000000..02003ab4‬
‭--- /dev/null‬
‭+++ b/src/common/dtos/permissions.ts‬
‭@@ -0,0 +1,14 @@‬
‭+export enum CustomerPolicies {‬
‭+ testPermission = "testPermission",‬
‭+}‬
‭+export enum FacilityPolicies {‬
‭+ canViewRooms = "canViewRooms",‬
‭+ canEditForecast = "canEditForecast",‬
‭+ canViewForecast = "canViewForecast",‬
‭+ canViewLots = "canViewLots",‬
‭+}‬
‭+‬
‭+export enum PolicyObjectType {‬
‭+ facility = "facility",‬
‭+ customer = "customer",‬
‭+}‬
‭diff --git a/src/common/schemas/permissions.schemas.ts‬
‭b/src/common/schemas/permissions.schemas.ts‬
‭new file mode 100644‬
‭index 00000000..8583fec6‬
‭--- /dev/null‬
‭+++ b/src/common/schemas/permissions.schemas.ts‬
‭@@ -0,0 +1,25 @@‬
‭+import { z } from "zod";‬
‭+import { CustomerPolicies, FacilityPolicies, PolicyObjectType } from‬
‭"@/common/dtos/permissions";‬
‭+‬
‭+export const customerAccessRequestSchema = z.object({‬
‭+ permission: z.nativeEnum(CustomerPolicies),‬
‭+ resource: z.string(),‬
‭+ resourceType: z.enum([PolicyObjectType.customer]),‬
‭+});‬
‭+‬
‭+export const facilityAccessRequestSchema = z.object({‬
‭+ permission: z.nativeEnum(FacilityPolicies),‬
‭ resource: z.string(),‬
+
‭+ resourceType: z.enum([PolicyObjectType.facility]),‬
‭+});‬
‭+‬
‭+export const accessRequestSchema = z.union([‬
‭+ customerAccessRequestSchema,‬
‭+ facilityAccessRequestSchema,‬
‭+]);‬
‭+export const accessRequestSchemaWithoutResource = z.union([‬
‭+ customerAccessRequestSchema.omit({ resource: true }),‬
‭+ facilityAccessRequestSchema.omit({ resource: true }),‬
‭+]);‬
‭+‬
‭+export type AccessRequest = z.infer<typeof accessRequestSchema>;‬
‭diff --git a/src/components/AppBar/NavLinks.tsx b/src/components/AppBar/NavLinks.tsx‬
‭index 8eed45e0..3eb65dbd 100644‬
‭--- a/src/components/AppBar/NavLinks.tsx‬
‭+++ b/src/components/AppBar/NavLinks.tsx‬
‭@@ -3,21 +3,50 @@ import { useRouter } from "next/router";‬

i‭mport { Button } from "@/components/Button";‬


‭import { trpc } from "@/utils/trpc";‬
‭+import { type UserSessionData } from "@/server/modules/common/auth";‬
‭+import { PermissionEnforcer } from "@/components/PermissionEnforcer/PermissionEnforcer";‬
‭+import { type AccessRequest } from "@/common/schemas/permissions.schemas";‬
‭+import { FacilityPolicies, PolicyObjectType } from "@/common/dtos/permissions";‬

-‭ const LINKS = (isAdmin: boolean): { href: string; title: string }[] => {‬
‭- if (isAdmin) {‬
‭+const LINKS = (data?: UserSessionData): (AccessRequest & { href: string; title: string })[] => {‬
‭+ if (!data) return [];‬
‭+ if (data.role === "ADMIN")‬
‭return [‬
‭- { href: "/admin/recipes", title: "Recipes" },‬
‭- { href: "/admin/users", title: "Users" },‬
‭- { href: "/admin/devices", title: "Devices" },‬
‭+ // { href: "/admin/recipes", title: "Recipes", resource: "recipe", action: "read" },‬
‭+ // /*{ href: "/admin/users", title: "Users", resource: "user", action: "read" },*/‬
‭+ // { href: "/admin/devices", title: "Devices", resource: "device", action: "read" },‬
‭+ ];‬
‭+ else‬
‭+ return [‬
‭+ {‬
‭+ href: "/rooms",‬

+ title: "Rooms",‬
‭+ resourceType: PolicyObjectType.facility,‬
‭+ resource: `${data.facilityId}`,‬
‭+ permission: FacilityPolicies.canViewRooms,‬
‭+ },‬
‭+ {‬
‭+ href: "/lots",‬
‭+ title: "Receiving",‬
‭+ resourceType: PolicyObjectType.facility,‬
‭+ resource: `${data.facilityId}`,‬
‭+ permission: FacilityPolicies.canViewLots,‬
‭+ },‬
‭+ {‬
‭+ href: "/forecast",‬
‭+ title: "Forecast",‬
‭+ resourceType: PolicyObjectType.facility,‬
‭+ resource: `${data.facilityId}`,‬
‭+ permission: FacilityPolicies.canViewForecast,‬
‭+ },‬
‭+ {‬
‭+ href: "/ship-out-forecast",‬
‭+ title: "Ship out",‬
‭+ resourceType: PolicyObjectType.facility,‬
‭+ resource: `${data.facilityId}`,‬
‭+ permission: FacilityPolicies.canViewForecast,‬
‭+ },‬
‭];‬
-‭ }‬
‭- return [‬
‭- { href: "/rooms", title: "Rooms" },‬
‭- { href: "/lots", title: "Receiving" },‬
‭- { href: "/forecast", title: "Forecast" },‬
‭- { href: "/ship-out-forecast", title: "Ship out" },‬
‭- ];‬
‭};‬

‭ xport const NavLinks = () => {‬


e
‭@@ -27,17 +56,21 @@ export const NavLinks = () => {‬

‭return (‬
‭<div className="flex space-x-4">‬
‭- {LINKS(data?.role === "ADMIN").map(({ href, title }) => (‬
‭- <div‬
‭- key={`${title.toLowerCase()}-link-container`}‬
-‭ className={`pb-1 ${‬
‭- isActive(href) ? "border-b-2 border-b-brand-periwinkle font-semibold text-gray-700" : ""‬
‭- }`}‬
‭- >‬
‭- <Link key={`${title.toLowerCase()}-link`} href={href}>‬
‭- <Button.Root variant="link">{title}</Button.Root>‬
‭- </Link>‬
‭- </div>‬
‭+ {LINKS(data).map(({ href, title, ...request }) => (‬
‭+ <PermissionEnforcer key={title} {...request}>‬
‭+ <div‬
‭+ key={`${title.toLowerCase()}-link-container`}‬
‭+ className={`pb-1 ${‬
‭+ isActive(href)‬
‭+ ? "border-b-2 border-b-brand-periwinkle font-semibold text-gray-700"‬
‭+ : ""‬
‭+ }`}‬
‭+ >‬
‭+ <Link key={`${title.toLowerCase()}-link`} href={href}>‬
‭+ <Button.Root variant="link">{title}</Button.Root>‬
‭+ </Link>‬
‭+ </div>‬
‭+ </PermissionEnforcer>‬
‭))}‬
‭</div>‬
‭);‬
‭diff --git a/src/components/PermissionEnforcer/PermissionEnforcer.tsx‬
‭b/src/components/PermissionEnforcer/PermissionEnforcer.tsx‬
‭new file mode 100644‬
‭index 00000000..bbdeed50‬
‭--- /dev/null‬
‭+++ b/src/components/PermissionEnforcer/PermissionEnforcer.tsx‬
‭@@ -0,0 +1,10 @@‬
‭+import { trpc } from "@/utils/trpc";‬
‭+import { type AccessRequest } from "@/common/schemas/permissions.schemas";‬
‭+‬
‭+type PermissionEnforcerProps = React.PropsWithChildren<AccessRequest>;‬
‭+‬
‭+export const PermissionEnforcer = ({ children, ...request }: PermissionEnforcerProps) => {‬
‭+ const { data: isAllowed } = trpc.permissions.hasPermission.useQuery(request);‬
‭+ if (!isAllowed) return null;‬
‭+ return <>{children}</>;‬
‭+};‬
‭diff --git a/src/env/schema.mjs b/src/env/schema.mjs‬
i‭ndex fa7b26f6..75199fc2 100644‬
‭--- a/src/env/schema.mjs‬
‭+++ b/src/env/schema.mjs‬
‭@@ -1,5 +1,5 @@‬
‭// @ts-check‬
‭-import { z } from "zod";‬
‭+import {z} from "zod";‬

/‭**‬
‭* Specify your server-side environment variables schema here.‬
‭diff --git a/src/pages/forecast/index.tsx b/src/pages/forecast/index.tsx‬
‭index 2ddafbf9..6712cf4c 100644‬
‭--- a/src/pages/forecast/index.tsx‬
‭+++ b/src/pages/forecast/index.tsx‬
‭@@ -25,6 +25,8 @@ import { PencilIcon } from "@heroicons/react/24/outline";‬
‭import { getClientLogger } from "@/utils/logging";‬
‭import { DeleteConfirmationModal } from "@/components/Modal/DeleteConfirmationModal";‬
‭import { TrashIcon } from "@heroicons/react/20/solid";‬
‭+import { PermissionEnforcer } from "@/components/PermissionEnforcer/PermissionEnforcer";‬
‭+import {FacilityPolicies, PolicyObjectType} from "@/common/dtos/permissions";‬

‭interface DaysCount {‬
‭Sunday?: number;‬
‭@@ -53,6 +55,7 @@ interface WeekTableValues extends DaysCount {‬

‭const ForecastPage: NextPageWithLayout<SSRPageProps> = ({ user }) => {‬


‭const logger = getClientLogger("ForecastPage");‬
‭+‬
‭const startOfWeek = useMemo(() => {‬
‭const date = new Date();‬
‭return getStartOfWeek(date);‬
‭@@ -183,8 +186,10 @@ const ForecastPage: NextPageWithLayout<SSRPageProps> = ({‬
‭user }) => {‬
‭},‬
‭});‬

-‭ const handleCloseDeleteModal = () => { setShowDeleteModal((prevState) => !prevState);‬


‭setWeekToDelete(""); };‬
‭-‬
‭+ const handleCloseDeleteModal = () => {‬
‭+ setShowDeleteModal((prevState) => !prevState);‬
‭+ setWeekToDelete("");‬
‭+ };‬
‭return (‬
‭<Page.Container>‬
‭@@ -195,9 +200,15 @@ const ForecastPage: NextPageWithLayout<SSRPageProps> = ({‬
‭user }) => {‬
‭onClose={handleCloseDeleteModal}‬
‭onConfirm={handleDeleteConfirm}‬
‭/>‬
‭- <Button.Root variant="primary" onClick={handleAddForecast}>‬
‭- Add Forecast‬
‭- </Button.Root>‬
‭+ <PermissionEnforcer‬
‭+ resourceType={PolicyObjectType.facility}‬
‭+ resource={`${user?.facilityId || ""}`}‬
‭+ permission={FacilityPolicies.canEditForecast}‬
‭+ >‬
‭+ <Button.Root variant="primary" onClick={handleAddForecast}>‬
‭+ Add Forecast‬
‭+ </Button.Root>‬
‭+ </PermissionEnforcer>‬
‭</div>‬
‭{sortedWeeks.map((week, index) => (‬
‭<div key={week}>‬
‭@@ -206,23 +217,35 @@ const ForecastPage: NextPageWithLayout<SSRPageProps> = ({‬
‭user }) => {‬
‭<h2>Week {getWeekIndex(parseDateOnlyStringISO(week))}</h2>‬
‭<div className="mr-10 space-x-2">‬
‭{sortedWeeks.length === index + 1 && (‬
‭- <Button.Root‬
‭+ <PermissionEnforcer‬
‭+ resourceType={PolicyObjectType.facility}‬
‭+ resource={`${user?.facilityId || ""}`}‬
‭+ permission={FacilityPolicies.canEditForecast}‬
‭+ >‬
‭+ <Button.Root‬
‭variant={"primary"}‬
‭withIcon‬
‭onClick={() => handleDeleteForecast(week)}‬
‭>‬
‭- <TrashIcon className="h-5 w-5" aria-hidden="true" />‬
‭- Delete‬
‭- </Button.Root>‬
‭+ <TrashIcon className="h-5 w-5" aria-hidden="true" />‬
‭+ Delete‬
‭+ </Button.Root>‬
‭+ </PermissionEnforcer>‬
‭)}‬
‭- <Button.Root‬
‭- variant={"primary"}‬
‭- withIcon‬
‭- onClick={() => handleEditForecast(week)}‬
‭+ <PermissionEnforcer‬
‭+ resourceType={PolicyObjectType.facility}‬
‭+ resource={`${user?.facilityId || ""}`}‬
‭+ permission={FacilityPolicies.canEditForecast}‬
‭>‬
‭- <PencilIcon className="h-5 w-5" aria-hidden="true" />‬
‭- Edit‬
‭- </Button.Root>‬
‭+ <Button.Root‬
‭+ variant={"primary"}‬
‭+ withIcon‬
‭+ onClick={() => handleEditForecast(week)}‬
‭+ >‬
‭+ <PencilIcon className="h-5 w-5" aria-hidden="true" />‬
‭+ Edit‬
‭+ </Button.Root>‬
‭+ </PermissionEnforcer>‬
‭</div>‬
‭</Card.Header>‬
‭<Card.Body>‬
‭diff --git a/src/server/modules/common/auth/types.ts b/src/server/modules/common/auth/types.ts‬
‭index 8087b994..52c3aa50 100644‬
‭--- a/src/server/modules/common/auth/types.ts‬
‭+++ b/src/server/modules/common/auth/types.ts‬
‭@@ -9,7 +9,7 @@ export type UserSessionData = {‬

/‭** Data passed to Pages after getServerSideProps has run. */‬


‭export type SSRPageProps = {‬
‭- user?: UserSessionData;‬
‭+ user?: UserSessionData; //TODO: why it is optional?‬
‭};‬

‭ eclare module "iron-session" {‬


d
‭diff --git a/src/server/modules/facility/facility.mapper.ts‬
‭b/src/server/modules/facility/facility.mapper.ts‬
‭index a0e67002..a4a7bd5f 100644‬
‭--- a/src/server/modules/facility/facility.mapper.ts‬
‭+++ b/src/server/modules/facility/facility.mapper.ts‬
‭ @ -2,13 +2,13 @@ import type { Facility } from "@prisma/client";‬
@
‭import type { FacilityDTO } from "./interfaces";‬

‭ xport const facilityMapper = {‬


e
‭- toDTO: (dbRecord: Facility): FacilityDTO => ({‬
‭+ toDTO: (dbRecord: Facility & {customer: {name: string}}): FacilityDTO => ({‬
‭id: dbRecord.id,‬
‭createdAt: dbRecord.createdAt,‬
‭updatedAt: dbRecord.updatedAt,‬
‭name: dbRecord.name,‬
‭location: dbRecord.location,‬
‭- customerName: dbRecord.customerName,‬
‭+ customerName: dbRecord.customer.name,‬
‭targetScore: dbRecord.targetScore.toNumber(),‬
‭}),‬
‭};‬
‭diff --git a/src/server/modules/facility/facility.repo.ts b/src/server/modules/facility/facility.repo.ts‬
‭index 28cb4d4d..d4216565 100644‬
‭--- a/src/server/modules/facility/facility.repo.ts‬
‭+++ b/src/server/modules/facility/facility.repo.ts‬
‭@@ -1,12 +1,18 @@‬
‭import { prisma } from "@/server/db/client";‬
‭-import type { Prisma, Facility } from "@prisma/client";‬
‭+import type { Facility, Customer } from "@prisma/client";‬
‭+import { type CreateFacilityRequestDTO } from "@/server/modules/facility/interfaces";‬

-‭ async function findAll(): Promise<Facility[]> {‬


‭- return await prisma.facility.findMany();‬
‭+async function findAll(): Promise<(Facility & { customer: Customer })[]> {‬
‭+ return await prisma.facility.findMany({‬
‭+ include: { customer: true },‬
‭+ });‬
‭}‬

-‭ async function findOneById(id: string): Promise<Facility | null> {‬


‭- return await prisma.facility.findFirst({ where: { id } });‬
‭+async function findOneById(id: string): Promise<(Facility & { customer: Customer }) | null> {‬
‭+ return await prisma.facility.findFirst({‬
‭+ where: { id },‬
‭+ include: { customer: true },‬
‭+ });‬
‭}‬

‭async function findOneByNameLocation({‬


‭@@ -19,8 +25,28 @@ async function findOneByNameLocation({‬
‭return await prisma.facility.findFirst({ where: { name, location } });‬
‭}‬

-‭ async function insert(data: Prisma.FacilityCreateInput): Promise<Facility> {‬


‭- return await prisma.facility.create({ data });‬
‭+async function insert(data: CreateFacilityRequestDTO): Promise<Facility> {‬
‭+ let customer = await prisma.customer.findFirst({‬
‭+ where: {‬
‭+ name: data.customerName,‬
‭+ },‬
‭+ });‬
‭+ if (!customer) {‬
‭+ customer = await prisma.customer.create({‬
‭+ data: {‬
‭+ name: data.customerName,‬
‭+ },‬
‭+ });‬
‭+ }‬
‭+ const customerId = customer.id;‬
‭+ return await prisma.facility.create({‬
‭+ data: {‬
‭+ name: data.name,‬
‭+ customerId: customerId,‬
‭+ targetScore: data.targetScore,‬
‭+ location: data.location,‬
‭+ },‬
‭+ });‬
‭}‬

‭ sync function update(‬


a
‭@@ -31,11 +57,16 @@ async function update(‬
‭customerName?: string;‬
‭targetScore?: number;‬
‭},‬
‭-): Promise<Facility> {‬
‭- return await prisma.facility.update({‬
‭+): Promise<Facility & { customer: Customer }> {‬
‭+ const facility = await prisma.facility.update({‬
‭where: { id },‬
‭data: { ...data },‬
‭});‬
‭+ const customer = await prisma.customer.update({‬
‭+ where: { id: facility.customerId },‬
‭ data: { name: data.customerName },‬
+
‭+ });‬
‭+ return { ...facility, customer };‬
‭}‬

‭ xport const facilityRepo = {‬


e
‭diff --git a/src/server/trpc/router/_app.ts b/src/server/trpc/router/_app.ts‬
‭index f91cd718..ab3036c6 100644‬
‭--- a/src/server/trpc/router/_app.ts‬
‭+++ b/src/server/trpc/router/_app.ts‬
‭@@ -10,6 +10,7 @@ import { appEvent } from "./appEvent";‬
‭import {deviceRouter} from "@/server/trpc/router/device";‬
‭import {facilityRouter} from "@/server/trpc/router/facility";‬
‭import {dailyShipmentForecastRouter} from "@/server/trpc/router/forecast";‬
‭+import {permissionsRouter} from "@/server/trpc/router/permissions";‬

‭export const appRouter = router({‬


‭user: userRouter,‬
‭@@ -23,6 +24,7 @@ export const appRouter = router({‬
‭recommendations: recommendationsRouter,‬
‭dailyShipmentForecast: dailyShipmentForecastRouter,‬
‭event: appEvent,‬
‭+ permissions: permissionsRouter,‬
‭});‬

/‭/ export type definition of API‬


‭diff --git a/src/server/trpc/router/forecast.ts b/src/server/trpc/router/forecast.ts‬
‭index 48c7ff80..33dfd3d5 100644‬
‭--- a/src/server/trpc/router/forecast.ts‬
‭+++ b/src/server/trpc/router/forecast.ts‬
‭@@ -7,10 +7,11 @@ import {‬
‭shipoutForecastOutputSchema,‬
‭} from "@/common/schemas";‬
‭import { formatTRPCError } from "@/server/modules/common/trpc";‬
‭-import { protectedProcedure, router } from "@/server/trpc/trpc";‬
‭+import { protectedProcedure, requireAccess, router } from "@/server/trpc/trpc";‬
‭import { dateOnlyStringISO, parseDateOnlyStringISO } from "@/utils/date-utils";‬
‭import { RipeningCycleManager } from "@/server/modules/CycleStateManager";‬
‭import { differenceInDays, startOfToday } from "date-fns";‬
‭+import { FacilityPolicies, PolicyObjectType } from "@/common/dtos/permissions";‬

‭export const dailyShipmentForecastRouter = router({‬


‭getRoomShipoutForecastByFacilityId: protectedProcedure‬
‭@@ -202,6 +203,16 @@ export const dailyShipmentForecastRouter = router({‬
‭}),‬

‭getForecastStartingFromDate: protectedProcedure‬
‭+ .use(‬
‭+ requireAccess(‬
‭+ {‬
‭+ resourceType: PolicyObjectType.facility,‬
‭+ permission: FacilityPolicies.canViewForecast,‬
‭+ },‬
‭+ (input) => input.facilityId,‬
‭+ dailyForecastRequestSchema,‬
‭+ ),‬
‭+ )‬
‭.input(dailyForecastRequestSchema)‬
‭.output(dailyForecastsSchema)‬
‭.query(async ({ input, ctx }) => {‬
‭@@ -224,11 +235,27 @@ export const dailyShipmentForecastRouter = router({‬
‭}),‬

‭create: protectedProcedure‬
‭+ .use(‬
‭+ requireAccess(‬
‭+ {‬
‭+ resourceType: PolicyObjectType.facility,‬
‭+ permission: FacilityPolicies.canEditForecast,‬
‭+ },‬
‭+ (input, user) => user.facilityId,‬
‭+ ),‬
‭+ )‬
‭.input(createDailyForecastInputSchema)‬
‭.mutation(async ({ input, ctx }): Promise<void> => {‬
‭try {‬
‭+ if (!ctx.session.user) throw formatTRPCError(new Error("User not found")); //should never‬
‭happen because of middlewares‬
‭+ const facilityId = ctx.session.user.facilityId;‬
‭+‬
‭await ctx.prisma.dailyShipmentForecast.createMany({‬
‭- data: input.map((item) => ({ ...item, date: parseDateOnlyStringISO(item.date) })),‬
‭+ data: input.map((item) => ({‬
‭+ ...item,‬
‭+ facilityId: facilityId,‬
‭+ date: parseDateOnlyStringISO(item.date),‬
‭+ })),‬
‭});‬
‭} catch (error) {‬
‭throw formatTRPCError(error);‬
‭@@ -236,14 +263,25 @@ export const dailyShipmentForecastRouter = router({‬
‭}),‬

‭update: protectedProcedure‬
‭+ .use(‬
‭+ requireAccess(‬
‭+ {‬
‭+ resourceType: PolicyObjectType.facility,‬
‭+ permission: FacilityPolicies.canEditForecast,‬
‭+ },‬
‭+ (input, user) => user.facilityId,‬
‭+ ),‬
‭+ )‬
‭.input(createDailyForecastInputSchema)‬
‭.mutation(async ({ input, ctx }): Promise<void> => {‬
‭try {‬
‭+ if (!ctx.session.user) throw formatTRPCError(new Error("User not found")); //should never‬
‭happen because of middlewares‬
‭+‬
‭for (const item of input) {‬
‭await ctx.prisma.dailyShipmentForecast.update({‬
‭where: {‬
‭date_bananaType_facilityId: {‬
‭- facilityId: item.facilityId,‬
‭+ facilityId: ctx.session.user.facilityId,‬
‭date: parseDateOnlyStringISO(item.date),‬
‭bananaType: item.bananaType,‬
‭},‬
‭@@ -259,6 +297,16 @@ export const dailyShipmentForecastRouter = router({‬
‭}),‬

‭delete: protectedProcedure‬
‭+ .use(‬
‭+ requireAccess(‬
‭+ {‬
‭+ resourceType: PolicyObjectType.facility,‬
‭+ permission: FacilityPolicies.canEditForecast,‬
‭+ },‬
‭+ (input, user) => user.facilityId,‬
‭+ deleteDailyForecastInputSchema,‬
‭+ ),‬
‭+ )‬
.‭input(deleteDailyForecastInputSchema)‬
‭.mutation(async ({ input, ctx }): Promise<void> => {‬
‭try {‬
‭diff --git a/src/server/trpc/router/permissions.ts b/src/server/trpc/router/permissions.ts‬
‭new file mode 100644‬
‭index 00000000..a6fa2e7f‬
‭--- /dev/null‬
‭+++ b/src/server/trpc/router/permissions.ts‬
‭@@ -0,0 +1,21 @@‬
‭+import { protectedProcedure, router } from "@/server/trpc/trpc";‬
‭+import { accessRequestSchema } from "@/common/schemas/permissions.schemas";‬
‭+import { TRPCError } from "@trpc/server";‬
‭+import { hasPermission } from "../../../authorization/authorizationModel";‬
‭+‬
‭+export const permissionsRouter = router({‬
‭+ hasPermission: protectedProcedure‬
‭+ .input(accessRequestSchema)‬
‭+ .query(async ({ ctx, input }) => {‬
‭+ if (!ctx.session.user)‬
‭+ throw new TRPCError({‬
‭+ code: "UNAUTHORIZED",‬
‭+ message: "You must be logged in to access this resource.",‬
‭+ }); //should never happen‬
‭+‬
‭+ return await hasPermission(‬
‭+ `${ctx.session.user?.id}`,‬
‭+ input‬
‭+ );‬
‭+ }),‬
‭+});‬
‭diff --git a/src/server/trpc/trpc.ts b/src/server/trpc/trpc.ts‬
‭index 13423f57..ccd13f14 100644‬
‭--- a/src/server/trpc/trpc.ts‬
‭+++ b/src/server/trpc/trpc.ts‬
‭@@ -5,6 +5,9 @@ import { type Context } from "./context";‬

i‭mport { z } from "zod";‬


‭import { getServerLogger } from "@/utils/logging";‬
‭+import { type UserSessionData } from "@/server/modules/common/auth";‬
‭+import { hasPermission } from "../../authorization/authorizationModel";‬
‭+import { type accessRequestSchemaWithoutResource } from‬
‭"@/common/schemas/permissions.schemas";‬

‭const logger = getServerLogger("server/trpc");‬


‭@@ -85,3 +88,39 @@ export const publicProcedure = t.procedure.use(loggerMiddleware);‬
‭* Middleware is called in the order it was added by `.use()`‬
‭**/‬
‭export const protectedProcedure = t.procedure.use(loggerMiddleware).use(isAuthed);‬
‭+‬
‭+export function requireAccess<TInput = void>(‬
‭+ policy: z.infer<typeof accessRequestSchemaWithoutResource>,‬
‭+ resourceName: string | ((input: TInput, user: UserSessionData) => string),‬
‭+ inputSchema?: z.Schema<TInput>,‬
‭+) {‬
‭+ return t.middleware(async ({ ctx, rawInput, next }) => {‬
‭+ if (!ctx.session.user?.id) {‬
‭+ throw new TRPCError({ code: "UNAUTHORIZED" });‬
‭+ }‬
‭+ let resource: string;‬
‭+ if (resourceName instanceof Function) {‬
‭+ if (inputSchema) {‬
‭+ const parseResult = inputSchema.safeParse(rawInput);‬
‭+ if (parseResult.success) {‬
‭+ const input = parseResult.data;‬
‭+ resource = resourceName(input, ctx.session.user);‬
‭+ } else {‬
‭+ throw new TRPCError({ code: "BAD_REQUEST", message: parseResult.error.message‬
‭});‬
‭+ }‬
‭+ } else {‬
‭+ throw new TRPCError({‬
‭+ code: "INTERNAL_SERVER_ERROR",‬
‭+ message: "Invalid configuration: inputSchema is required",‬
‭+ });‬
‭+ }‬
‭+ } else {‬
‭+ resource = resourceName;‬
‭+ }‬
‭+‬
‭+ if (await hasPermission(`${ctx.session.user?.id}`, { ...policy, resource })) {‬
‭+ throw new TRPCError({ code: "FORBIDDEN" });‬
‭+ }‬
‭+ return next();‬
‭+ });‬
‭+}‬

You might also like