Change structure and implement some admin functions

This commit is contained in:
2024-05-12 18:44:33 +02:00
parent 3b19804537
commit 1e750e1ca5
27 changed files with 502 additions and 185 deletions
+13 -11
View File
@@ -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"],
+11 -7
View File
@@ -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;
}
+9 -2
View File
@@ -29,6 +29,13 @@ function Header(data: Arguments) {
</LinkContainer>
<Navbar.Toggle />
<Navbar.Collapse>
{ loggedin ? (
<Nav>
<LinkContainer to="/inventory">
<Nav.Link>Inventory</Nav.Link>
</LinkContainer>
</Nav>
) : "" }
<Nav className="me-auto">
{ showScanButtons ? (
<LinkContainer to="/scan">
@@ -36,8 +43,8 @@ function Header(data: Arguments) {
</LinkContainer>
) : "" }
{ showAdminButtons ? (
<LinkContainer to="/new/beer">
<Nav.Link>Add Beer</Nav.Link>
<LinkContainer to="/admin">
<Nav.Link>Admin</Nav.Link>
</LinkContainer>
) : "" }
</Nav>
+6
View File
@@ -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,
+16
View File
@@ -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";
}
}
+6 -35
View File
@@ -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 (
<html lang="en" data-bs-theme="light">
<head>
@@ -57,12 +36,7 @@ function Document({
<Links />
</head>
<body>
<header>
<Header data={headerData}></Header>
</header>
<main>
{children}
</main>
{children}
<Scripts />
</body>
</html>
@@ -70,22 +44,19 @@ function Document({
}
export default function App() {
const data = useLoaderData<typeof loader>();
return (
<Document headerData={data.data}>
<Outlet />
<Document>
<Outlet></Outlet>
</Document>
);
}
export function ErrorBoundary() {
const error = useRouteError();
const data = useLoaderData<typeof loader>();
if (isRouteErrorResponse(error)) {
return (
<Document headerData={data.data}
<Document
title={`${error.status} ${error.statusText}`}
>
<Container>
@@ -107,7 +78,7 @@ export function ErrorBoundary() {
? error.message
: "Unknown error";
return (
<Document title="Uh-oh!" headerData={data.data}>
<Document title="Uh-oh!">
<Container>
<Row className="align-items-md-stretch">
<Col md={12}>
+31 -76
View File
@@ -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<typeof loader>();
const [searchParams, setSearchParams] = useSearchParams();
function CustomToggle({ children, eventKey }) {
const decoratedOnClick = useAccordionButton(eventKey, () =>
console.log('totally custom!'),
);
return (
<Button
variant="primary"
onClick={decoratedOnClick}
>
{children}
</Button>
);
}
return (
<Container>
<Row className="mt-3">
<Col xs={12} md={4} xl={3} className="mb-3">
<Accordion>
<CustomToggle eventKey="0">Sorting and Filtering</CustomToggle>
<Accordion.Collapse eventKey="0">
<div className="h-100 p-3 mt-3 text-bg-dark rounded-3">
<DrinkFilter
beerStyles={loadData.beerStyles}
wineStyles={loadData.wineStyles}
manufacturers={loadData.manufacturers}
searchParams={searchParams} >
</DrinkFilter>
</div>
</Accordion.Collapse>
</Accordion>
</Col>
<Col>
<Row xs={2} md={3} lg={4} className="g-4">
{loadData.drinkResults.map((drink) => (
<Col >
<DrinkCard drink={drink}></DrinkCard>
</Col>
))}
</Row>
</Col>
</Row>
</Container>
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<typeof loader>();
}
+59
View File
@@ -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<typeof loader>();
return (
<div>
<h2>Update inventory</h2>
<Form method="post">
<Form.Group className='mb-2'>
<Form.Label>Checkouts</Form.Label>
<Form.Select name="id">
{ lData.checkouts.map((chkt) => (
<option value={chkt.id}>{chkt.container.drink.name} - {chkt.checkoutAt} ({chkt.container.inventory})</option>
))}
</Form.Select>
</Form.Group>
<Button type="submit">Remove</Button>
</Form>
</div>
);
}
+59
View File
@@ -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<typeof loader>();
return (
<div>
<h2>Update inventory</h2>
<Form method="post">
<Form.Group className='mb-2'>
<Form.Label>Drink container</Form.Label>
<Form.Select name="id">
{ lData.containers.map((container) => (
<option value={container.id}>{container.drink.name} - {containerTypeToString(container.type)} ({container.inventory})</option>
))}
</Form.Select>
</Form.Group>
<Form.Group className='mb-2'>
<Form.Label>New inventory amount</Form.Label>
<Form.Control name="inventory" type="number" min={0} max={50} step={1} />
</Form.Group>
<Button type="submit">Submit</Button>
</Form>
</div>
);
}
+51
View File
@@ -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<typeof loader>();
return (
<>
<header>
<Header data={data.data}></Header>
</header>
<main>
<Container>
<Row>
<Outlet />
</Row>
</Container>
</main>
</>
);
}
@@ -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() {
+11
View File
@@ -0,0 +1,11 @@
import { Link } from "@remix-run/react";
import { ListGroup } from "react-bootstrap";
export default function AdminRoute() {
return (
<ListGroup>
<ListGroup.Item><Link to="/admin/edit/inventory">Change inventory numbers</Link></ListGroup.Item>
<ListGroup.Item><Link to="/admin/edit/checkout">Undo checkout</Link></ListGroup.Item>
</ListGroup>
);
}
@@ -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,
+36
View File
@@ -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<typeof loader>();
return (
<>
<header>
<Header data={data.data}></Header>
</header>
<main>
<Outlet />
</main>
</>
);
}
+81
View File
@@ -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<typeof loader>();
const [searchParams, setSearchParams] = useSearchParams();
function CustomToggle({ children, eventKey } : any) {
const decoratedOnClick = useAccordionButton(eventKey, () =>
console.log('totally custom!'),
);
return (
<Button
variant="primary"
onClick={decoratedOnClick}
>
{children}
</Button>
);
}
return (
<Container>
<Row className="mt-3">
<Col xs={12} md={4} xl={3} className="mb-3">
<Accordion>
<CustomToggle eventKey="0">Sorting and Filtering</CustomToggle>
<Accordion.Collapse eventKey="0">
<div className="h-100 p-3 mt-3 text-bg-dark rounded-3">
<DrinkFilter
beerStyles={loadData.beerStyles}
wineStyles={loadData.wineStyles}
manufacturers={loadData.manufacturers}
searchParams={searchParams} >
</DrinkFilter>
</div>
</Accordion.Collapse>
</Accordion>
</Col>
<Col>
<Row xs={2} md={3} lg={4} className="g-4">
{loadData.drinkResults.map((drink) => (
<Col >
<DrinkCard drink={drink}></DrinkCard>
</Col>
))}
</Row>
</Col>
</Row>
</Container>
);
}
+6 -10
View File
@@ -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<typeof loader>();
const { error } = useActionData<typeof action>() || {error: undefined};
return (
<Container>
+6 -3
View File
@@ -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("/");
+30
View File
@@ -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<typeof loader>();
return (
<>
<header>
</header>
<main>
<Outlet />
</main>
</>
);
}
@@ -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() {
<Container>
<Row className="mt-3">
<Col xs={4}>
<Form method="post" noValidate>
<Form method="post" action="/scan?index" noValidate>
<Form.Group>
<Form.Label>
Scan barcode:
+12
View File
@@ -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};
}
+18 -18
View File
@@ -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",
+2 -2
View File
@@ -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"
+2 -2
View File
@@ -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()
+1 -1
View File
@@ -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)
}
+29 -1
View File
@@ -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()],
});