From 1e750e1ca5d5d516ec1f2cf0ce0ac72b07c0e2a3 Mon Sep 17 00:00:00 2001 From: kennyboy55 Date: Sun, 12 May 2024 18:44:33 +0200 Subject: [PATCH] Change structure and implement some admin functions --- app/auth/session.ts | 24 ++-- app/components/cards/drink.card.tsx | 18 +-- app/components/header.tsx | 11 +- app/entry.server.tsx | 6 + app/models/types.tsx | 16 +++ app/root.tsx | 41 +------ app/routes/_index.tsx | 107 +++++------------- app/routes/admin/edit/checkout.tsx | 59 ++++++++++ app/routes/admin/edit/inventory.tsx | 59 ++++++++++ app/routes/admin/layout.tsx | 51 +++++++++ app/routes/{ => admin}/new/beer.tsx | 2 +- app/routes/admin/route.tsx | 11 ++ app/routes/{ => inventory}/beer.$beerslug.tsx | 1 - app/routes/inventory/layout.tsx | 36 ++++++ app/routes/inventory/route.tsx | 81 +++++++++++++ app/routes/{ => inventory}/soda.$sodaslug.tsx | 0 app/routes/{ => inventory}/wine.$wineslug.tsx | 0 app/routes/login.tsx | 16 +-- app/routes/logout.tsx | 9 +- app/routes/scan/layout.tsx | 30 +++++ app/routes/{scan.tsx => scan/route.tsx} | 21 +--- app/utils/db.server.ts | 12 ++ package-lock.json | 36 +++--- package.json | 4 +- prisma/schema.prisma | 4 +- schema.zmodel | 2 +- vite.config.ts | 30 ++++- 27 files changed, 502 insertions(+), 185 deletions(-) create mode 100644 app/routes/admin/edit/checkout.tsx create mode 100644 app/routes/admin/edit/inventory.tsx create mode 100644 app/routes/admin/layout.tsx rename app/routes/{ => admin}/new/beer.tsx (98%) create mode 100644 app/routes/admin/route.tsx rename app/routes/{ => inventory}/beer.$beerslug.tsx (96%) create mode 100644 app/routes/inventory/layout.tsx create mode 100644 app/routes/inventory/route.tsx rename app/routes/{ => inventory}/soda.$sodaslug.tsx (100%) rename app/routes/{ => inventory}/wine.$wineslug.tsx (100%) create mode 100644 app/routes/scan/layout.tsx rename app/routes/{scan.tsx => scan/route.tsx} (84%) diff --git a/app/auth/session.ts b/app/auth/session.ts index cb4b1b4..f1c3f57 100644 --- a/app/auth/session.ts +++ b/app/auth/session.ts @@ -1,30 +1,32 @@ -import { createSessionStorage } from "@remix-run/node"; // or cloudflare/deno +import { CookieOptions, createSessionStorage } from "@remix-run/node"; // or cloudflare/deno import { db } from "~/utils/db.server"; function createDatabaseSessionStorage({ cookie -}) { +} : { cookie: {name : string} & CookieOptions } ) { // Configure your database client... return createSessionStorage({ cookie, async createData(data, expires) { - var id; + var session; - if(data && data.id){ - id = await db.session.create({data:{data: JSON.stringify({ data }), user:{connect:{id: data.id}}}}); + if(data && data.user_id){ + session = await db.session.create({data:{data: JSON.stringify({ data }), user:{connect:{id: data.user_id}}}}); } else { - id = await db.session.create({data:{data: JSON.stringify({ data })}}); + session = await db.session.create({data:{data: JSON.stringify({ data })}}); } - return id.id; + return session.id; }, async readData(id) { - return (await db.session.findUnique({where:{id: id}, include: {user: true}})) || null; + let sessiondata = await db.session.findUnique({select: {user_id: true, data: true, user: true},where:{id: id}}) || null; + return sessiondata; }, async updateData(id, data, expires) { - if(data && data.id){ - await db.session.upsert({create:{data: JSON.stringify({ data }), user:{connect:{id: data.id}}}, update:{data: JSON.stringify({ data }), user:{connect:{id: data.id}}}, where: {id: id}}); + + if(data && data.user_id){ + await db.session.upsert({create:{data: JSON.stringify({ data }), user:{connect:{id: data.user_id}}}, update:{data: JSON.stringify({ data }), user:{connect:{id: data.user_id}}}, where: {id: id}}); } else { await db.session.upsert({create:{data: JSON.stringify({ data })}, update:{data: JSON.stringify({ data })}, where: {id: id}}); @@ -44,7 +46,7 @@ createDatabaseSessionStorage( name: "__session", httpOnly: true, - maxAge: 60, + maxAge: 31556952, path: "/", sameSite: "lax", secrets: ["super_secret_cookie_thingy"], diff --git a/app/components/cards/drink.card.tsx b/app/components/cards/drink.card.tsx index 0efbf22..6e3436d 100644 --- a/app/components/cards/drink.card.tsx +++ b/app/components/cards/drink.card.tsx @@ -1,4 +1,5 @@ +import { DrinkType } from '@prisma/client'; import { Link } from '@remix-run/react'; import { Card, ListGroup } from 'react-bootstrap'; import { DrinkComposite, isDrinkWithContainers, isDrinkWithManufacturer, isDrinkWithStyle } from '~/models/types'; @@ -9,17 +10,20 @@ interface Arguments { function DrinkCard(arg:Arguments) { var trimmedDescription = arg.drink.description.length > 120 ? arg.drink.description.substring(0, 120) + "..." : arg.drink.description; - var link = "/drink/"; + var link = "/inventory/"; switch (arg.drink.type) { - case 'Beer': - link = "/beer/"; + case DrinkType.Beer: + link += "beer/"; break; - case 'Wine': - link = "/wine/"; + case DrinkType.Wine: + link += "wine/"; break; - case 'Soda': - link = "/soda/"; + case DrinkType.Soda: + link += "soda/"; + break; + case DrinkType.Cocktail: + link += "cocktail/"; break; } diff --git a/app/components/header.tsx b/app/components/header.tsx index 24b7e8f..90ca0f7 100644 --- a/app/components/header.tsx +++ b/app/components/header.tsx @@ -29,6 +29,13 @@ function Header(data: Arguments) { + { loggedin ? ( + + ) : "" } diff --git a/app/entry.server.tsx b/app/entry.server.tsx index 45db322..b867e3f 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -14,6 +14,12 @@ import { renderToPipeableStream } from "react-dom/server"; const ABORT_DELAY = 5_000; +const error = console.error; +console.error = (...args: any) => { + if (/defaultProps/.test(args[0])) return; + error(...args); +}; + export default function handleRequest( request: Request, responseStatusCode: number, diff --git a/app/models/types.tsx b/app/models/types.tsx index 9fe67b5..704d74b 100644 --- a/app/models/types.tsx +++ b/app/models/types.tsx @@ -1,3 +1,4 @@ +import { ContainerType } from '@prisma/client'; import { Drink, Beer, BeerStyle, Wine, WineStyle, Soda, Manufacturer, Container, Cocktail, Section } from '@zenstackhq/runtime/models'; export type ContainerWithSection = Container & { section: Section} @@ -27,4 +28,19 @@ export function isDrinkWithStyle(drink:DrinkComposite) : drink is WineWithStyle const winestyle = (drink as { style: WineStyle}).style != undefined; return beerstyle || winestyle; +} + +export function containerTypeToString(type: ContainerType){ + switch(type){ + case ContainerType.BeerBottle: + return "Beer"; + case ContainerType.Can: + return "Can"; + case ContainerType.Carton: + return "Carton"; + case ContainerType.PlasticBottle: + return "PET"; + case ContainerType.WineBottle: + return "Wine"; + } } \ No newline at end of file diff --git a/app/root.tsx b/app/root.tsx index 54d19dc..3cf661b 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -6,7 +6,6 @@ import { useRouteError, isRouteErrorResponse, json, - redirect, useLoaderData } from "@remix-run/react"; import type { PropsWithChildren } from "react"; @@ -20,31 +19,11 @@ export const links: LinksFunction = () => [ import Header, { HeaderData } from "~/components/header"; import { Col, Container, Row } from "react-bootstrap"; import { getSession } from "./auth/session"; -import { head } from "lodash"; -import { User } from "@zenstackhq/runtime/models"; - -export async function loader({ - request, -}: LoaderFunctionArgs) { - const session = await getSession( - request.headers.get("Cookie") - ); - - let data : HeaderData = {loggedin: false}; - - if(session.has("user_id")){ - data.user = session.get("user"); - data.loggedin = true; - } - - return json({data}); -} function Document({ children, - headerData, title = "K-FRIDGE", -}: PropsWithChildren<{ title?: string, headerData: HeaderData }>) { +}: PropsWithChildren<{ title?: string}>) { return ( @@ -57,12 +36,7 @@ function Document({ -
-
-
-
- {children} -
+ {children} @@ -70,22 +44,19 @@ function Document({ } export default function App() { - const data = useLoaderData(); - return ( - - + + ); } export function ErrorBoundary() { const error = useRouteError(); - const data = useLoaderData(); if (isRouteErrorResponse(error)) { return ( - @@ -107,7 +78,7 @@ export function ErrorBoundary() { ? error.message : "Unknown error"; return ( - + diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 528b007..2857896 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -1,81 +1,36 @@ -import { LoaderFunctionArgs, json } from "@remix-run/node"; -import { useLoaderData, useSearchParams } from "@remix-run/react"; -import { enhance } from "@zenstackhq/runtime"; -import { Accordion, Button, Col, Container, Row, useAccordionButton } from "react-bootstrap"; -import DrinkCard from "~/components/cards/drink.card"; -import DrinkFilter from "~/components/filters/drink.filter"; -import { findDrinksFromSearch } from "~/models/drinks.filter.server"; -import { findManufacturersFromSearch } from "~/models/drinks.server"; -import { sortDrinksFromSearch } from "~/models/drinks.sort.server"; +import { UserType } from "@prisma/client"; +import type { LoaderFunctionArgs } from "@remix-run/node"; +import { + json, + redirect, + useLoaderData +} from "@remix-run/react"; -import { db } from "~/utils/db.server"; +import { getSession } from "~/auth/session"; -export const loader = async ({request} : LoaderFunctionArgs) => { - const dbe = enhance(db); - - const url = new URL(request.url); - - const beerStyles = await dbe.beerStyle.findMany(); - const wineStyles = await dbe.wineStyle.findMany(); - const manufacturers = await findManufacturersFromSearch(url.searchParams); - - const drinkResultsUnsorted = await findDrinksFromSearch(url.searchParams); - const drinkResults = sortDrinksFromSearch(url.searchParams, drinkResultsUnsorted); - - return json({beerStyles, wineStyles, manufacturers, drinkResults}); -}; - -export default function BeersRoute() { - const loadData = useLoaderData(); - - const [searchParams, setSearchParams] = useSearchParams(); - - function CustomToggle({ children, eventKey }) { - const decoratedOnClick = useAccordionButton(eventKey, () => - console.log('totally custom!'), - ); - - return ( - - ); - } - - return ( - - - - - Sorting and Filtering - - -
- - -
-
-
- - - - {loadData.drinkResults.map((drink) => ( - - - - ))} - - -
-
+export async function loader({ + request, +}: LoaderFunctionArgs) { + const session = await getSession( + request.headers.get("Cookie") ); + + if(session.has("user_id")){ + let user = session.get("user"); + + // Admin + if(user.type == UserType.Admin){ + return redirect("/admin"); + } + + if(user.type == UserType.Scanner){ + return redirect("/scan"); + } + } + + return redirect("/inventory"); } - +export default function Index() { + useLoaderData(); +} \ No newline at end of file diff --git a/app/routes/admin/edit/checkout.tsx b/app/routes/admin/edit/checkout.tsx new file mode 100644 index 0000000..80913fa --- /dev/null +++ b/app/routes/admin/edit/checkout.tsx @@ -0,0 +1,59 @@ +import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { useLoaderData } from "@remix-run/react"; +import { Button, Form } from "react-bootstrap"; +import { containerTypeToString } from "~/models/types"; + +import { enhance } from "~/utils/db.server"; + +export async function loader({ + params, + request + }: LoaderFunctionArgs) { + const { dbe } = await enhance(request); + + const checkouts = await dbe.history.findMany({select: {id: true, checkoutAt: true, container: {include: {drink: true}}}, orderBy: {checkoutAt: "desc"}, take: 20}); + + return json({ checkouts }); + }; + +export async function action({ + request, +}: ActionFunctionArgs) { + const { dbe } = await enhance(request); + + const form = await request.formData(); + const checkoutId = Number(form.get("id")); + + // Find related container + const container = await dbe.history.findUnique({select: {container_id: true}, where: {id: checkoutId}}); + + // Add one back to the inventory and remove history line + if(container){ + await dbe.container.update({where: {id: container.container_id}, data: {inventory: {increment: 1}}}); + await dbe.history.delete({where: {id: checkoutId}}); + } + + return null; +}; + +export default function EditCheckoutRoute() { + const lData = useLoaderData(); + + return ( +
+

Update inventory

+
+ + Checkouts + + { lData.checkouts.map((chkt) => ( + + ))} + + + +
+
+ ); + } \ No newline at end of file diff --git a/app/routes/admin/edit/inventory.tsx b/app/routes/admin/edit/inventory.tsx new file mode 100644 index 0000000..dbc3637 --- /dev/null +++ b/app/routes/admin/edit/inventory.tsx @@ -0,0 +1,59 @@ +import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; +import { json } from "@remix-run/node"; +import { useLoaderData } from "@remix-run/react"; +import { Button, Form } from "react-bootstrap"; +import { containerTypeToString } from "~/models/types"; + +import { enhance } from "~/utils/db.server"; + +export async function loader({ + params, + request + }: LoaderFunctionArgs) { + const { dbe } = await enhance(request); + + const containers = await dbe.container.findMany({select: {id: true, drink: true, type: true, inventory: true}, orderBy: {drink: {name: "asc"}}}); + + return json({ containers }); + }; + +export async function action({ + request, +}: ActionFunctionArgs) { + const { dbe } = await enhance(request); + + const form = await request.formData(); + const containerid = Number(form.get("id")); + let inventory = Number(form.get("inventory")); + + if(inventory <= 0) inventory = 0; + + await dbe.container.update({data: { inventory: inventory}, where: {id: containerid}}); + + return null; +}; + +export default function EditInventoryRoute() { + const lData = useLoaderData(); + + return ( +
+

Update inventory

+
+ + Drink container + + { lData.containers.map((container) => ( + + ))} + + + + New inventory amount + + + +
+
+ ); + } \ No newline at end of file diff --git a/app/routes/admin/layout.tsx b/app/routes/admin/layout.tsx new file mode 100644 index 0000000..ca32d82 --- /dev/null +++ b/app/routes/admin/layout.tsx @@ -0,0 +1,51 @@ +import { UserType } from "@prisma/client"; +import { LoaderFunctionArgs, redirect } from "@remix-run/node"; +import { Outlet, json, useLoaderData } from "@remix-run/react"; +import { Container, Row } from "react-bootstrap"; +import { getSession } from "~/auth/session"; +import Header, { HeaderData } from "~/components/header"; + +export async function loader({ + request, + }: LoaderFunctionArgs) { + const session = await getSession( + request.headers.get("Cookie") + ); + + if(!session.has("user_id")){ + return redirect("/"); + } + + let user = session.get("user"); + if(user.type != UserType.Admin){ + return redirect("/"); + } + + let data : HeaderData = {loggedin: false}; + + if(session.has("user_id")){ + data.user = session.get("user"); + data.loggedin = true; + } + + return json({data}); + } + +export default function InventoryLayout() { + let data = useLoaderData(); + + return ( + <> +
+
+
+
+ + + + + +
+ + ); + } \ No newline at end of file diff --git a/app/routes/new/beer.tsx b/app/routes/admin/new/beer.tsx similarity index 98% rename from app/routes/new/beer.tsx rename to app/routes/admin/new/beer.tsx index 5df3e53..a5f3a4f 100644 --- a/app/routes/new/beer.tsx +++ b/app/routes/admin/new/beer.tsx @@ -57,7 +57,7 @@ export const action = async ({ } const beer = await dbe.beer.create({ data: fields }); - return redirect(`/beer/${beer.id}`); + return redirect(`/inventory/beer/${beer.id}`); }; export default function NewBeerRoute() { diff --git a/app/routes/admin/route.tsx b/app/routes/admin/route.tsx new file mode 100644 index 0000000..6fd3ca7 --- /dev/null +++ b/app/routes/admin/route.tsx @@ -0,0 +1,11 @@ +import { Link } from "@remix-run/react"; +import { ListGroup } from "react-bootstrap"; + +export default function AdminRoute() { + return ( + + Change inventory numbers + Undo checkout + + ); + } \ No newline at end of file diff --git a/app/routes/beer.$beerslug.tsx b/app/routes/inventory/beer.$beerslug.tsx similarity index 96% rename from app/routes/beer.$beerslug.tsx rename to app/routes/inventory/beer.$beerslug.tsx index 0c7c944..afc42d3 100644 --- a/app/routes/beer.$beerslug.tsx +++ b/app/routes/inventory/beer.$beerslug.tsx @@ -13,7 +13,6 @@ import DrinkPage from "~/components/cards/drink.page"; import { db } from "~/utils/db.server"; import { enhance } from "@zenstackhq/runtime"; import { findIdFromSlug } from "~/models/drinks.server"; -import { Container, Row, Col } from "react-bootstrap"; export const loader = async ({ params, diff --git a/app/routes/inventory/layout.tsx b/app/routes/inventory/layout.tsx new file mode 100644 index 0000000..d07e903 --- /dev/null +++ b/app/routes/inventory/layout.tsx @@ -0,0 +1,36 @@ +import { LoaderFunctionArgs } from "@remix-run/node"; +import { Outlet, json, useLoaderData } from "@remix-run/react"; +import { getSession } from "~/auth/session"; +import Header, { HeaderData } from "~/components/header"; + +export async function loader({ + request, + }: LoaderFunctionArgs) { + const session = await getSession( + request.headers.get("Cookie") + ); + + let data : HeaderData = {loggedin: false}; + + if(session.has("user_id")){ + data.user = session.get("user"); + data.loggedin = true; + } + + return json({data}); + } + +export default function InventoryLayout() { + let data = useLoaderData(); + + return ( + <> +
+
+
+
+ +
+ + ); + } \ No newline at end of file diff --git a/app/routes/inventory/route.tsx b/app/routes/inventory/route.tsx new file mode 100644 index 0000000..7ae55a4 --- /dev/null +++ b/app/routes/inventory/route.tsx @@ -0,0 +1,81 @@ +import { LoaderFunctionArgs, json } from "@remix-run/node"; +import { useLoaderData, useSearchParams } from "@remix-run/react"; +import { enhance } from "@zenstackhq/runtime"; +import { Accordion, Button, Col, Container, Row, useAccordionButton } from "react-bootstrap"; +import DrinkCard from "~/components/cards/drink.card"; +import DrinkFilter from "~/components/filters/drink.filter"; +import { findDrinksFromSearch } from "~/models/drinks.filter.server"; +import { findManufacturersFromSearch } from "~/models/drinks.server"; +import { sortDrinksFromSearch } from "~/models/drinks.sort.server"; + +import { db } from "~/utils/db.server"; + +export const loader = async ({request} : LoaderFunctionArgs) => { + const dbe = enhance(db); + + const url = new URL(request.url); + + const beerStyles = await dbe.beerStyle.findMany(); + const wineStyles = await dbe.wineStyle.findMany(); + const manufacturers = await findManufacturersFromSearch(url.searchParams); + + const drinkResultsUnsorted = await findDrinksFromSearch(url.searchParams); + const drinkResults = sortDrinksFromSearch(url.searchParams, drinkResultsUnsorted); + + return json({beerStyles, wineStyles, manufacturers, drinkResults}); +}; + +export default function BeersRoute() { + const loadData = useLoaderData(); + + const [searchParams, setSearchParams] = useSearchParams(); + + function CustomToggle({ children, eventKey } : any) { + const decoratedOnClick = useAccordionButton(eventKey, () => + console.log('totally custom!'), + ); + + return ( + + ); + } + + return ( + + + + + Sorting and Filtering + + +
+ + +
+
+
+ + + + {loadData.drinkResults.map((drink) => ( + + + + ))} + + +
+
+ ); +} + + diff --git a/app/routes/soda.$sodaslug.tsx b/app/routes/inventory/soda.$sodaslug.tsx similarity index 100% rename from app/routes/soda.$sodaslug.tsx rename to app/routes/inventory/soda.$sodaslug.tsx diff --git a/app/routes/wine.$wineslug.tsx b/app/routes/inventory/wine.$wineslug.tsx similarity index 100% rename from app/routes/wine.$wineslug.tsx rename to app/routes/inventory/wine.$wineslug.tsx diff --git a/app/routes/login.tsx b/app/routes/login.tsx index ffda2cf..54579e1 100644 --- a/app/routes/login.tsx +++ b/app/routes/login.tsx @@ -3,7 +3,7 @@ import type { LoaderFunctionArgs, } from "@remix-run/node"; // or cloudflare/deno import { json, redirect } from "@remix-run/node"; // or cloudflare/deno - import { useLoaderData } from "@remix-run/react"; + import { useActionData, useLoaderData } from "@remix-run/react"; import { Button, Col, Container, Form, InputGroup, Row } from "react-bootstrap"; import { getSession, commitSession } from "~/auth/session"; @@ -16,14 +16,12 @@ import { validateCredentials } from "~/auth/validate"; request.headers.get("Cookie") ); - if (session.has("id")) { + if (session.has("user_id")) { // Redirect to the home page if they are already signed in. return redirect("/"); } - const data = { error: session.get("error") }; - - return json(data); + return null; } export async function action({ @@ -42,13 +40,11 @@ import { validateCredentials } from "~/auth/validate"; ); if (id == null) { - session.flash("error", "Invalid username/password"); - // Redirect back to the login page with errors. - return redirect("/login"); + return json({error: "Invalid credentials"}); } - session.set("id", id); + session.set("user_id", id); // Login succeeded, send them to the home page. return redirect("/", { @@ -59,7 +55,7 @@ import { validateCredentials } from "~/auth/validate"; } export default function Login() { - const { error } = useLoaderData(); + const { error } = useActionData() || {error: undefined}; return ( diff --git a/app/routes/logout.tsx b/app/routes/logout.tsx index 0cf6b90..671e9ef 100644 --- a/app/routes/logout.tsx +++ b/app/routes/logout.tsx @@ -12,10 +12,13 @@ import type { request.headers.get("Cookie") ); - if (session.has("id")) { + if (session.has("user_id")) { // Logout - await destroySession(session); - return redirect("/login"); + return redirect("/login", { + headers: { + "Set-Cookie": await destroySession(session), + }, + }); } return redirect("/"); diff --git a/app/routes/scan/layout.tsx b/app/routes/scan/layout.tsx new file mode 100644 index 0000000..a343359 --- /dev/null +++ b/app/routes/scan/layout.tsx @@ -0,0 +1,30 @@ +import { LoaderFunctionArgs } from "@remix-run/node"; +import { Outlet, json, redirect, useLoaderData } from "@remix-run/react"; +import { enhance } from "~/utils/db.server"; + +export async function loader({ + request, +}: LoaderFunctionArgs) { + const { session } = await enhance(request); + + if(!session.has("user_id")){ + return redirect("/"); + } + + return null; +} + +export default function InventoryLayout() { + useLoaderData(); + + return ( + <> +
+ +
+
+ +
+ + ); + } \ No newline at end of file diff --git a/app/routes/scan.tsx b/app/routes/scan/route.tsx similarity index 84% rename from app/routes/scan.tsx rename to app/routes/scan/route.tsx index 6d7a2a6..12125c4 100644 --- a/app/routes/scan.tsx +++ b/app/routes/scan/route.tsx @@ -1,38 +1,29 @@ import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; import { json, redirect } from "@remix-run/node"; import { useActionData, useLoaderData } from "@remix-run/react"; -import { enhance } from "@zenstackhq/runtime"; import { Col, Container, Form, Row, Table } from "react-bootstrap"; import { getSession } from "~/auth/session"; import timeAgo from "~/utils/datetime"; -import { db } from "~/utils/db.server"; +import { db, enhance } from "~/utils/db.server"; export async function loader({ request, }: LoaderFunctionArgs) { - const session = await getSession( - request.headers.get("Cookie") - ); + const { dbe, session } = await enhance(request); if(!session.has("user_id")){ return redirect("/"); } - const dbe = session.has("userid") ? enhance(db) : enhance(db, {user: session.get("user")}); - const history = await dbe.history.findMany({select: {checkoutAt: true ,container: {select: {drink: true}}}, take: 10, orderBy: {checkoutAt: "desc"}}); return json({history}); } -export const action = async ({ +export async function action({ request, -}: ActionFunctionArgs) => { - const session = await getSession( - request.headers.get("Cookie") - ); - - const dbe = session.has("userid") ? enhance(db) : enhance(db, {user: session.get("user")}); +}: ActionFunctionArgs){ + const { dbe } = await enhance(request); const form = await request.formData(); const barcode = form.get("barcode")?.toString() || ""; @@ -71,7 +62,7 @@ export default function ScanRoute() { -
+ Scan barcode: diff --git a/app/utils/db.server.ts b/app/utils/db.server.ts index 8736437..ebdc6e0 100644 --- a/app/utils/db.server.ts +++ b/app/utils/db.server.ts @@ -1,6 +1,8 @@ import { PrismaClient } from "@prisma/client"; import { singleton } from "./singleton.server"; +import { getSession } from "~/auth/session"; +import { enhance as znenhance } from "@zenstackhq/runtime"; // Hard-code a unique key, so we can look up the client when this module gets re-imported export const db = singleton( @@ -8,3 +10,13 @@ export const db = singleton( () => new PrismaClient() ); +export async function enhance( + request : Request +){ + const session = await getSession( + request.headers.get("Cookie") + ); + + const dbe = session.has("user_id") ? znenhance(db, {user: session.get("user")}) : znenhance(db); + return {dbe, session}; +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ffc3d36..72c4cd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "@remix-run/node": "^2.9.1", "@remix-run/react": "^2.9.1", "@remix-run/serve": "^2.9.1", - "@zenstackhq/runtime": "2.0.1", + "@zenstackhq/runtime": "2.0.3", "bootstrap": "^5.3.3", "isbot": "^4.1.0", "lodash": "^4.17.21", @@ -41,7 +41,7 @@ "typescript": "^5.4.5", "vite": "^5.1.0", "vite-tsconfig-paths": "^4.2.1", - "zenstack": "2.0.1" + "zenstack": "2.0.3" }, "engines": { "node": ">=18.0.0" @@ -2759,18 +2759,18 @@ "integrity": "sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==" }, "node_modules/@zenstackhq/language": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@zenstackhq/language/-/language-2.0.1.tgz", - "integrity": "sha512-PdnjVKndmXNn/vmdO4aKTwR26MJdyHO+Q8UOgmamIsA3Hp0nBsBy1BhvJPqFnPGCf7Pqwg5AD6pPvCmAg2i3Kw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@zenstackhq/language/-/language-2.0.3.tgz", + "integrity": "sha512-q8cy+QukLeRCTTg2vqTeZZLnN2ghq4ZXxRmy/P6QA5lvJo3sPREQkPBkhFmXR/soVIbEL73HxWTMZrxJvSYYHA==", "dev": true, "dependencies": { "langium": "1.3.1" } }, "node_modules/@zenstackhq/runtime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@zenstackhq/runtime/-/runtime-2.0.1.tgz", - "integrity": "sha512-iRIEtYrElqHSvrEN9NF8IHeNUlBSb1yGch1U1unLtaK806I5meLw17k1kBbRE3CwKNhicrClpYkXBCpOZ8xBqg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@zenstackhq/runtime/-/runtime-2.0.3.tgz", + "integrity": "sha512-ER/DNU2Hhb816D6kZEHqyMy5auwr8s31L+aNfKm4phgj52FYwj3eVFutx5ERIJSsx08pFSrH8E9YP4C/7rT9Mg==", "dependencies": { "bcryptjs": "^2.4.3", "buffer": "^6.0.3", @@ -2818,15 +2818,15 @@ } }, "node_modules/@zenstackhq/sdk": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@zenstackhq/sdk/-/sdk-2.0.1.tgz", - "integrity": "sha512-FoBGNOeYGVP31uqmSUzqWJ3N9O6/rkRrVOJn590OHF5PtShJU3sLFonzSU3IBdrv2xpnMTu2iH/WSgo7+LSLxQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@zenstackhq/sdk/-/sdk-2.0.3.tgz", + "integrity": "sha512-YL7mux6Yecd1EMLS3RbmyhcmsWtFklF170ZHI7KMmz8sdiJ+rmjFnjy5XzW1TwnQ7swYbjhgQQfY9OPuKLY/MQ==", "dev": true, "dependencies": { "@prisma/generator-helper": "5.7.0", "@prisma/internals": "5.7.0", - "@zenstackhq/language": "2.0.1", - "@zenstackhq/runtime": "2.0.1", + "@zenstackhq/language": "2.0.3", + "@zenstackhq/runtime": "2.0.3", "langium": "1.3.1", "lower-case-first": "^2.0.2", "semver": "^7.5.2", @@ -13329,16 +13329,16 @@ } }, "node_modules/zenstack": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/zenstack/-/zenstack-2.0.1.tgz", - "integrity": "sha512-IDTXX+aTYxow//3w+w11buMSDPbxm1zMzp5/EMRurmrzPrJDkCByhow36LSfzoaXlcvUOF1JFTW2G47YOC6ZdA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/zenstack/-/zenstack-2.0.3.tgz", + "integrity": "sha512-ManI6LC/b4oEQzsndJCsipLCbYynjZVal+jh7p/M2t1GQDEPA+7S2UDv1IYbzNy/J5DQh5htsEZFqZ3txT9/Ig==", "dev": true, "hasInstallScript": true, "dependencies": { "@paralleldrive/cuid2": "^2.2.0", "@types/node": "^20.12.7", - "@zenstackhq/language": "2.0.1", - "@zenstackhq/sdk": "2.0.1", + "@zenstackhq/language": "2.0.3", + "@zenstackhq/sdk": "2.0.3", "async-exit-hook": "^2.0.1", "change-case": "^4.1.2", "colors": "1.4.0", diff --git a/package.json b/package.json index 0ad79e4..ceedfc4 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@remix-run/node": "^2.9.1", "@remix-run/react": "^2.9.1", "@remix-run/serve": "^2.9.1", - "@zenstackhq/runtime": "2.0.1", + "@zenstackhq/runtime": "2.0.3", "bootstrap": "^5.3.3", "isbot": "^4.1.0", "lodash": "^4.17.21", @@ -46,7 +46,7 @@ "typescript": "^5.4.5", "vite": "^5.1.0", "vite-tsconfig-paths": "^4.2.1", - "zenstack": "2.0.1" + "zenstack": "2.0.3" }, "engines": { "node": ">=18.0.0" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 698a954..df8b5a8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -125,8 +125,8 @@ model Cocktail { } /// @@allow('read', true) -/// @@allow('update', auth().type == Scanner) -/// @@allow('all', auth().type == Admin) +/// @@allow('update', auth() != null) +/// @@allow('create,delete', auth().type == Admin) model Container { id Int @id() @default(autoincrement()) barcode String? @unique() diff --git a/schema.zmodel b/schema.zmodel index cfc8a82..a08d72e 100644 --- a/schema.zmodel +++ b/schema.zmodel @@ -141,7 +141,7 @@ model Container { checkouts History[] @@allow('read', true) - @@allow('update', auth().type == Scanner) + @@allow('update', auth() != null) @@allow('all', auth().type == Admin) } diff --git a/vite.config.ts b/vite.config.ts index 2b6aff9..c48bc63 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,5 +6,33 @@ import tsconfigPaths from "vite-tsconfig-paths"; installGlobals(); export default defineConfig({ - plugins: [remix(), tsconfigPaths()], + plugins: [remix({ + routes(defineRoutes) { + return defineRoutes((route) => { + // Index + route("/", "routes/_index.tsx", { index: true }); + + // Inventory + route("inventory", "routes/inventory/layout.tsx", () => { + route("", "routes/inventory/route.tsx", { index: true }); + route("beer/:beerslug", "routes/inventory/beer.$beerslug.tsx"); + route("wine/:wineslug", "routes/inventory/wine.$wineslug.tsx"); + route("soda/:sodaslug", "routes/inventory/soda.$sodaslug.tsx"); + }); + + // Scanner + route("scan", "routes/scan/layout.tsx", () => { + route("", "routes/scan/route.tsx", { index: true }); + }); + + // Admin + route("admin", "routes/admin/layout.tsx", () => { + route("", "routes/admin/route.tsx", { index: true }); + route("new/beer", "routes/admin/new/beer.tsx"); + route("edit/inventory", "routes/admin/edit/inventory.tsx"); + route("edit/checkout", "routes/admin/edit/checkout.tsx"); + }); + }); + }, + }), tsconfigPaths()], });