auth init

This commit is contained in:
shafi54 2026-05-23 20:17:38 +05:30
parent 3fd769a36f
commit 51bfb1513a
29 changed files with 1606 additions and 50 deletions

View file

@ -2,6 +2,7 @@
"name": "backend", "name": "backend",
"module": "index.ts", "module": "index.ts",
"devDependencies": { "devDependencies": {
"@types/bcryptjs": "^3.0.0",
"@types/bun": "latest" "@types/bun": "latest"
}, },
"peerDependencies": { "peerDependencies": {
@ -14,8 +15,10 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@trpc/server": "^11.6.0", "@trpc/server": "^11.6.0",
"bcryptjs": "^3.0.3",
"data-manager-sqlite": "*", "data-manager-sqlite": "*",
"hono": "^4.12.18", "hono": "^4.12.18",
"jose": "^6.2.3",
"zod": "^3.25.0" "zod": "^3.25.0"
} }
} }

View file

@ -1,43 +1,51 @@
import { Hono } from 'hono' import { Hono } from "hono";
import { cors } from 'hono/cors' import { cors } from "hono/cors";
import { env } from './lib/env-exporter' import { env } from "./lib/env-exporter";
import { appRouter } from './trpc/router' import { appRouter } from "./trpc/router";
import { fetchRequestHandler } from '@trpc/server/adapters/fetch' import { createContext } from "./trpc/context";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
const app = new Hono() const app = new Hono();
app.use( app.use(
'*', "*",
cors({ cors({
origin: (origin) => { origin: (origin) => {
// Allow local dev UIs. if (origin === "http://localhost:3000") return origin;
if (origin === 'http://localhost:3000') return origin if (origin === "http://localhost:3001") return origin;
if (origin === 'http://localhost:3001') return origin if (!origin) return "*";
// Non-browser clients (no Origin header) return null;
if (!origin) return '*'
return null
}, },
allowHeaders: ['content-type'], allowHeaders: ["content-type"],
allowMethods: ['GET', 'POST', 'OPTIONS'], 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) => app.all("/trpc/*", async (c) => {
fetchRequestHandler({ const resHeaders = new Headers();
endpoint: '/trpc', const response = await fetchRequestHandler({
endpoint: "/trpc",
req: c.req.raw, req: c.req.raw,
router: appRouter, 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({ Bun.serve({
port, port,
fetch: app.fetch, fetch: app.fetch,
}) });
console.log(`Backend listening on http://localhost:${port}`) console.log(`Backend listening on http://localhost:${port}`);

View file

@ -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<string> {
return new SignJWT({ ...payload })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime(EXPIRY)
.sign(JWT_SECRET);
}
export async function verifyJWT(token: string): Promise<JWTPayload | null> {
try {
const { payload } = await jwtVerify(token, JWT_SECRET);
return payload as unknown as JWTPayload;
} catch {
return null;
}
}
export { COOKIE_NAME };

View file

@ -5,12 +5,18 @@ import {
createDrugInfoRepo, createDrugInfoRepo,
createUnitsRepo, createUnitsRepo,
createStockBatchesRepo, createStockBatchesRepo,
createEnterpriseRepo,
createStaffRepo,
createEnterpriseStaffRepo,
type StorageSpacesRepo, type StorageSpacesRepo,
type DistributorsRepo, type DistributorsRepo,
type ProductsRepo, type ProductsRepo,
type DrugInfoRepo, type DrugInfoRepo,
type UnitsRepo, type UnitsRepo,
type StockBatchesRepo, type StockBatchesRepo,
type EnterpriseRepo,
type StaffRepo,
type EnterpriseStaffRepo,
} from "data-manager-sqlite"; } from "data-manager-sqlite";
export class DataManager { export class DataManager {
@ -20,6 +26,9 @@ export class DataManager {
readonly drugInfo: DrugInfoRepo; readonly drugInfo: DrugInfoRepo;
readonly units: UnitsRepo; readonly units: UnitsRepo;
readonly stockBatches: StockBatchesRepo; readonly stockBatches: StockBatchesRepo;
readonly enterprises: EnterpriseRepo;
readonly staff: StaffRepo;
readonly enterpriseStaff: EnterpriseStaffRepo;
constructor() { constructor() {
const { repo: storageSpacesRepo } = createStorageSpacesRepo(); const { repo: storageSpacesRepo } = createStorageSpacesRepo();
@ -28,6 +37,9 @@ export class DataManager {
const { repo: drugInfoRepo } = createDrugInfoRepo(); const { repo: drugInfoRepo } = createDrugInfoRepo();
const { repo: unitsRepo } = createUnitsRepo(); const { repo: unitsRepo } = createUnitsRepo();
const { repo: stockBatchesRepo } = createStockBatchesRepo(); const { repo: stockBatchesRepo } = createStockBatchesRepo();
const { repo: enterpriseRepo } = createEnterpriseRepo();
const { repo: staffRepo } = createStaffRepo();
const { repo: enterpriseStaffRepo } = createEnterpriseStaffRepo();
this.storageSpaces = storageSpacesRepo; this.storageSpaces = storageSpacesRepo;
this.distributors = distributorsRepo; this.distributors = distributorsRepo;
@ -35,5 +47,8 @@ export class DataManager {
this.drugInfo = drugInfoRepo; this.drugInfo = drugInfoRepo;
this.units = unitsRepo; this.units = unitsRepo;
this.stockBatches = stockBatchesRepo; this.stockBatches = stockBatchesRepo;
this.enterprises = enterpriseRepo;
this.staff = staffRepo;
this.enterpriseStaff = enterpriseStaffRepo;
} }
} }

View file

@ -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<Context> {
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,
};
}

View file

@ -1,4 +1,16 @@
import { initTRPC } from "@trpc/server"; import { initTRPC, TRPCError } from "@trpc/server";
import type { Context } from "./context";
const t = initTRPC.context<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<Context["staff"]> } });
});
const t = initTRPC.create();
export { t }; export { t };

View file

@ -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,
};
}),
});

View file

@ -1,18 +1,20 @@
import { t } from "./init"; import { router } from "./init";
import { storageRouter } from "./pharmanager/v1/storage"; import { storageRouter } from "./pharmanager/v1/storage";
import { distributorRouter } from "./pharmanager/v1/distributor"; import { distributorRouter } from "./pharmanager/v1/distributor";
import { productRouter } from "./pharmanager/v1/product"; import { productRouter } from "./pharmanager/v1/product";
import { drugInfoRouter } from "./pharmanager/v1/drugInfo"; import { drugInfoRouter } from "./pharmanager/v1/drugInfo";
import { unitsRouter } from "./pharmanager/v1/units"; import { unitsRouter } from "./pharmanager/v1/units";
import { stockRouter } from "./pharmanager/v1/stock"; import { stockRouter } from "./pharmanager/v1/stock";
import { authRouter } from "./pharmanager/v1/auth";
export const appRouter = t.router({ export const appRouter = router({
storage: storageRouter, storage: storageRouter,
distributor: distributorRouter, distributor: distributorRouter,
product: productRouter, product: productRouter,
drugInfo: drugInfoRouter, drugInfo: drugInfoRouter,
units: unitsRouter, units: unitsRouter,
stock: stockRouter, stock: stockRouter,
auth: authRouter,
}); });
export type AppRouter = typeof appRouter; export type AppRouter = typeof appRouter;

View file

@ -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`);

View file

@ -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": {}
}
}

View file

@ -36,6 +36,13 @@
"when": 1779533139096, "when": 1779533139096,
"tag": "0004_ambiguous_captain_america", "tag": "0004_ambiguous_captain_america",
"breakpoints": true "breakpoints": true
},
{
"idx": 5,
"version": "6",
"when": 1779546674621,
"tag": "0005_yielding_silver_fox",
"breakpoints": true
} }
] ]
} }

View file

@ -11,12 +11,14 @@
"typecheck": "tsc -p tsconfig.json --noEmit" "typecheck": "tsc -p tsconfig.json --noEmit"
}, },
"dependencies": { "dependencies": {
"bcryptjs": "^3.0.3",
"drizzle-orm": "^0.44.5" "drizzle-orm": "^0.44.5"
}, },
"devDependencies": { "devDependencies": {
"drizzle-kit": "^0.31.4", "@types/bcryptjs": "^3.0.0",
"@types/node": "^22.10.2",
"@types/bun": "^1.3.14", "@types/bun": "^1.3.14",
"@types/node": "^22.10.2",
"drizzle-kit": "^0.31.4",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }
} }

View file

@ -1,13 +1,56 @@
import bcrypt from 'bcryptjs'
import { createDb } from './db' import { createDb } from './db'
import { runMigrations } from './migrate' import { runMigrations } from './migrate'
const { db, sqlite } = createDb() const { db, sqlite } = createDb()
runMigrations(sqlite) 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 ('mg')")
sqlite.run("INSERT OR IGNORE INTO units (name) VALUES ('gm')") 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 ('ml')")
sqlite.run("INSERT OR IGNORE INTO units (name) VALUES ('piece')") 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 } export { db, sqlite }

View file

@ -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 }
}

View file

@ -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<Enterprise | null>
createEnterprise: (input: { name: string; type: string; owner_name: string; mobile: string; address?: string | null }) => Promise<Enterprise>
}
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 }
}

View file

@ -40,3 +40,25 @@ export {
type StockBatchesRepo, type StockBatchesRepo,
} from './stockBatches' } from './stockBatches'
export { stockBatches } from './schema/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'

View file

@ -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', { export const distributors = sqliteTable('distributors', {
id: integer('id').primaryKey({ autoIncrement: true }), id: integer('id').primaryKey({ autoIncrement: true }),
@ -6,4 +7,7 @@ export const distributors = sqliteTable('distributors', {
contact: text('contact').notNull(), contact: text('contact').notNull(),
mobile: text('mobile').notNull(), mobile: text('mobile').notNull(),
address: text('address'), address: text('address'),
}) enterpriseId: integer('enterprise_id').references(() => enterprises.id),
}, (table) => ({
enterpriseIdx: index('distributors_enterprise_idx').on(table.enterpriseId),
}))

View file

@ -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),
})

View file

@ -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'),
})

View file

@ -5,3 +5,10 @@ export * from './drugInfo'
export * from './units' export * from './units'
export * from './productCompositions' export * from './productCompositions'
export * from './stockBatches' export * from './stockBatches'
export * from './enterprises'
export * from './staff'
export * from './roles'
export * from './permissions'
export * from './rolePermissions'
export * from './staffRoles'
export * from './enterpriseStaff'

View file

@ -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(),
})

View file

@ -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 { distributors } from './distributors'
import { units } from './units' import { units } from './units'
import { enterprises } from './enterprises'
export const products = sqliteTable('products', { export const products = sqliteTable('products', {
id: integer('id').primaryKey({ autoIncrement: true }), id: integer('id').primaryKey({ autoIncrement: true }),
@ -17,4 +18,7 @@ export const products = sqliteTable('products', {
unitsPerStrip: integer('units_per_strip'), unitsPerStrip: integer('units_per_strip'),
hideProductFromPublic: integer('hide_product_from_public', { mode: 'boolean' }).notNull().default(false), hideProductFromPublic: integer('hide_product_from_public', { mode: 'boolean' }).notNull().default(false),
hidePriceFromPublic: integer('hide_price_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),
}))

View file

@ -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),
})

View file

@ -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(),
})

View file

@ -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),
})

View file

@ -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),
})

View file

@ -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 { products } from './products'
import { storageSpaces } from './storageSpacesSchema' import { storageSpaces } from './storageSpacesSchema'
import { distributors } from './distributors' import { distributors } from './distributors'
import { enterprises } from './enterprises'
export const stockBatches = sqliteTable('stock_batches', { export const stockBatches = sqliteTable('stock_batches', {
id: integer('id').primaryKey({ autoIncrement: true }), id: integer('id').primaryKey({ autoIncrement: true }),
@ -14,4 +15,7 @@ export const stockBatches = sqliteTable('stock_batches', {
distributorId: integer('distributor_id').references(() => distributors.id), distributorId: integer('distributor_id').references(() => distributors.id),
quantity: integer('quantity').notNull().default(0), quantity: integer('quantity').notNull().default(0),
isDefault: integer('is_default', { mode: 'boolean' }).notNull().default(false), 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),
}))

View file

@ -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', { export const storageSpaces = sqliteTable('storage_spaces', {
id: integer('id').primaryKey({ autoIncrement: true }), id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(), name: text('name').notNull(),
description: text('description'), description: text('description'),
aliases: text('aliases').notNull().default('[]'), aliases: text('aliases').notNull().default('[]'),
// JSON array string of image URLs
imageUrls: text('image_urls').notNull().default('[]'), imageUrls: text('image_urls').notNull().default('[]'),
}) enterpriseId: integer('enterprise_id').references(() => enterprises.id),
}, (table) => ({
enterpriseIdx: index('storage_spaces_enterprise_idx').on(table.enterpriseId),
}))

View file

@ -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<Staff | null>
getStaffByUsername: (username: string) => Promise<Staff | null>
createStaff: (input: { name: string; username: string; email?: string | null; mobile?: string | null; added_on: string; password: string; is_password_reset_needed?: boolean }) => Promise<Staff>
verifyPassword: (staff: Staff, password: string) => Promise<boolean>
}
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 }
}