diff --git a/apps/backend/package.json b/apps/backend/package.json index 9936b36..90d62b1 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -2,6 +2,7 @@ "name": "backend", "module": "index.ts", "devDependencies": { + "@types/bcryptjs": "^3.0.0", "@types/bun": "latest" }, "peerDependencies": { @@ -14,8 +15,10 @@ "type": "module", "dependencies": { "@trpc/server": "^11.6.0", + "bcryptjs": "^3.0.3", "data-manager-sqlite": "*", "hono": "^4.12.18", + "jose": "^6.2.3", "zod": "^3.25.0" } } diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 6a4096d..13821b9 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -1,43 +1,51 @@ -import { Hono } from 'hono' -import { cors } from 'hono/cors' -import { env } from './lib/env-exporter' -import { appRouter } from './trpc/router' -import { fetchRequestHandler } from '@trpc/server/adapters/fetch' +import { Hono } from "hono"; +import { cors } from "hono/cors"; +import { env } from "./lib/env-exporter"; +import { appRouter } from "./trpc/router"; +import { createContext } from "./trpc/context"; +import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; -const app = new Hono() +const app = new Hono(); app.use( - '*', - cors({ - origin: (origin) => { - // Allow local dev UIs. - if (origin === 'http://localhost:3000') return origin - if (origin === 'http://localhost:3001') return origin - // Non-browser clients (no Origin header) - if (!origin) return '*' - return null - }, - allowHeaders: ['content-type'], - allowMethods: ['GET', 'POST', 'OPTIONS'], - }), -) + "*", + cors({ + origin: (origin) => { + if (origin === "http://localhost:3000") return origin; + if (origin === "http://localhost:3001") return origin; + if (!origin) return "*"; + return null; + }, + allowHeaders: ["content-type"], + allowMethods: ["GET", "POST", "OPTIONS"], + credentials: true, + }), +); -app.get('/health', (c) => c.json({ ok: true })) +app.get("/health", (c) => c.json({ ok: true })); -app.all('/trpc/*', (c) => - fetchRequestHandler({ - endpoint: '/trpc', - req: c.req.raw, - router: appRouter, - }), -) +app.all("/trpc/*", async (c) => { + const resHeaders = new Headers(); + const response = await fetchRequestHandler({ + endpoint: "/trpc", + req: c.req.raw, + router: appRouter, + createContext: () => createContext({ req: c.req.raw, resHeaders }), + }); -export { app } + resHeaders.forEach((value, key) => { + response.headers.set(key, value); + }); -const port = Number(env.PORT || 4004) + return response; +}); + +export { app }; + +const port = Number(env.PORT || 4004); Bun.serve({ - port, - fetch: app.fetch, -}) + port, + fetch: app.fetch, +}); -console.log(`Backend listening on http://localhost:${port}`) +console.log(`Backend listening on http://localhost:${port}`); diff --git a/apps/backend/src/lib/auth.ts b/apps/backend/src/lib/auth.ts new file mode 100644 index 0000000..ff8f79a --- /dev/null +++ b/apps/backend/src/lib/auth.ts @@ -0,0 +1,31 @@ +import { SignJWT, jwtVerify } from "jose"; + +const JWT_SECRET = new TextEncoder().encode( + process.env.JWT_SECRET || "pharmanager-dev-secret-change-me", +); + +const COOKIE_NAME = "pharmanager_token"; +const EXPIRY = "24h"; + +export interface JWTPayload { + staffId: number; + enterpriseId: number; +} + +export async function signJWT(payload: JWTPayload): Promise { + return new SignJWT({ ...payload }) + .setProtectedHeader({ alg: "HS256" }) + .setExpirationTime(EXPIRY) + .sign(JWT_SECRET); +} + +export async function verifyJWT(token: string): Promise { + try { + const { payload } = await jwtVerify(token, JWT_SECRET); + return payload as unknown as JWTPayload; + } catch { + return null; + } +} + +export { COOKIE_NAME }; diff --git a/apps/backend/src/lib/data-manager.ts b/apps/backend/src/lib/data-manager.ts index 890d842..79b8265 100644 --- a/apps/backend/src/lib/data-manager.ts +++ b/apps/backend/src/lib/data-manager.ts @@ -5,12 +5,18 @@ import { createDrugInfoRepo, createUnitsRepo, createStockBatchesRepo, + createEnterpriseRepo, + createStaffRepo, + createEnterpriseStaffRepo, type StorageSpacesRepo, type DistributorsRepo, type ProductsRepo, type DrugInfoRepo, type UnitsRepo, type StockBatchesRepo, + type EnterpriseRepo, + type StaffRepo, + type EnterpriseStaffRepo, } from "data-manager-sqlite"; export class DataManager { @@ -20,6 +26,9 @@ export class DataManager { readonly drugInfo: DrugInfoRepo; readonly units: UnitsRepo; readonly stockBatches: StockBatchesRepo; + readonly enterprises: EnterpriseRepo; + readonly staff: StaffRepo; + readonly enterpriseStaff: EnterpriseStaffRepo; constructor() { const { repo: storageSpacesRepo } = createStorageSpacesRepo(); @@ -28,6 +37,9 @@ export class DataManager { const { repo: drugInfoRepo } = createDrugInfoRepo(); const { repo: unitsRepo } = createUnitsRepo(); const { repo: stockBatchesRepo } = createStockBatchesRepo(); + const { repo: enterpriseRepo } = createEnterpriseRepo(); + const { repo: staffRepo } = createStaffRepo(); + const { repo: enterpriseStaffRepo } = createEnterpriseStaffRepo(); this.storageSpaces = storageSpacesRepo; this.distributors = distributorsRepo; @@ -35,5 +47,8 @@ export class DataManager { this.drugInfo = drugInfoRepo; this.units = unitsRepo; this.stockBatches = stockBatchesRepo; + this.enterprises = enterpriseRepo; + this.staff = staffRepo; + this.enterpriseStaff = enterpriseStaffRepo; } } diff --git a/apps/backend/src/trpc/context.ts b/apps/backend/src/trpc/context.ts new file mode 100644 index 0000000..69742cc --- /dev/null +++ b/apps/backend/src/trpc/context.ts @@ -0,0 +1,26 @@ +import { COOKIE_NAME, verifyJWT, type JWTPayload } from "../lib/auth"; + +export interface Context { + req: Request; + resHeaders: Headers; + staff?: JWTPayload; +} + +export async function createContext(opts: { + req: Request; + resHeaders: Headers; +}): Promise { + const cookieHeader = opts.req.headers.get("cookie") || ""; + const token = cookieHeader + .split("; ") + .find((c) => c.startsWith(`${COOKIE_NAME}=`)) + ?.split("=")[1]; + + const staff = token ? (await verifyJWT(token)) ?? undefined : undefined; + + return { + req: opts.req, + resHeaders: opts.resHeaders, + staff, + }; +} diff --git a/apps/backend/src/trpc/init.ts b/apps/backend/src/trpc/init.ts index d0b7a36..fc3cbe1 100644 --- a/apps/backend/src/trpc/init.ts +++ b/apps/backend/src/trpc/init.ts @@ -1,4 +1,16 @@ -import { initTRPC } from "@trpc/server"; +import { initTRPC, TRPCError } from "@trpc/server"; +import type { Context } from "./context"; + +const t = initTRPC.context().create(); + +export const router = t.router; +export const publicProcedure = t.procedure; + +export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => { + if (!ctx.staff) { + throw new TRPCError({ code: "UNAUTHORIZED" }); + } + return next({ ctx: ctx as Context & { staff: NonNullable } }); +}); -const t = initTRPC.create(); export { t }; diff --git a/apps/backend/src/trpc/pharmanager/v1/auth.ts b/apps/backend/src/trpc/pharmanager/v1/auth.ts new file mode 100644 index 0000000..752e51c --- /dev/null +++ b/apps/backend/src/trpc/pharmanager/v1/auth.ts @@ -0,0 +1,75 @@ +import { z } from "zod"; +import { COOKIE_NAME, signJWT } from "../../../lib/auth"; +import { dataManager } from "../../../lib/data-manager-instance"; +import { publicProcedure, protectedProcedure, router } from "../../init"; + +export const authRouter = router({ + login: publicProcedure + .input(z.object({ username: z.string(), password: z.string() })) + .mutation(async ({ ctx, input }) => { + const staff = await dataManager.staff.getStaffByUsername( + input.username, + ); + if (!staff) { + throw new Error("Invalid username or password"); + } + + const valid = await dataManager.staff.verifyPassword( + staff, + input.password, + ); + if (!valid) { + throw new Error("Invalid username or password"); + } + + const enterpriseStaff = dataManager.enterpriseStaff.getByStaffId( + staff.id, + ); + if (!enterpriseStaff) { + throw new Error("No enterprise assigned"); + } + + const token = await signJWT({ + staffId: staff.id, + enterpriseId: enterpriseStaff.enterprise_id, + }); + + ctx.resHeaders.set( + "Set-Cookie", + `${COOKIE_NAME}=${token}; HttpOnly; Path=/; Max-Age=86400; SameSite=Lax`, + ); + + return { + staff: { + id: staff.id, + name: staff.name, + username: staff.username, + is_password_reset_needed: staff.is_password_reset_needed, + }, + enterprise_id: enterpriseStaff.enterprise_id, + }; + }), + + whoAmI: protectedProcedure.query(async ({ ctx }) => { + const staff = await dataManager.staff.getStaffById(ctx.staff.staffId); + if (!staff) { + throw new Error("Staff not found"); + } + + const enterprise = await dataManager.enterprises.getEnterpriseById( + ctx.staff.enterpriseId, + ); + + return { + staff: { + id: staff.id, + name: staff.name, + username: staff.username, + email: staff.email, + mobile: staff.mobile, + is_password_reset_needed: staff.is_password_reset_needed, + }, + enterprise, + }; + }), +}); diff --git a/apps/backend/src/trpc/router.ts b/apps/backend/src/trpc/router.ts index 0f9e0e6..261accf 100644 --- a/apps/backend/src/trpc/router.ts +++ b/apps/backend/src/trpc/router.ts @@ -1,18 +1,20 @@ -import { t } from "./init"; +import { router } from "./init"; import { storageRouter } from "./pharmanager/v1/storage"; import { distributorRouter } from "./pharmanager/v1/distributor"; import { productRouter } from "./pharmanager/v1/product"; import { drugInfoRouter } from "./pharmanager/v1/drugInfo"; import { unitsRouter } from "./pharmanager/v1/units"; import { stockRouter } from "./pharmanager/v1/stock"; +import { authRouter } from "./pharmanager/v1/auth"; -export const appRouter = t.router({ +export const appRouter = router({ storage: storageRouter, distributor: distributorRouter, product: productRouter, drugInfo: drugInfoRouter, units: unitsRouter, stock: stockRouter, + auth: authRouter, }); export type AppRouter = typeof appRouter; diff --git a/packages/data-manager-sqlite/drizzle/0005_yielding_silver_fox.sql b/packages/data-manager-sqlite/drizzle/0005_yielding_silver_fox.sql new file mode 100644 index 0000000..4372abb --- /dev/null +++ b/packages/data-manager-sqlite/drizzle/0005_yielding_silver_fox.sql @@ -0,0 +1,65 @@ +CREATE TABLE `enterprises` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `type` text NOT NULL, + `owner_name` text NOT NULL, + `mobile` text NOT NULL, + `address` text +); +--> statement-breakpoint +CREATE TABLE `staff` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `username` text NOT NULL, + `email` text, + `mobile` text, + `added_on` text NOT NULL, + `password` text NOT NULL, + `is_password_reset_needed` integer DEFAULT true NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `staff_username_unique` ON `staff` (`username`);--> statement-breakpoint +CREATE TABLE `roles` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `roles_name_unique` ON `roles` (`name`);--> statement-breakpoint +CREATE TABLE `permissions` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `permissions_name_unique` ON `permissions` (`name`);--> statement-breakpoint +CREATE TABLE `role_permissions` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `role_id` integer NOT NULL, + `permission_id` integer NOT NULL, + FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`permission_id`) REFERENCES `permissions`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `staff_roles` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `staff_id` integer NOT NULL, + `role_id` integer NOT NULL, + FOREIGN KEY (`staff_id`) REFERENCES `staff`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `enterprise_staff` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `staff_id` integer NOT NULL, + `enterprise_id` integer NOT NULL, + FOREIGN KEY (`staff_id`) REFERENCES `staff`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`enterprise_id`) REFERENCES `enterprises`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +ALTER TABLE `storage_spaces` ADD `enterprise_id` integer REFERENCES enterprises(id);--> statement-breakpoint +CREATE INDEX `storage_spaces_enterprise_idx` ON `storage_spaces` (`enterprise_id`);--> statement-breakpoint +ALTER TABLE `distributors` ADD `enterprise_id` integer REFERENCES enterprises(id);--> statement-breakpoint +CREATE INDEX `distributors_enterprise_idx` ON `distributors` (`enterprise_id`);--> statement-breakpoint +ALTER TABLE `products` ADD `enterprise_id` integer REFERENCES enterprises(id);--> statement-breakpoint +CREATE INDEX `products_enterprise_idx` ON `products` (`enterprise_id`);--> statement-breakpoint +ALTER TABLE `stock_batches` ADD `enterprise_id` integer REFERENCES enterprises(id);--> statement-breakpoint +CREATE INDEX `stock_batches_enterprise_idx` ON `stock_batches` (`enterprise_id`); \ No newline at end of file diff --git a/packages/data-manager-sqlite/drizzle/meta/0005_snapshot.json b/packages/data-manager-sqlite/drizzle/meta/0005_snapshot.json new file mode 100644 index 0000000..01effe9 --- /dev/null +++ b/packages/data-manager-sqlite/drizzle/meta/0005_snapshot.json @@ -0,0 +1,999 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "4ebc4f18-552b-459a-abef-dc2dfe600cd2", + "prevId": "dfa18399-c3f0-4ef0-896b-632b38f67c1f", + "tables": { + "storage_spaces": { + "name": "storage_spaces", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "aliases": { + "name": "aliases", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "image_urls": { + "name": "image_urls", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "enterprise_id": { + "name": "enterprise_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "storage_spaces_enterprise_idx": { + "name": "storage_spaces_enterprise_idx", + "columns": [ + "enterprise_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "storage_spaces_enterprise_id_enterprises_id_fk": { + "name": "storage_spaces_enterprise_id_enterprises_id_fk", + "tableFrom": "storage_spaces", + "tableTo": "enterprises", + "columnsFrom": [ + "enterprise_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "distributors": { + "name": "distributors", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "agency": { + "name": "agency", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contact": { + "name": "contact", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mobile": { + "name": "mobile", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enterprise_id": { + "name": "enterprise_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "distributors_enterprise_idx": { + "name": "distributors_enterprise_idx", + "columns": [ + "enterprise_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "distributors_enterprise_id_enterprises_id_fk": { + "name": "distributors_enterprise_id_enterprises_id_fk", + "tableFrom": "distributors", + "tableTo": "enterprises", + "columnsFrom": [ + "enterprise_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "products": { + "name": "products", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "distributor_id": { + "name": "distributor_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unit_id": { + "name": "unit_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "procured_price": { + "name": "procured_price", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mrp": { + "name": "mrp", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "selling_price": { + "name": "selling_price", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reorder_level": { + "name": "reorder_level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "units_per_strip": { + "name": "units_per_strip", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hide_product_from_public": { + "name": "hide_product_from_public", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "hide_price_from_public": { + "name": "hide_price_from_public", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enterprise_id": { + "name": "enterprise_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "products_enterprise_idx": { + "name": "products_enterprise_idx", + "columns": [ + "enterprise_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "products_distributor_id_distributors_id_fk": { + "name": "products_distributor_id_distributors_id_fk", + "tableFrom": "products", + "tableTo": "distributors", + "columnsFrom": [ + "distributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "products_unit_id_units_id_fk": { + "name": "products_unit_id_units_id_fk", + "tableFrom": "products", + "tableTo": "units", + "columnsFrom": [ + "unit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "products_enterprise_id_enterprises_id_fk": { + "name": "products_enterprise_id_enterprises_id_fk", + "tableFrom": "products", + "tableTo": "enterprises", + "columnsFrom": [ + "enterprise_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "drug_info": { + "name": "drug_info", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "drug_info_name_unique": { + "name": "drug_info_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "units": { + "name": "units", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "units_name_unique": { + "name": "units_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "product_compositions": { + "name": "product_compositions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "drug_info_id": { + "name": "drug_info_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unit_id": { + "name": "unit_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "product_compositions_product_id_products_id_fk": { + "name": "product_compositions_product_id_products_id_fk", + "tableFrom": "product_compositions", + "tableTo": "products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "product_compositions_drug_info_id_drug_info_id_fk": { + "name": "product_compositions_drug_info_id_drug_info_id_fk", + "tableFrom": "product_compositions", + "tableTo": "drug_info", + "columnsFrom": [ + "drug_info_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "product_compositions_unit_id_units_id_fk": { + "name": "product_compositions_unit_id_units_id_fk", + "tableFrom": "product_compositions", + "tableTo": "units", + "columnsFrom": [ + "unit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stock_batches": { + "name": "stock_batches", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "product_id": { + "name": "product_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "arrived": { + "name": "arrived", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "batch_no": { + "name": "batch_no", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mfg": { + "name": "mfg", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expiry": { + "name": "expiry", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rack_id": { + "name": "rack_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "distributor_id": { + "name": "distributor_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enterprise_id": { + "name": "enterprise_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "stock_batches_enterprise_idx": { + "name": "stock_batches_enterprise_idx", + "columns": [ + "enterprise_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "stock_batches_product_id_products_id_fk": { + "name": "stock_batches_product_id_products_id_fk", + "tableFrom": "stock_batches", + "tableTo": "products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "stock_batches_rack_id_storage_spaces_id_fk": { + "name": "stock_batches_rack_id_storage_spaces_id_fk", + "tableFrom": "stock_batches", + "tableTo": "storage_spaces", + "columnsFrom": [ + "rack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "stock_batches_distributor_id_distributors_id_fk": { + "name": "stock_batches_distributor_id_distributors_id_fk", + "tableFrom": "stock_batches", + "tableTo": "distributors", + "columnsFrom": [ + "distributor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "stock_batches_enterprise_id_enterprises_id_fk": { + "name": "stock_batches_enterprise_id_enterprises_id_fk", + "tableFrom": "stock_batches", + "tableTo": "enterprises", + "columnsFrom": [ + "enterprise_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "enterprises": { + "name": "enterprises", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_name": { + "name": "owner_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mobile": { + "name": "mobile", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "staff": { + "name": "staff", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mobile": { + "name": "mobile", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "added_on": { + "name": "added_on", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_password_reset_needed": { + "name": "is_password_reset_needed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + } + }, + "indexes": { + "staff_username_unique": { + "name": "staff_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "roles": { + "name": "roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "roles_name_unique": { + "name": "roles_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "permissions": { + "name": "permissions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "permissions_name_unique": { + "name": "permissions_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "role_permissions": { + "name": "role_permissions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission_id": { + "name": "permission_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "role_permissions_role_id_roles_id_fk": { + "name": "role_permissions_role_id_roles_id_fk", + "tableFrom": "role_permissions", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "role_permissions_permission_id_permissions_id_fk": { + "name": "role_permissions_permission_id_permissions_id_fk", + "tableFrom": "role_permissions", + "tableTo": "permissions", + "columnsFrom": [ + "permission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "staff_roles": { + "name": "staff_roles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "staff_id": { + "name": "staff_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role_id": { + "name": "role_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "staff_roles_staff_id_staff_id_fk": { + "name": "staff_roles_staff_id_staff_id_fk", + "tableFrom": "staff_roles", + "tableTo": "staff", + "columnsFrom": [ + "staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "staff_roles_role_id_roles_id_fk": { + "name": "staff_roles_role_id_roles_id_fk", + "tableFrom": "staff_roles", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "enterprise_staff": { + "name": "enterprise_staff", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "staff_id": { + "name": "staff_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enterprise_id": { + "name": "enterprise_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "enterprise_staff_staff_id_staff_id_fk": { + "name": "enterprise_staff_staff_id_staff_id_fk", + "tableFrom": "enterprise_staff", + "tableTo": "staff", + "columnsFrom": [ + "staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "enterprise_staff_enterprise_id_enterprises_id_fk": { + "name": "enterprise_staff_enterprise_id_enterprises_id_fk", + "tableFrom": "enterprise_staff", + "tableTo": "enterprises", + "columnsFrom": [ + "enterprise_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/data-manager-sqlite/drizzle/meta/_journal.json b/packages/data-manager-sqlite/drizzle/meta/_journal.json index a435b2b..63a801c 100644 --- a/packages/data-manager-sqlite/drizzle/meta/_journal.json +++ b/packages/data-manager-sqlite/drizzle/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1779533139096, "tag": "0004_ambiguous_captain_america", "breakpoints": true + }, + { + "idx": 5, + "version": "6", + "when": 1779546674621, + "tag": "0005_yielding_silver_fox", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/data-manager-sqlite/package.json b/packages/data-manager-sqlite/package.json index d3ddb6d..3d30463 100644 --- a/packages/data-manager-sqlite/package.json +++ b/packages/data-manager-sqlite/package.json @@ -11,12 +11,14 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { + "bcryptjs": "^3.0.3", "drizzle-orm": "^0.44.5" }, "devDependencies": { - "drizzle-kit": "^0.31.4", - "@types/node": "^22.10.2", + "@types/bcryptjs": "^3.0.0", "@types/bun": "^1.3.14", + "@types/node": "^22.10.2", + "drizzle-kit": "^0.31.4", "typescript": "^5.9.3" } } diff --git a/packages/data-manager-sqlite/src/db-instance.ts b/packages/data-manager-sqlite/src/db-instance.ts index 9c9992a..62e219d 100644 --- a/packages/data-manager-sqlite/src/db-instance.ts +++ b/packages/data-manager-sqlite/src/db-instance.ts @@ -1,13 +1,56 @@ +import bcrypt from 'bcryptjs' import { createDb } from './db' import { runMigrations } from './migrate' const { db, sqlite } = createDb() runMigrations(sqlite) -// Seed reference data +// Seed reference data — units sqlite.run("INSERT OR IGNORE INTO units (name) VALUES ('mg')") sqlite.run("INSERT OR IGNORE INTO units (name) VALUES ('gm')") sqlite.run("INSERT OR IGNORE INTO units (name) VALUES ('ml')") sqlite.run("INSERT OR IGNORE INTO units (name) VALUES ('piece')") +// Seed enterprise +sqlite.run("INSERT OR IGNORE INTO enterprises (id, name, type, owner_name, mobile) VALUES (1, 'Main Pharmacy', 'Pharmacy', 'Admin User', '0000000000')") + +// Seed staff (admin user) +const today = new Date().toISOString().split('T')[0] +const adminHash = bcrypt.hashSync('admin123', 10) +sqlite.run( + "INSERT OR IGNORE INTO staff (id, name, username, password, added_on, is_password_reset_needed) VALUES (1, 'Admin', 'admin', ?, ?, 1)", + [adminHash, today], +) + +// Link staff to enterprise +sqlite.run("INSERT OR IGNORE INTO enterprise_staff (id, staff_id, enterprise_id) VALUES (1, 1, 1)") + +// Seed roles +sqlite.run("INSERT OR IGNORE INTO roles (id, name) VALUES (1, 'Admin')") +sqlite.run("INSERT OR IGNORE INTO roles (id, name) VALUES (2, 'Pharmacist')") +sqlite.run("INSERT OR IGNORE INTO roles (id, name) VALUES (3, 'Manager')") +sqlite.run("INSERT OR IGNORE INTO roles (id, name) VALUES (4, 'Cashier')") + +// Seed permissions +const permNames = [ + 'product.read', 'product.write', + 'stock.read', 'stock.write', + 'distributor.read', 'distributor.write', + 'storage.read', 'storage.write', + 'billing.read', 'billing.write', + 'staff.read', 'staff.write', + 'customer.read', 'customer.write', +] +for (let i = 0; i < permNames.length; i++) { + sqlite.run(`INSERT OR IGNORE INTO permissions (id, name) VALUES (${i + 1}, '${permNames[i]}')`) +} + +// Seed role_permissions — Admin gets all +for (let i = 1; i <= permNames.length; i++) { + sqlite.run(`INSERT OR IGNORE INTO role_permissions (role_id, permission_id) VALUES (1, ${i})`) +} + +// Seed staff_roles — admin gets Admin role +sqlite.run("INSERT OR IGNORE INTO staff_roles (staff_id, role_id) VALUES (1, 1)") + export { db, sqlite } diff --git a/packages/data-manager-sqlite/src/enterpriseStaff.ts b/packages/data-manager-sqlite/src/enterpriseStaff.ts new file mode 100644 index 0000000..671ff00 --- /dev/null +++ b/packages/data-manager-sqlite/src/enterpriseStaff.ts @@ -0,0 +1,42 @@ +import { eq } from 'drizzle-orm' + +import { db } from './db-instance' +import { enterpriseStaff } from './schema/enterpriseStaff' + +export type EnterpriseStaff = { + id: number + staff_id: number + enterprise_id: number +} + +export type EnterpriseStaffRepo = { + getByStaffId: (staffId: number) => EnterpriseStaff | null + create: (input: { staff_id: number; enterprise_id: number }) => EnterpriseStaff +} + +function toEnterpriseStaff(row: typeof enterpriseStaff.$inferSelect): EnterpriseStaff { + return { + id: row.id, + staff_id: row.staffId, + enterprise_id: row.enterpriseId, + } +} + +export function createEnterpriseStaffRepo(): { repo: EnterpriseStaffRepo } { + const repo: EnterpriseStaffRepo = { + getByStaffId(staffId) { + const row = db.select().from(enterpriseStaff).where(eq(enterpriseStaff.staffId, staffId)).get() + return row ? toEnterpriseStaff(row) : null + }, + + create(input) { + const created = db.insert(enterpriseStaff).values({ + staffId: input.staff_id, + enterpriseId: input.enterprise_id, + }).returning().get() + return toEnterpriseStaff(created) + }, + } + + return { repo } +} diff --git a/packages/data-manager-sqlite/src/enterprises.ts b/packages/data-manager-sqlite/src/enterprises.ts new file mode 100644 index 0000000..77fc8b2 --- /dev/null +++ b/packages/data-manager-sqlite/src/enterprises.ts @@ -0,0 +1,51 @@ +import { eq } from 'drizzle-orm' + +import { db } from './db-instance' +import { enterprises } from './schema/enterprises' + +export type Enterprise = { + id: number + name: string + type: string + owner_name: string + mobile: string + address: string | null +} + +export type EnterpriseRepo = { + getEnterpriseById: (id: number) => Promise + createEnterprise: (input: { name: string; type: string; owner_name: string; mobile: string; address?: string | null }) => Promise +} + +function toEnterprise(row: typeof enterprises.$inferSelect): Enterprise { + return { + id: row.id, + name: row.name, + type: row.type, + owner_name: row.ownerName, + mobile: row.mobile, + address: row.address, + } +} + +export function createEnterpriseRepo(): { repo: EnterpriseRepo } { + const repo: EnterpriseRepo = { + getEnterpriseById(id) { + const row = db.select().from(enterprises).where(eq(enterprises.id, id)).get() + return Promise.resolve(row ? toEnterprise(row) : null) + }, + + createEnterprise(input) { + const created = db.insert(enterprises).values({ + name: input.name, + type: input.type, + ownerName: input.owner_name, + mobile: input.mobile, + address: input.address ?? null, + }).returning().get() + return Promise.resolve(toEnterprise(created)) + }, + } + + return { repo } +} diff --git a/packages/data-manager-sqlite/src/index.ts b/packages/data-manager-sqlite/src/index.ts index 20baf35..893bcb8 100644 --- a/packages/data-manager-sqlite/src/index.ts +++ b/packages/data-manager-sqlite/src/index.ts @@ -40,3 +40,25 @@ export { type StockBatchesRepo, } from './stockBatches' export { stockBatches } from './schema/stockBatches' +export { enterprises } from './schema/enterprises' +export { staff } from './schema/staff' +export { roles } from './schema/roles' +export { permissions } from './schema/permissions' +export { rolePermissions } from './schema/rolePermissions' +export { staffRoles } from './schema/staffRoles' +export { enterpriseStaff } from './schema/enterpriseStaff' +export { + createEnterpriseRepo, + type Enterprise, + type EnterpriseRepo, +} from './enterprises' +export { + createStaffRepo, + type Staff, + type StaffRepo, +} from './staff' +export { + createEnterpriseStaffRepo, + type EnterpriseStaff, + type EnterpriseStaffRepo, +} from './enterpriseStaff' diff --git a/packages/data-manager-sqlite/src/schema/distributors.ts b/packages/data-manager-sqlite/src/schema/distributors.ts index 3f325ef..df24036 100644 --- a/packages/data-manager-sqlite/src/schema/distributors.ts +++ b/packages/data-manager-sqlite/src/schema/distributors.ts @@ -1,4 +1,5 @@ -import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' +import { integer, sqliteTable, text, index } from 'drizzle-orm/sqlite-core' +import { enterprises } from './enterprises' export const distributors = sqliteTable('distributors', { id: integer('id').primaryKey({ autoIncrement: true }), @@ -6,4 +7,7 @@ export const distributors = sqliteTable('distributors', { contact: text('contact').notNull(), mobile: text('mobile').notNull(), address: text('address'), -}) + enterpriseId: integer('enterprise_id').references(() => enterprises.id), +}, (table) => ({ + enterpriseIdx: index('distributors_enterprise_idx').on(table.enterpriseId), +})) diff --git a/packages/data-manager-sqlite/src/schema/enterpriseStaff.ts b/packages/data-manager-sqlite/src/schema/enterpriseStaff.ts new file mode 100644 index 0000000..be31134 --- /dev/null +++ b/packages/data-manager-sqlite/src/schema/enterpriseStaff.ts @@ -0,0 +1,9 @@ +import { integer, sqliteTable } from 'drizzle-orm/sqlite-core' +import { staff } from './staff' +import { enterprises } from './enterprises' + +export const enterpriseStaff = sqliteTable('enterprise_staff', { + id: integer('id').primaryKey({ autoIncrement: true }), + staffId: integer('staff_id').notNull().references(() => staff.id), + enterpriseId: integer('enterprise_id').notNull().references(() => enterprises.id), +}) diff --git a/packages/data-manager-sqlite/src/schema/enterprises.ts b/packages/data-manager-sqlite/src/schema/enterprises.ts new file mode 100644 index 0000000..627756e --- /dev/null +++ b/packages/data-manager-sqlite/src/schema/enterprises.ts @@ -0,0 +1,10 @@ +import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' + +export const enterprises = sqliteTable('enterprises', { + id: integer('id').primaryKey({ autoIncrement: true }), + name: text('name').notNull(), + type: text('type').notNull(), + ownerName: text('owner_name').notNull(), + mobile: text('mobile').notNull(), + address: text('address'), +}) diff --git a/packages/data-manager-sqlite/src/schema/index.ts b/packages/data-manager-sqlite/src/schema/index.ts index adeb0af..e5b409b 100644 --- a/packages/data-manager-sqlite/src/schema/index.ts +++ b/packages/data-manager-sqlite/src/schema/index.ts @@ -5,3 +5,10 @@ export * from './drugInfo' export * from './units' export * from './productCompositions' export * from './stockBatches' +export * from './enterprises' +export * from './staff' +export * from './roles' +export * from './permissions' +export * from './rolePermissions' +export * from './staffRoles' +export * from './enterpriseStaff' diff --git a/packages/data-manager-sqlite/src/schema/permissions.ts b/packages/data-manager-sqlite/src/schema/permissions.ts new file mode 100644 index 0000000..483c49b --- /dev/null +++ b/packages/data-manager-sqlite/src/schema/permissions.ts @@ -0,0 +1,6 @@ +import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' + +export const permissions = sqliteTable('permissions', { + id: integer('id').primaryKey({ autoIncrement: true }), + name: text('name').notNull().unique(), +}) diff --git a/packages/data-manager-sqlite/src/schema/products.ts b/packages/data-manager-sqlite/src/schema/products.ts index 137d333..b8f89ce 100644 --- a/packages/data-manager-sqlite/src/schema/products.ts +++ b/packages/data-manager-sqlite/src/schema/products.ts @@ -1,6 +1,7 @@ -import { integer, sqliteTable, text, real } from 'drizzle-orm/sqlite-core' +import { integer, sqliteTable, text, real, index } from 'drizzle-orm/sqlite-core' import { distributors } from './distributors' import { units } from './units' +import { enterprises } from './enterprises' export const products = sqliteTable('products', { id: integer('id').primaryKey({ autoIncrement: true }), @@ -17,4 +18,7 @@ export const products = sqliteTable('products', { unitsPerStrip: integer('units_per_strip'), hideProductFromPublic: integer('hide_product_from_public', { mode: 'boolean' }).notNull().default(false), hidePriceFromPublic: integer('hide_price_from_public', { mode: 'boolean' }).notNull().default(false), -}) + enterpriseId: integer('enterprise_id').references(() => enterprises.id), +}, (table) => ({ + enterpriseIdx: index('products_enterprise_idx').on(table.enterpriseId), +})) diff --git a/packages/data-manager-sqlite/src/schema/rolePermissions.ts b/packages/data-manager-sqlite/src/schema/rolePermissions.ts new file mode 100644 index 0000000..b92b154 --- /dev/null +++ b/packages/data-manager-sqlite/src/schema/rolePermissions.ts @@ -0,0 +1,9 @@ +import { integer, sqliteTable } from 'drizzle-orm/sqlite-core' +import { roles } from './roles' +import { permissions } from './permissions' + +export const rolePermissions = sqliteTable('role_permissions', { + id: integer('id').primaryKey({ autoIncrement: true }), + roleId: integer('role_id').notNull().references(() => roles.id), + permissionId: integer('permission_id').notNull().references(() => permissions.id), +}) diff --git a/packages/data-manager-sqlite/src/schema/roles.ts b/packages/data-manager-sqlite/src/schema/roles.ts new file mode 100644 index 0000000..c539c95 --- /dev/null +++ b/packages/data-manager-sqlite/src/schema/roles.ts @@ -0,0 +1,6 @@ +import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' + +export const roles = sqliteTable('roles', { + id: integer('id').primaryKey({ autoIncrement: true }), + name: text('name').notNull().unique(), +}) diff --git a/packages/data-manager-sqlite/src/schema/staff.ts b/packages/data-manager-sqlite/src/schema/staff.ts new file mode 100644 index 0000000..df040b3 --- /dev/null +++ b/packages/data-manager-sqlite/src/schema/staff.ts @@ -0,0 +1,12 @@ +import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' + +export const staff = sqliteTable('staff', { + id: integer('id').primaryKey({ autoIncrement: true }), + name: text('name').notNull(), + username: text('username').notNull().unique(), + email: text('email'), + mobile: text('mobile'), + addedOn: text('added_on').notNull(), + password: text('password').notNull(), + isPasswordResetNeeded: integer('is_password_reset_needed', { mode: 'boolean' }).notNull().default(true), +}) diff --git a/packages/data-manager-sqlite/src/schema/staffRoles.ts b/packages/data-manager-sqlite/src/schema/staffRoles.ts new file mode 100644 index 0000000..3ef94ae --- /dev/null +++ b/packages/data-manager-sqlite/src/schema/staffRoles.ts @@ -0,0 +1,9 @@ +import { integer, sqliteTable } from 'drizzle-orm/sqlite-core' +import { staff } from './staff' +import { roles } from './roles' + +export const staffRoles = sqliteTable('staff_roles', { + id: integer('id').primaryKey({ autoIncrement: true }), + staffId: integer('staff_id').notNull().references(() => staff.id), + roleId: integer('role_id').notNull().references(() => roles.id), +}) diff --git a/packages/data-manager-sqlite/src/schema/stockBatches.ts b/packages/data-manager-sqlite/src/schema/stockBatches.ts index 620ada5..f335a76 100644 --- a/packages/data-manager-sqlite/src/schema/stockBatches.ts +++ b/packages/data-manager-sqlite/src/schema/stockBatches.ts @@ -1,7 +1,8 @@ -import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' +import { integer, sqliteTable, text, index } from 'drizzle-orm/sqlite-core' import { products } from './products' import { storageSpaces } from './storageSpacesSchema' import { distributors } from './distributors' +import { enterprises } from './enterprises' export const stockBatches = sqliteTable('stock_batches', { id: integer('id').primaryKey({ autoIncrement: true }), @@ -14,4 +15,7 @@ export const stockBatches = sqliteTable('stock_batches', { distributorId: integer('distributor_id').references(() => distributors.id), quantity: integer('quantity').notNull().default(0), isDefault: integer('is_default', { mode: 'boolean' }).notNull().default(false), -}) + enterpriseId: integer('enterprise_id').references(() => enterprises.id), +}, (table) => ({ + enterpriseIdx: index('stock_batches_enterprise_idx').on(table.enterpriseId), +})) diff --git a/packages/data-manager-sqlite/src/schema/storageSpacesSchema.ts b/packages/data-manager-sqlite/src/schema/storageSpacesSchema.ts index e24c3d7..532c968 100644 --- a/packages/data-manager-sqlite/src/schema/storageSpacesSchema.ts +++ b/packages/data-manager-sqlite/src/schema/storageSpacesSchema.ts @@ -1,10 +1,13 @@ -import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core' +import { integer, sqliteTable, text, index } from 'drizzle-orm/sqlite-core' +import { enterprises } from './enterprises' export const storageSpaces = sqliteTable('storage_spaces', { id: integer('id').primaryKey({ autoIncrement: true }), name: text('name').notNull(), description: text('description'), aliases: text('aliases').notNull().default('[]'), - // JSON array string of image URLs imageUrls: text('image_urls').notNull().default('[]'), -}) + enterpriseId: integer('enterprise_id').references(() => enterprises.id), +}, (table) => ({ + enterpriseIdx: index('storage_spaces_enterprise_idx').on(table.enterpriseId), +})) diff --git a/packages/data-manager-sqlite/src/staff.ts b/packages/data-manager-sqlite/src/staff.ts new file mode 100644 index 0000000..56484f9 --- /dev/null +++ b/packages/data-manager-sqlite/src/staff.ts @@ -0,0 +1,70 @@ +import { eq } from 'drizzle-orm' +import bcrypt from 'bcryptjs' + +import { db } from './db-instance' +import { staff } from './schema/staff' + +export type Staff = { + id: number + name: string + username: string + email: string | null + mobile: string | null + added_on: string + password: string + is_password_reset_needed: boolean +} + +export type StaffRepo = { + getStaffById: (id: number) => Promise + getStaffByUsername: (username: string) => Promise + createStaff: (input: { name: string; username: string; email?: string | null; mobile?: string | null; added_on: string; password: string; is_password_reset_needed?: boolean }) => Promise + verifyPassword: (staff: Staff, password: string) => Promise +} + +function toStaff(row: typeof staff.$inferSelect): Staff { + return { + id: row.id, + name: row.name, + username: row.username, + email: row.email, + mobile: row.mobile, + added_on: row.addedOn, + password: row.password, + is_password_reset_needed: row.isPasswordResetNeeded, + } +} + +export function createStaffRepo(): { repo: StaffRepo } { + const repo: StaffRepo = { + getStaffById(id) { + const row = db.select().from(staff).where(eq(staff.id, id)).get() + return Promise.resolve(row ? toStaff(row) : null) + }, + + getStaffByUsername(username) { + const row = db.select().from(staff).where(eq(staff.username, username)).get() + return Promise.resolve(row ? toStaff(row) : null) + }, + + createStaff(input) { + const hashed = bcrypt.hashSync(input.password, 10) + const created = db.insert(staff).values({ + name: input.name, + username: input.username, + email: input.email ?? null, + mobile: input.mobile ?? null, + addedOn: input.added_on, + password: hashed, + isPasswordResetNeeded: input.is_password_reset_needed ?? true, + }).returning().get() + return Promise.resolve(toStaff(created)) + }, + + async verifyPassword(s, password) { + return bcrypt.compare(password, s.password) + }, + } + + return { repo } +}