diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 13821b9..1c1883a 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -33,11 +33,14 @@ app.all("/trpc/*", async (c) => { createContext: () => createContext({ req: c.req.raw, resHeaders }), }); - resHeaders.forEach((value, key) => { - response.headers.set(key, value); - }); + const merged = new Headers(response.headers); + resHeaders.forEach((value, key) => merged.set(key, value)); - return response; + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: merged, + }); }); export { app }; diff --git a/apps/backend/src/trpc/pharmanager/v1/distributor.ts b/apps/backend/src/trpc/pharmanager/v1/distributor.ts index 1d8b85c..0f0dfc7 100644 --- a/apps/backend/src/trpc/pharmanager/v1/distributor.ts +++ b/apps/backend/src/trpc/pharmanager/v1/distributor.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { t } from "../../init"; +import { protectedProcedure, router } from "../../init"; import { dataManager } from "../../../lib/data-manager-instance"; export const DistributorSchema = z.object({ @@ -25,39 +25,54 @@ export const UpdateDistributorInput = z export type Distributor = z.infer; -export const distributorRouter = t.router({ - list: t.procedure +export const distributorRouter = router({ + list: protectedProcedure .output(z.array(DistributorSchema)) - .query(() => dataManager.distributors.getDistributors()), - - byId: t.procedure - .input(z.object({ id: z.number().int() })) - .output(DistributorSchema.nullable()) - .query(({ input }) => - dataManager.distributors.getDistributorById(input.id), + .query(({ ctx }) => + dataManager.distributors.getDistributors(ctx.staff.enterpriseId), ), - create: t.procedure + byId: protectedProcedure + .input(z.object({ id: z.number().int() })) + .output(DistributorSchema.nullable()) + .query(({ ctx, input }) => + dataManager.distributors.getDistributorById( + input.id, + ctx.staff.enterpriseId, + ), + ), + + create: protectedProcedure .input(CreateDistributorInput) .output(DistributorSchema) - .mutation(({ input }) => - dataManager.distributors.createDistributor(input), + .mutation(({ ctx, input }) => + dataManager.distributors.createDistributor( + input, + ctx.staff.enterpriseId, + ), ), - update: t.procedure + update: protectedProcedure .input(UpdateDistributorInput) .output(DistributorSchema.nullable()) - .mutation(({ input }) => { + .mutation(({ ctx, input }) => { const { id, ...patch } = input; - return dataManager.distributors.updateDistributor(id, patch); + return dataManager.distributors.updateDistributor( + id, + patch, + ctx.staff.enterpriseId, + ); }), - remove: t.procedure + remove: protectedProcedure .input(z.object({ id: z.number().int() })) .output(z.object({ ok: z.boolean() })) - .mutation(async ({ input }) => { + .mutation(async ({ ctx, input }) => { const ok = - await dataManager.distributors.deleteDistributor(input.id); + await dataManager.distributors.deleteDistributor( + input.id, + ctx.staff.enterpriseId, + ); return { ok }; }), }); diff --git a/apps/backend/src/trpc/pharmanager/v1/product.ts b/apps/backend/src/trpc/pharmanager/v1/product.ts index 653bb1b..5089a45 100644 --- a/apps/backend/src/trpc/pharmanager/v1/product.ts +++ b/apps/backend/src/trpc/pharmanager/v1/product.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { t } from "../../init"; +import { protectedProcedure, router } from "../../init"; import { dataManager } from "../../../lib/data-manager-instance"; import { ProductSchema, @@ -7,34 +7,53 @@ import { UpdateProductInput, } from "@repo/shared"; -export const productRouter = t.router({ - list: t.procedure +export const productRouter = router({ + list: protectedProcedure .output(z.array(ProductSchema)) - .query(() => dataManager.products.getProducts()), + .query(({ ctx }) => + dataManager.products.getProducts(ctx.staff.enterpriseId), + ), - byId: t.procedure + byId: protectedProcedure .input(z.object({ id: z.number().int() })) .output(ProductSchema.nullable()) - .query(({ input }) => dataManager.products.getProductById(input.id)), + .query(({ ctx, input }) => + dataManager.products.getProductById( + input.id, + ctx.staff.enterpriseId, + ), + ), - create: t.procedure + create: protectedProcedure .input(CreateProductInput) .output(ProductSchema) - .mutation(({ input }) => dataManager.products.createProduct(input)), + .mutation(({ ctx, input }) => + dataManager.products.createProduct( + input, + ctx.staff.enterpriseId, + ), + ), - update: t.procedure + update: protectedProcedure .input(UpdateProductInput) .output(ProductSchema.nullable()) - .mutation(({ input }) => { + .mutation(({ ctx, input }) => { const { id, ...patch } = input; - return dataManager.products.updateProduct(id, patch); + return dataManager.products.updateProduct( + id, + patch, + ctx.staff.enterpriseId, + ); }), - remove: t.procedure + remove: protectedProcedure .input(z.object({ id: z.number().int() })) .output(z.object({ ok: z.boolean() })) - .mutation(async ({ input }) => { - const ok = await dataManager.products.deleteProduct(input.id); + .mutation(async ({ ctx, input }) => { + const ok = await dataManager.products.deleteProduct( + input.id, + ctx.staff.enterpriseId, + ); return { ok }; }), }); diff --git a/apps/backend/src/trpc/pharmanager/v1/stock.ts b/apps/backend/src/trpc/pharmanager/v1/stock.ts index ac672bf..fe8c4fd 100644 --- a/apps/backend/src/trpc/pharmanager/v1/stock.ts +++ b/apps/backend/src/trpc/pharmanager/v1/stock.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { t } from "../../init"; +import { protectedProcedure, router } from "../../init"; import { dataManager } from "../../../lib/data-manager-instance"; export const StockBatchSchema = z.object({ @@ -11,6 +11,7 @@ export const StockBatchSchema = z.object({ expiry: z.string(), rack: z.object({ id: z.number().int(), name: z.string() }).nullable(), distributor: z.object({ id: z.number().int(), agency: z.string() }).nullable(), + quantity: z.number().int(), is_default: z.boolean(), }); @@ -24,6 +25,7 @@ export const CreateStockBatchInput = z.object({ expiry: shape.expiry.min(1), rack_id: z.number().int().nullable().optional(), distributor_id: z.number().int().nullable().optional(), + quantity: z.number().int().min(1), is_default: shape.is_default.default(false), }); @@ -33,34 +35,53 @@ export const UpdateStockBatchInput = z export type StockBatch = z.infer; -export const stockRouter = t.router({ - list: t.procedure +export const stockRouter = router({ + list: protectedProcedure .output(z.array(StockBatchSchema)) - .query(() => dataManager.stockBatches.getStockBatches()), + .query(({ ctx }) => + dataManager.stockBatches.getStockBatches(ctx.staff.enterpriseId), + ), - byId: t.procedure + byId: protectedProcedure .input(z.object({ id: z.number().int() })) .output(StockBatchSchema.nullable()) - .query(({ input }) => dataManager.stockBatches.getStockBatchById(input.id)), + .query(({ ctx, input }) => + dataManager.stockBatches.getStockBatchById( + input.id, + ctx.staff.enterpriseId, + ), + ), - create: t.procedure + create: protectedProcedure .input(CreateStockBatchInput) .output(StockBatchSchema) - .mutation(({ input }) => dataManager.stockBatches.createStockBatch(input)), + .mutation(({ ctx, input }) => + dataManager.stockBatches.createStockBatch( + input, + ctx.staff.enterpriseId, + ), + ), - update: t.procedure + update: protectedProcedure .input(UpdateStockBatchInput) .output(StockBatchSchema.nullable()) - .mutation(({ input }) => { + .mutation(({ ctx, input }) => { const { id, ...patch } = input; - return dataManager.stockBatches.updateStockBatch(id, patch); + return dataManager.stockBatches.updateStockBatch( + id, + patch, + ctx.staff.enterpriseId, + ); }), - remove: t.procedure + remove: protectedProcedure .input(z.object({ id: z.number().int() })) .output(z.object({ ok: z.boolean() })) - .mutation(async ({ input }) => { - const ok = await dataManager.stockBatches.deleteStockBatch(input.id); + .mutation(async ({ ctx, input }) => { + const ok = await dataManager.stockBatches.deleteStockBatch( + input.id, + ctx.staff.enterpriseId, + ); return { ok }; }), }); diff --git a/apps/backend/src/trpc/pharmanager/v1/storage.ts b/apps/backend/src/trpc/pharmanager/v1/storage.ts index 0f96292..90f4189 100644 --- a/apps/backend/src/trpc/pharmanager/v1/storage.ts +++ b/apps/backend/src/trpc/pharmanager/v1/storage.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import {t} from '../../init' +import { protectedProcedure, router } from "../../init"; import { dataManager } from "../../../lib/data-manager-instance"; export const StorageSpaceSchema = z.object({ @@ -25,39 +25,54 @@ export const UpdateStorageInput = z export type StorageSpace = z.infer; -export const storageRouter = t.router({ - list: t.procedure +export const storageRouter = router({ + list: protectedProcedure .output(z.array(StorageSpaceSchema)) - .query(() => dataManager.storageSpaces.getStorageSpaces()), - - byId: t.procedure - .input(z.object({ id: z.number().int() })) - .output(StorageSpaceSchema.nullable()) - .query(({ input }) => - dataManager.storageSpaces.getStorageSpaceById(input.id), + .query(({ ctx }) => + dataManager.storageSpaces.getStorageSpaces(ctx.staff.enterpriseId), ), - create: t.procedure + byId: protectedProcedure + .input(z.object({ id: z.number().int() })) + .output(StorageSpaceSchema.nullable()) + .query(({ ctx, input }) => + dataManager.storageSpaces.getStorageSpaceById( + input.id, + ctx.staff.enterpriseId, + ), + ), + + create: protectedProcedure .input(CreateStorageInput) .output(StorageSpaceSchema) - .mutation(({ input }) => - dataManager.storageSpaces.createStorageSpace(input), + .mutation(({ ctx, input }) => + dataManager.storageSpaces.createStorageSpace( + input, + ctx.staff.enterpriseId, + ), ), - update: t.procedure + update: protectedProcedure .input(UpdateStorageInput) .output(StorageSpaceSchema.nullable()) - .mutation(({ input }) => { + .mutation(({ ctx, input }) => { const { id, ...patch } = input; - return dataManager.storageSpaces.updateStorageSpace(id, patch); + return dataManager.storageSpaces.updateStorageSpace( + id, + patch, + ctx.staff.enterpriseId, + ); }), - remove: t.procedure + remove: protectedProcedure .input(z.object({ id: z.number().int() })) .output(z.object({ ok: z.boolean() })) - .mutation(async ({ input }) => { + .mutation(async ({ ctx, input }) => { const ok = - await dataManager.storageSpaces.deleteStorageSpace(input.id); + await dataManager.storageSpaces.deleteStorageSpace( + input.id, + ctx.staff.enterpriseId, + ); return { ok }; }), }); diff --git a/apps/pharmanager/src/routeTree.gen.ts b/apps/pharmanager/src/routeTree.gen.ts index 21366c3..36933f7 100644 --- a/apps/pharmanager/src/routeTree.gen.ts +++ b/apps/pharmanager/src/routeTree.gen.ts @@ -14,6 +14,7 @@ import { Route as StockRouteImport } from './routes/stock' import { Route as StaffRouteImport } from './routes/staff' import { Route as ProfileRouteImport } from './routes/profile' import { Route as ProductsRouteImport } from './routes/products' +import { Route as LoginRouteImport } from './routes/login' import { Route as DistributorsRouteImport } from './routes/distributors' import { Route as CustomersRouteImport } from './routes/customers' import { Route as BillingRouteImport } from './routes/billing' @@ -56,6 +57,11 @@ const ProductsRoute = ProductsRouteImport.update({ path: '/products', getParentRoute: () => rootRouteImport, } as any) +const LoginRoute = LoginRouteImport.update({ + id: '/login', + path: '/login', + getParentRoute: () => rootRouteImport, +} as any) const DistributorsRoute = DistributorsRouteImport.update({ id: '/distributors', path: '/distributors', @@ -142,6 +148,7 @@ export interface FileRoutesByFullPath { '/billing': typeof BillingRoute '/customers': typeof CustomersRoute '/distributors': typeof DistributorsRouteWithChildren + '/login': typeof LoginRoute '/products': typeof ProductsRouteWithChildren '/profile': typeof ProfileRoute '/staff': typeof StaffRoute @@ -164,6 +171,7 @@ export interface FileRoutesByTo { '/': typeof IndexRoute '/billing': typeof BillingRoute '/customers': typeof CustomersRoute + '/login': typeof LoginRoute '/profile': typeof ProfileRoute '/staff': typeof StaffRoute '/distributors/$id': typeof DistributorsIdRoute @@ -185,6 +193,7 @@ export interface FileRoutesById { '/billing': typeof BillingRoute '/customers': typeof CustomersRoute '/distributors': typeof DistributorsRouteWithChildren + '/login': typeof LoginRoute '/products': typeof ProductsRouteWithChildren '/profile': typeof ProfileRoute '/staff': typeof StaffRoute @@ -210,6 +219,7 @@ export interface FileRouteTypes { | '/billing' | '/customers' | '/distributors' + | '/login' | '/products' | '/profile' | '/staff' @@ -232,6 +242,7 @@ export interface FileRouteTypes { | '/' | '/billing' | '/customers' + | '/login' | '/profile' | '/staff' | '/distributors/$id' @@ -252,6 +263,7 @@ export interface FileRouteTypes { | '/billing' | '/customers' | '/distributors' + | '/login' | '/products' | '/profile' | '/staff' @@ -276,6 +288,7 @@ export interface RootRouteChildren { BillingRoute: typeof BillingRoute CustomersRoute: typeof CustomersRoute DistributorsRoute: typeof DistributorsRouteWithChildren + LoginRoute: typeof LoginRoute ProductsRoute: typeof ProductsRouteWithChildren ProfileRoute: typeof ProfileRoute StaffRoute: typeof StaffRoute @@ -320,6 +333,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ProductsRouteImport parentRoute: typeof rootRouteImport } + '/login': { + id: '/login' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof LoginRouteImport + parentRoute: typeof rootRouteImport + } '/distributors': { id: '/distributors' path: '/distributors' @@ -501,6 +521,7 @@ const rootRouteChildren: RootRouteChildren = { BillingRoute: BillingRoute, CustomersRoute: CustomersRoute, DistributorsRoute: DistributorsRouteWithChildren, + LoginRoute: LoginRoute, ProductsRoute: ProductsRouteWithChildren, ProfileRoute: ProfileRoute, StaffRoute: StaffRoute, diff --git a/apps/pharmanager/src/routes/__root.tsx b/apps/pharmanager/src/routes/__root.tsx index 6d2abf6..6c81a20 100644 --- a/apps/pharmanager/src/routes/__root.tsx +++ b/apps/pharmanager/src/routes/__root.tsx @@ -1,7 +1,14 @@ -import { Outlet, createRootRoute } from "@tanstack/react-router"; +import { useEffect } from "react"; +import { + Outlet, + createRootRoute, + useNavigate, + useRouterState, +} from "@tanstack/react-router"; import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"; import { TanStackDevtools } from "@tanstack/react-devtools"; import { AppLayout } from "#/components/AppLayout"; +import { AuthGate, useAuthStore } from "shared-react"; import "../styles.css"; @@ -9,12 +16,46 @@ export const Route = createRootRoute({ component: RootComponent, }); +function AuthGuard() { + const navigate = useNavigate(); + const pathname = useRouterState({ select: (s) => s.location.pathname }); + const isLoading = useAuthStore((s) => s.isLoading); + const staff = useAuthStore((s) => s.staff); + const isLoginPage = pathname === "/login"; + + useEffect(() => { + if (!isLoading && !staff && !isLoginPage) { + navigate({ to: "/login" }); + } + }, [isLoading, staff, isLoginPage, navigate]); + + if (isLoading) { + return ( + +
+

Loading...

+
+
+ ); + } + + if (!staff && !isLoginPage) return null; + + if (isLoginPage) return ; + + return ( + + + + ); +} + function RootComponent() { return ( <> - - - + + + ; + +export const Route = createFileRoute("/login")({ + component: LoginPage, + staticData: { + title: "Login", + subtitle: "Sign in to your account", + }, +}); + +function LoginPage() { + const navigate = useNavigate(); + const loginMutation = useLogin(); + const setUser = useAuthStore((s) => s.setUser); + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(formSchema), + }); + + function onSubmit(values: FormValues) { + loginMutation.mutate(values, { + onSuccess: (data) => { + setUser(data.staff, { + id: data.enterprise_id, + name: "", + type: "", + }); + navigate({ to: "/" }); + }, + }); + } + + return ( +
+
+
+

+ Sign In +

+

+ Enter your credentials to continue +

+
+ +
+
+ + + {errors.username && ( +

+ {errors.username.message} +

+ )} +
+ +
+ + + {errors.password && ( +

+ {errors.password.message} +

+ )} +
+
+ + {loginMutation.error && ( +

+ Invalid username or password +

+ )} + + +
+
+ ); +} diff --git a/packages/data-manager-sqlite/src/db-instance.ts b/packages/data-manager-sqlite/src/db-instance.ts index 62e219d..7168dae 100644 --- a/packages/data-manager-sqlite/src/db-instance.ts +++ b/packages/data-manager-sqlite/src/db-instance.ts @@ -15,8 +15,8 @@ sqlite.run("INSERT OR IGNORE INTO units (name) VALUES ('piece')") 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) +const today = new Date().toISOString().slice(0, 10) +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], diff --git a/packages/data-manager-sqlite/src/distributors.ts b/packages/data-manager-sqlite/src/distributors.ts index 2bc5acd..910a8cd 100644 --- a/packages/data-manager-sqlite/src/distributors.ts +++ b/packages/data-manager-sqlite/src/distributors.ts @@ -1,4 +1,4 @@ -import { eq } from 'drizzle-orm' +import { eq, and } from 'drizzle-orm' import { db } from './db-instance' import { distributors } from './schema/distributors' @@ -21,11 +21,11 @@ export type CreateDistributorInput = { export type UpdateDistributorPatch = Partial export type DistributorsRepo = { - getDistributors: () => Promise - getDistributorById: (id: number) => Promise - createDistributor: (input: CreateDistributorInput) => Promise - updateDistributor: (id: number, patch: UpdateDistributorPatch) => Promise - deleteDistributor: (id: number) => Promise + getDistributors: (enterpriseId: number) => Promise + getDistributorById: (id: number, enterpriseId: number) => Promise + createDistributor: (input: CreateDistributorInput, enterpriseId: number) => Promise + updateDistributor: (id: number, patch: UpdateDistributorPatch, enterpriseId: number) => Promise + deleteDistributor: (id: number, enterpriseId: number) => Promise } function toDistributor(row: { @@ -48,21 +48,25 @@ export function createDistributorsRepo(): { repo: DistributorsRepo } { const repo: DistributorsRepo = { - getDistributors() { - const rows = db.select().from(distributors).all() + getDistributors(enterpriseId) { + const rows = db + .select() + .from(distributors) + .where(eq(distributors.enterpriseId, enterpriseId)) + .all() return Promise.resolve(rows.map(toDistributor)) }, - getDistributorById(id) { + getDistributorById(id, enterpriseId) { const row = db .select() .from(distributors) - .where(eq(distributors.id, id)) + .where(and(eq(distributors.id, id), eq(distributors.enterpriseId, enterpriseId))) .get() return Promise.resolve(row ? toDistributor(row) : null) }, - createDistributor(input) { + createDistributor(input, enterpriseId) { const created = db .insert(distributors) .values({ @@ -70,13 +74,14 @@ export function createDistributorsRepo(): { contact: input.contact, mobile: input.mobile, address: input.address ?? null, + enterpriseId, }) .returning() .get() return Promise.resolve(toDistributor(created)) }, - updateDistributor(id, patch) { + updateDistributor(id, patch, enterpriseId) { const updated = db .update(distributors) .set({ @@ -85,16 +90,16 @@ export function createDistributorsRepo(): { ...(patch.mobile !== undefined ? { mobile: patch.mobile } : {}), ...(patch.address !== undefined ? { address: patch.address } : {}), }) - .where(eq(distributors.id, id)) + .where(and(eq(distributors.id, id), eq(distributors.enterpriseId, enterpriseId))) .returning() .get() return Promise.resolve(updated ? toDistributor(updated) : null) }, - deleteDistributor(id) { + deleteDistributor(id, enterpriseId) { const deleted = db .delete(distributors) - .where(eq(distributors.id, id)) + .where(and(eq(distributors.id, id), eq(distributors.enterpriseId, enterpriseId))) .returning({ id: distributors.id }) .get() return Promise.resolve(Boolean(deleted)) diff --git a/packages/data-manager-sqlite/src/products.ts b/packages/data-manager-sqlite/src/products.ts index ade3168..8be4f1f 100644 --- a/packages/data-manager-sqlite/src/products.ts +++ b/packages/data-manager-sqlite/src/products.ts @@ -1,5 +1,4 @@ -import { eq } from 'drizzle-orm' -import type { Database } from 'bun:sqlite' +import { eq, and } from 'drizzle-orm' import { db, sqlite } from './db-instance' import { products } from './schema/products' @@ -56,11 +55,11 @@ export type CreateProductInput = export type UpdateProductPatch = Partial export type ProductsRepo = { - getProducts: () => Promise - getProductById: (id: number) => Promise - createProduct: (input: CreateProductInput) => Promise - updateProduct: (id: number, patch: UpdateProductPatch) => Promise - deleteProduct: (id: number) => Promise + getProducts: (enterpriseId: number) => Promise + getProductById: (id: number, enterpriseId: number) => Promise + createProduct: (input: CreateProductInput, enterpriseId: number) => Promise + updateProduct: (id: number, patch: UpdateProductPatch, enterpriseId: number) => Promise + deleteProduct: (id: number, enterpriseId: number) => Promise } function getOrCreateDrug(name: string): number { @@ -151,8 +150,8 @@ function setCompositions(productId: number, comps: CompositionInput[]) { export function createProductsRepo(): { repo: ProductsRepo } { const repo: ProductsRepo = { - getProducts() { - const rows = db.select().from(products).all() + getProducts(enterpriseId) { + const rows = db.select().from(products).where(eq(products.enterpriseId, enterpriseId)).all() return Promise.resolve( rows.map((r) => { const distributor = fetchDistributor(r.distributorId) @@ -165,8 +164,8 @@ export function createProductsRepo(): { repo: ProductsRepo } { ) }, - getProductById(id) { - const row = db.select().from(products).where(eq(products.id, id)).get() + getProductById(id, enterpriseId) { + const row = db.select().from(products).where(and(eq(products.id, id), eq(products.enterpriseId, enterpriseId))).get() if (!row) return Promise.resolve(null) const distributor = fetchDistributor(row.distributorId) const unit = fetchUnit(row.unitId) @@ -176,7 +175,7 @@ export function createProductsRepo(): { repo: ProductsRepo } { }) }, - createProduct(input) { + createProduct(input, enterpriseId) { const result = sqlite.transaction(() => { const unitId = getOrCreateUnit(input.unit_name) @@ -196,6 +195,7 @@ export function createProductsRepo(): { repo: ProductsRepo } { unitsPerStrip: input.units_per_strip ?? null, hideProductFromPublic: input.hide_product_from_public ?? false, hidePriceFromPublic: input.hide_price_from_public ?? false, + enterpriseId, }) .returning() .get() @@ -214,8 +214,8 @@ export function createProductsRepo(): { repo: ProductsRepo } { }) }, - updateProduct(id, patch) { - const existing = db.select().from(products).where(eq(products.id, id)).get() + updateProduct(id, patch, enterpriseId) { + const existing = db.select().from(products).where(and(eq(products.id, id), eq(products.enterpriseId, enterpriseId))).get() if (!existing) return Promise.resolve(null) sqlite.transaction(() => { @@ -236,7 +236,7 @@ export function createProductsRepo(): { repo: ProductsRepo } { if (patch.hide_price_from_public !== undefined) setData.hidePriceFromPublic = patch.hide_price_from_public if (Object.keys(setData).length > 0) { - db.update(products).set(setData).where(eq(products.id, id)).run() + db.update(products).set(setData).where(and(eq(products.id, id), eq(products.enterpriseId, enterpriseId))).run() } if (patch.compositions) { @@ -244,7 +244,7 @@ export function createProductsRepo(): { repo: ProductsRepo } { } })() - const updated = db.select().from(products).where(eq(products.id, id)).get()! + const updated = db.select().from(products).where(and(eq(products.id, id), eq(products.enterpriseId, enterpriseId))).get()! const distributor = fetchDistributor(updated.distributorId) const unit = fetchUnit(updated.unitId) @@ -254,9 +254,9 @@ export function createProductsRepo(): { repo: ProductsRepo } { }) }, - deleteProduct(id) { + deleteProduct(id, enterpriseId) { db.delete(productCompositions).where(eq(productCompositions.productId, id)).run() - const deleted = db.delete(products).where(eq(products.id, id)).returning({ id: products.id }).get() + const deleted = db.delete(products).where(and(eq(products.id, id), eq(products.enterpriseId, enterpriseId))).returning({ id: products.id }).get() return Promise.resolve(Boolean(deleted)) }, } diff --git a/packages/data-manager-sqlite/src/stockBatches.ts b/packages/data-manager-sqlite/src/stockBatches.ts index f04c883..d08855c 100644 --- a/packages/data-manager-sqlite/src/stockBatches.ts +++ b/packages/data-manager-sqlite/src/stockBatches.ts @@ -1,4 +1,4 @@ -import { eq } from 'drizzle-orm' +import { eq, and } from 'drizzle-orm' import { db, sqlite } from './db-instance' import { stockBatches } from './schema/stockBatches' @@ -34,11 +34,11 @@ export type CreateStockBatchInput = { export type UpdateStockBatchPatch = Partial export type StockBatchesRepo = { - getStockBatches: () => Promise - getStockBatchById: (id: number) => Promise - createStockBatch: (input: CreateStockBatchInput) => Promise - updateStockBatch: (id: number, patch: UpdateStockBatchPatch) => Promise - deleteStockBatch: (id: number) => Promise + getStockBatches: (enterpriseId: number) => Promise + getStockBatchById: (id: number, enterpriseId: number) => Promise + createStockBatch: (input: CreateStockBatchInput, enterpriseId: number) => Promise + updateStockBatch: (id: number, patch: UpdateStockBatchPatch, enterpriseId: number) => Promise + deleteStockBatch: (id: number, enterpriseId: number) => Promise } function fetchProduct(productId: number): { id: number; name: string; brand: string } | null { @@ -78,22 +78,22 @@ function toStockBatch(row: typeof stockBatches.$inferSelect): StockBatch { export function createStockBatchesRepo(): { repo: StockBatchesRepo } { const repo: StockBatchesRepo = { - getStockBatches() { - const rows = db.select().from(stockBatches).all() + getStockBatches(enterpriseId) { + const rows = db.select().from(stockBatches).where(eq(stockBatches.enterpriseId, enterpriseId)).all() return Promise.resolve(rows.map(toStockBatch)) }, - getStockBatchById(id) { - const row = db.select().from(stockBatches).where(eq(stockBatches.id, id)).get() + getStockBatchById(id, enterpriseId) { + const row = db.select().from(stockBatches).where(and(eq(stockBatches.id, id), eq(stockBatches.enterpriseId, enterpriseId))).get() return Promise.resolve(row ? toStockBatch(row) : null) }, - createStockBatch(input) { + createStockBatch(input, enterpriseId) { const result = sqlite.transaction(() => { if (input.is_default) { db.update(stockBatches) .set({ isDefault: false }) - .where(eq(stockBatches.productId, input.product_id)) + .where(and(eq(stockBatches.productId, input.product_id), eq(stockBatches.enterpriseId, enterpriseId))) .run() } @@ -109,6 +109,7 @@ export function createStockBatchesRepo(): { repo: StockBatchesRepo } { distributorId: input.distributor_id ?? null, quantity: input.quantity ?? 0, isDefault: input.is_default ?? false, + enterpriseId, }) .returning() .get() @@ -119,15 +120,15 @@ export function createStockBatchesRepo(): { repo: StockBatchesRepo } { return Promise.resolve(toStockBatch(result)) }, - updateStockBatch(id, patch) { - const existing = db.select().from(stockBatches).where(eq(stockBatches.id, id)).get() + updateStockBatch(id, patch, enterpriseId) { + const existing = db.select().from(stockBatches).where(and(eq(stockBatches.id, id), eq(stockBatches.enterpriseId, enterpriseId))).get() if (!existing) return Promise.resolve(null) sqlite.transaction(() => { if (patch.is_default) { db.update(stockBatches) .set({ isDefault: false }) - .where(eq(stockBatches.productId, patch.product_id ?? existing.productId)) + .where(and(eq(stockBatches.productId, patch.product_id ?? existing.productId), eq(stockBatches.enterpriseId, enterpriseId))) .run() } @@ -143,16 +144,16 @@ export function createStockBatchesRepo(): { repo: StockBatchesRepo } { if (patch.is_default !== undefined) setData.isDefault = patch.is_default if (Object.keys(setData).length > 0) { - db.update(stockBatches).set(setData).where(eq(stockBatches.id, id)).run() + db.update(stockBatches).set(setData).where(and(eq(stockBatches.id, id), eq(stockBatches.enterpriseId, enterpriseId))).run() } })() - const updated = db.select().from(stockBatches).where(eq(stockBatches.id, id)).get()! + const updated = db.select().from(stockBatches).where(and(eq(stockBatches.id, id), eq(stockBatches.enterpriseId, enterpriseId))).get()! return Promise.resolve(toStockBatch(updated)) }, - deleteStockBatch(id) { - const deleted = db.delete(stockBatches).where(eq(stockBatches.id, id)).returning({ id: stockBatches.id }).get() + deleteStockBatch(id, enterpriseId) { + const deleted = db.delete(stockBatches).where(and(eq(stockBatches.id, id), eq(stockBatches.enterpriseId, enterpriseId))).returning({ id: stockBatches.id }).get() return Promise.resolve(Boolean(deleted)) }, } diff --git a/packages/data-manager-sqlite/src/storageSpaces.ts b/packages/data-manager-sqlite/src/storageSpaces.ts index 58b872b..0b098eb 100644 --- a/packages/data-manager-sqlite/src/storageSpaces.ts +++ b/packages/data-manager-sqlite/src/storageSpaces.ts @@ -1,4 +1,4 @@ -import { eq } from 'drizzle-orm' +import { eq, and } from 'drizzle-orm' import { db } from './db-instance' import { storageSpaces } from './schema/storageSpacesSchema' @@ -22,11 +22,11 @@ export type CreateStorageSpaceInput = { export type UpdateStorageSpacePatch = Partial export type StorageSpacesRepo = { - getStorageSpaces: () => Promise - getStorageSpaceById: (id: number) => Promise - createStorageSpace: (input: CreateStorageSpaceInput) => Promise - updateStorageSpace: (id: number, patch: UpdateStorageSpacePatch) => Promise - deleteStorageSpace: (id: number) => Promise + getStorageSpaces: (enterpriseId: number) => Promise + getStorageSpaceById: (id: number, enterpriseId: number) => Promise + createStorageSpace: (input: CreateStorageSpaceInput, enterpriseId: number) => Promise + updateStorageSpace: (id: number, patch: UpdateStorageSpacePatch, enterpriseId: number) => Promise + deleteStorageSpace: (id: number, enterpriseId: number) => Promise } function toStorageSpace(row: { @@ -50,21 +50,25 @@ export function createStorageSpacesRepo(): { } { const repo: StorageSpacesRepo = { - getStorageSpaces() { - const rows = db.select().from(storageSpaces).all() + getStorageSpaces(enterpriseId) { + const rows = db + .select() + .from(storageSpaces) + .where(eq(storageSpaces.enterpriseId, enterpriseId)) + .all() return Promise.resolve(rows.map(toStorageSpace)) }, - getStorageSpaceById(id) { + getStorageSpaceById(id, enterpriseId) { const row = db .select() .from(storageSpaces) - .where(eq(storageSpaces.id, id)) + .where(and(eq(storageSpaces.id, id), eq(storageSpaces.enterpriseId, enterpriseId))) .get() return Promise.resolve(row ? toStorageSpace(row) : null) }, - createStorageSpace(input) { + createStorageSpace(input, enterpriseId) { const created = db .insert(storageSpaces) .values({ @@ -72,13 +76,14 @@ export function createStorageSpacesRepo(): { description: input.description ?? null, aliases: serializeStringArrJson(input.aliases), imageUrls: serializeStringArrJson(input.image_urls), + enterpriseId, }) .returning() .get() return Promise.resolve(toStorageSpace(created)) }, - updateStorageSpace(id, patch) { + updateStorageSpace(id, patch, enterpriseId) { const updated = db .update(storageSpaces) .set({ @@ -91,16 +96,16 @@ export function createStorageSpacesRepo(): { ? { imageUrls: serializeStringArrJson(patch.image_urls) } : {}), }) - .where(eq(storageSpaces.id, id)) + .where(and(eq(storageSpaces.id, id), eq(storageSpaces.enterpriseId, enterpriseId))) .returning() .get() return Promise.resolve(updated ? toStorageSpace(updated) : null) }, - deleteStorageSpace(id) { + deleteStorageSpace(id, enterpriseId) { const deleted = db .delete(storageSpaces) - .where(eq(storageSpaces.id, id)) + .where(and(eq(storageSpaces.id, id), eq(storageSpaces.enterpriseId, enterpriseId))) .returning({ id: storageSpaces.id }) .get() return Promise.resolve(Boolean(deleted)) diff --git a/packages/shared-react/src/auth-gate.tsx b/packages/shared-react/src/auth-gate.tsx new file mode 100644 index 0000000..b6a6210 --- /dev/null +++ b/packages/shared-react/src/auth-gate.tsx @@ -0,0 +1,24 @@ +import { useEffect, type ReactNode } from "react"; +import { useWhoAmI, useAuthStore } from "./auth"; + +export function AuthGate({ children }: { children: ReactNode }) { + const { data, isLoading } = useWhoAmI(); + const setUser = useAuthStore((s) => s.setUser); + const setLoading = useAuthStore((s) => s.setLoading); + + useEffect(() => { + setLoading(isLoading); + }, [isLoading, setLoading]); + + useEffect(() => { + if (!isLoading) { + if (data) { + setUser(data.staff, data.enterprise); + } else { + setUser(null, null); + } + } + }, [data, isLoading, setUser]); + + return <>{children}; +} diff --git a/packages/shared-react/src/auth.ts b/packages/shared-react/src/auth.ts new file mode 100644 index 0000000..3aaebe4 --- /dev/null +++ b/packages/shared-react/src/auth.ts @@ -0,0 +1,48 @@ +import { create } from "zustand"; +import { trpc } from "./trpc"; + +export interface AuthStaff { + id: number; + name: string; + username: string; + isPasswordResetNeeded: boolean; +} + +export interface AuthEnterprise { + id: number; + name: string; + type: string; +} + +interface AuthState { + staff: AuthStaff | null; + enterprise: AuthEnterprise | null; + isLoading: boolean; + setUser: ( + staff: AuthStaff | null, + enterprise: AuthEnterprise | null, + ) => void; + setLoading: (loading: boolean) => void; + logout: () => void; +} + +export const useAuthStore = create((set) => ({ + staff: null, + enterprise: null, + isLoading: true, + setUser: (staff, enterprise) => + set({ staff, enterprise, isLoading: false }), + setLoading: (isLoading) => set({ isLoading }), + logout: () => set({ staff: null, enterprise: null, isLoading: false }), +})); + +export function useWhoAmI() { + return trpc.auth.whoAmI.useQuery(undefined, { + retry: false, + refetchOnWindowFocus: false, + }); +} + +export function useLogin() { + return trpc.auth.login.useMutation(); +} diff --git a/packages/shared-react/src/index.ts b/packages/shared-react/src/index.ts index a77176a..8ebfab9 100644 --- a/packages/shared-react/src/index.ts +++ b/packages/shared-react/src/index.ts @@ -8,3 +8,6 @@ export * from './hooks/drugInfo' export * from './hooks/units' export * from './hooks/stockBatches' export { trpc } from './trpc' +export { useAuthStore, useWhoAmI, useLogin } from './auth' +export type { AuthStaff, AuthEnterprise } from './auth' +export { AuthGate } from './auth-gate' diff --git a/packages/shared-react/src/trpc.ts b/packages/shared-react/src/trpc.ts index 3f1458c..6843c12 100644 --- a/packages/shared-react/src/trpc.ts +++ b/packages/shared-react/src/trpc.ts @@ -9,6 +9,8 @@ export function createTrpcClient(baseUrl: string) { links: [ httpBatchLink({ url: `${baseUrl}/trpc`, + fetch: (url, options) => + fetch(url, { ...options, credentials: "include" }), }), ], })