AI Changes: add user management and hashed passwords, admin dashboard, report fixes
This commit is contained in:
@@ -18,3 +18,8 @@ Architecture:
|
||||
- user (can view the website on their phone)
|
||||
- scanner (the physical raspberry pi with barcode scanner at the fridge)
|
||||
- admin (me, the developer that can access and update everything)
|
||||
|
||||
Building and validation:
|
||||
`npx remix vite:build` to build the project and check for errors
|
||||
`npx remix vite:dev` to run the project in live dev mode
|
||||
`npx remix routes` to validate all routes are present and correct
|
||||
+60
-2
@@ -1,9 +1,67 @@
|
||||
import { createHash, timingSafeEqual } from "node:crypto";
|
||||
|
||||
import { db } from "~/utils/db.server";
|
||||
|
||||
function normalizePassword(password: string) {
|
||||
return password.trim();
|
||||
}
|
||||
|
||||
export function hashPassword(password: string) {
|
||||
const normalizedPassword = normalizePassword(password);
|
||||
return createHash("sha256").update(normalizedPassword).digest("hex");
|
||||
}
|
||||
|
||||
function isHashedPassword(value: string) {
|
||||
return /^[a-f0-9]{64}$/i.test(value);
|
||||
}
|
||||
|
||||
export function verifyPassword(password: string, storedPassword: string | null | undefined) {
|
||||
if (!storedPassword) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedPassword = normalizePassword(password);
|
||||
const hashedPassword = hashPassword(normalizedPassword);
|
||||
|
||||
if (isHashedPassword(storedPassword)) {
|
||||
const storedBuffer = Buffer.from(storedPassword);
|
||||
const hashedBuffer = Buffer.from(hashedPassword);
|
||||
|
||||
if (hashedBuffer.length !== storedBuffer.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return timingSafeEqual(hashedBuffer, storedBuffer);
|
||||
}
|
||||
|
||||
return normalizePassword(storedPassword) === normalizedPassword;
|
||||
}
|
||||
|
||||
export async function validateCredentials(
|
||||
username: string,
|
||||
password: string
|
||||
) {
|
||||
var user = await db.user.findUnique({where:{username: username, password: password}, select:{id: true}});
|
||||
return user?.id;
|
||||
const user = await db.user.findUnique({
|
||||
where: { username },
|
||||
select: { id: true, password: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const passwordMatches = verifyPassword(password, user.password);
|
||||
|
||||
if (!passwordMatches) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isHashedPassword(user.password ?? "")) {
|
||||
await db.user.update({
|
||||
where: { id: user.id },
|
||||
data: { password: hashPassword(password) },
|
||||
});
|
||||
}
|
||||
|
||||
return user.id;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ function Header(data: Arguments) {
|
||||
</Nav>
|
||||
{ loggedin ? (
|
||||
<Nav>
|
||||
<Nav.Link as={Link} to="/users">Users</Nav.Link>
|
||||
<Nav.Link as={Link} to="/logout">Logout ({username})</Nav.Link>
|
||||
</Nav>
|
||||
) : (
|
||||
|
||||
@@ -17,23 +17,27 @@ export const action = async ({
|
||||
const dateEnd = String(form.get("dateEnd")) + ":00.000z" || Date.now().toString();
|
||||
|
||||
try{
|
||||
const lastIdEntries = await dbe.report.findMany({select: {id: true}, take: 1, orderBy:{createdAt: "desc"}})
|
||||
|
||||
const containersWithHistoryAndDrink = await dbe.container.findMany({
|
||||
where: {checkouts: {some:{AND: [{checkoutAt: {gte: dateStart}}, {checkoutAt: {lte: dateEnd}}]}}},
|
||||
include: {drink: true, checkouts: {where:{AND: [{checkoutAt: {gte: dateStart}}, {checkoutAt: {lte: dateEnd}}]}}}
|
||||
});
|
||||
|
||||
const reportId = String(lastIdEntries[0] ? lastIdEntries[0].id + 1 : 0).padStart(8, "0");
|
||||
const report = await dbe.report.create({ data: {
|
||||
name: name,
|
||||
dateStart: dateStart,
|
||||
dateEnd: dateEnd
|
||||
}
|
||||
});
|
||||
|
||||
const reportId = String(report.id).padStart(8, "0");
|
||||
|
||||
const filepath = await createPdf(slugify(name), reportId, dateStart, dateEnd, containersWithHistoryAndDrink);
|
||||
|
||||
await dbe.report.create({ data: {
|
||||
name: name,
|
||||
dateStart: dateStart,
|
||||
dateEnd: dateEnd,
|
||||
await dbe.report.update({ data: {
|
||||
file: filepath
|
||||
}
|
||||
},
|
||||
where: {id: report.id}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
+237
-3
@@ -1,10 +1,174 @@
|
||||
import { Link } from "@remix-run/react";
|
||||
import { ListGroup } from "react-bootstrap";
|
||||
import { UserType } from "@prisma/client";
|
||||
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { json, redirect } from "@remix-run/node";
|
||||
import { Form, Link, useActionData, useLoaderData } from "@remix-run/react";
|
||||
import { Alert, Button, Card, Col, Form as BootstrapForm, ListGroup, Row } from "react-bootstrap";
|
||||
|
||||
import { hashPassword } from "~/auth/validate";
|
||||
import { enhance } from "~/utils/db.server";
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const { dbe } = await enhance(request);
|
||||
|
||||
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
|
||||
|
||||
const [containersWithoutBarcode, checkoutsLastHour, latestCheckout, users] = await Promise.all([
|
||||
dbe.container.count({ where: { barcode: null } }),
|
||||
dbe.history.count({ where: { checkoutAt: { gte: oneHourAgo } } }),
|
||||
dbe.history.findFirst({
|
||||
orderBy: { checkoutAt: "desc" },
|
||||
select: {
|
||||
id: true,
|
||||
checkoutAt: true,
|
||||
container: {
|
||||
select: {
|
||||
drink: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
dbe.user.findMany({
|
||||
select: { id: true, username: true, type: true },
|
||||
orderBy: { username: "asc" },
|
||||
}),
|
||||
]);
|
||||
|
||||
return json({
|
||||
containersWithoutBarcode,
|
||||
checkoutsLastHour,
|
||||
latestCheckout,
|
||||
users,
|
||||
});
|
||||
}
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const { dbe } = await enhance(request);
|
||||
const form = await request.formData();
|
||||
const intent = form.get("intent")?.toString();
|
||||
|
||||
if (intent === "undo-last-checkout") {
|
||||
const checkoutId = Number(form.get("checkoutId"));
|
||||
|
||||
if (!checkoutId) {
|
||||
return json({ error: "No checkout selected." });
|
||||
}
|
||||
|
||||
const checkout = await dbe.history.findUnique({
|
||||
where: { id: checkoutId },
|
||||
select: { container_id: true },
|
||||
});
|
||||
|
||||
if (checkout) {
|
||||
await dbe.container.update({
|
||||
where: { id: checkout.container_id },
|
||||
data: { inventory: { increment: 1 } },
|
||||
});
|
||||
await dbe.history.delete({ where: { id: checkoutId } });
|
||||
}
|
||||
|
||||
return redirect("/admin");
|
||||
}
|
||||
|
||||
if (intent === "add-user") {
|
||||
const username = form.get("username")?.toString().trim();
|
||||
const password = form.get("password")?.toString() ?? "";
|
||||
const type = form.get("type")?.toString();
|
||||
|
||||
if (!username || !password) {
|
||||
return json({ error: "Username and password are required." });
|
||||
}
|
||||
|
||||
const normalizedType = type === UserType.Admin || type === UserType.Scanner ? type : UserType.User;
|
||||
|
||||
try {
|
||||
await dbe.user.create({
|
||||
data: {
|
||||
username,
|
||||
password: hashPassword(password),
|
||||
type: normalizedType,
|
||||
},
|
||||
});
|
||||
|
||||
return json({ success: "User added" });
|
||||
} catch {
|
||||
return json({ error: "That username is already in use." });
|
||||
}
|
||||
}
|
||||
|
||||
if (intent === "set-password") {
|
||||
const userId = Number(form.get("userId"));
|
||||
const password = form.get("password")?.toString() ?? "";
|
||||
|
||||
if (!userId || !password) {
|
||||
return json({ error: "A user and a password are required." });
|
||||
}
|
||||
|
||||
const existingUser = await dbe.user.findUnique({ where: { id: userId }, select: { id: true } });
|
||||
if (!existingUser) {
|
||||
return json({ error: "User not found." });
|
||||
}
|
||||
|
||||
await dbe.user.update({
|
||||
where: { id: userId },
|
||||
data: { password: hashPassword(password) },
|
||||
});
|
||||
|
||||
return json({ success: "Password set" });
|
||||
}
|
||||
|
||||
return json({ error: "Unknown action." });
|
||||
}
|
||||
|
||||
export default function AdminRoute() {
|
||||
const loaderData = useLoaderData<typeof loader>();
|
||||
const actionData = useActionData<typeof action>();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Admin</h1>
|
||||
|
||||
<h2 className="mt-4">Dashboard</h2>
|
||||
<Row className="g-3">
|
||||
<Col md={4}>
|
||||
<Card className="h-100">
|
||||
<Card.Body>
|
||||
<Card.Title>Containers without barcode</Card.Title>
|
||||
<Card.Text className="display-6">{loaderData.containersWithoutBarcode}</Card.Text>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col md={4}>
|
||||
<Card className="h-100">
|
||||
<Card.Body>
|
||||
<Card.Title>Checkouts in the last hour</Card.Title>
|
||||
<Card.Text className="display-6">{loaderData.checkoutsLastHour}</Card.Text>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col md={4}>
|
||||
<Card className="h-100">
|
||||
<Card.Body>
|
||||
<Card.Title>Last checked out</Card.Title>
|
||||
<Card.Text>
|
||||
{loaderData.latestCheckout?.container.drink.name ?? "No recent checkouts"}
|
||||
</Card.Text>
|
||||
{loaderData.latestCheckout ? (
|
||||
<Form method="post">
|
||||
<input type="hidden" name="intent" value="undo-last-checkout" />
|
||||
<input type="hidden" name="checkoutId" value={loaderData.latestCheckout.id} />
|
||||
<Button type="submit" variant="outline-danger">Undo</Button>
|
||||
</Form>
|
||||
) : null}
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<h2 className="mt-4">Quick actions</h2>
|
||||
<ListGroup>
|
||||
<ListGroup.Item><Link to="/admin/new">Add new items</Link></ListGroup.Item>
|
||||
<ListGroup.Item><Link to="/admin/edit">Edit or remove items</Link></ListGroup.Item>
|
||||
@@ -12,10 +176,80 @@ export default function AdminRoute() {
|
||||
<ListGroup.Item><Link to="/admin/manage/undo-checkout">Undo checkout</Link></ListGroup.Item>
|
||||
</ListGroup>
|
||||
|
||||
<h1>Reports</h1>
|
||||
<h2 className="mt-4">Reports</h2>
|
||||
<ListGroup>
|
||||
<ListGroup.Item><Link to="/admin/reports">Reports</Link></ListGroup.Item>
|
||||
</ListGroup>
|
||||
|
||||
<h2 className="mt-4">User management</h2>
|
||||
{actionData?.error ? <Alert variant="danger" className="mb-3">{actionData.error}</Alert> : null}
|
||||
{actionData?.success ? <Alert variant="success" className="mb-3">{actionData.success}</Alert> : null}
|
||||
<Row className="g-3 mb-4">
|
||||
<Col lg={6}>
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<Card.Title>Add user</Card.Title>
|
||||
<Form method="post">
|
||||
<input type="hidden" name="intent" value="add-user" />
|
||||
<BootstrapForm.Group className="mb-3">
|
||||
<BootstrapForm.Label>Username</BootstrapForm.Label>
|
||||
<BootstrapForm.Control name="username" required />
|
||||
</BootstrapForm.Group>
|
||||
<BootstrapForm.Group className="mb-3">
|
||||
<BootstrapForm.Label>Password</BootstrapForm.Label>
|
||||
<BootstrapForm.Control type="password" name="password" required />
|
||||
</BootstrapForm.Group>
|
||||
<BootstrapForm.Group className="mb-3">
|
||||
<BootstrapForm.Label>Role</BootstrapForm.Label>
|
||||
<BootstrapForm.Select name="type" defaultValue={UserType.User}>
|
||||
<option value={UserType.User}>User</option>
|
||||
<option value={UserType.Scanner}>Scanner</option>
|
||||
<option value={UserType.Admin}>Admin</option>
|
||||
</BootstrapForm.Select>
|
||||
</BootstrapForm.Group>
|
||||
<Button type="submit">Create user</Button>
|
||||
</Form>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col lg={6}>
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<Card.Title>Set password</Card.Title>
|
||||
<Form method="post">
|
||||
<input type="hidden" name="intent" value="set-password" />
|
||||
<BootstrapForm.Group className="mb-3">
|
||||
<BootstrapForm.Label>User</BootstrapForm.Label>
|
||||
<BootstrapForm.Select name="userId" defaultValue={loaderData.users[0]?.id ?? ""}>
|
||||
{loaderData.users.map((user) => (
|
||||
<option key={user.id} value={user.id}>{user.username} ({user.type})</option>
|
||||
))}
|
||||
</BootstrapForm.Select>
|
||||
</BootstrapForm.Group>
|
||||
<BootstrapForm.Group className="mb-3">
|
||||
<BootstrapForm.Label>New password</BootstrapForm.Label>
|
||||
<BootstrapForm.Control type="password" name="password" required />
|
||||
</BootstrapForm.Group>
|
||||
<Button type="submit" variant="outline-primary">Update password</Button>
|
||||
</Form>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card className="mb-4">
|
||||
<Card.Body>
|
||||
<Card.Title>Existing users</Card.Title>
|
||||
<ListGroup variant="flush">
|
||||
{loaderData.users.map((user) => (
|
||||
<ListGroup.Item key={user.id} className="d-flex justify-content-between align-items-center">
|
||||
<span>{user.username}</span>
|
||||
<span className="text-muted">{user.type}</span>
|
||||
</ListGroup.Item>
|
||||
))}
|
||||
</ListGroup>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
|
||||
import { Outlet, json, useFetcher, useLoaderData, useRevalidator } from "@remix-run/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getSession } from "~/auth/session";
|
||||
import { Outlet, json, useLoaderData, useRevalidator } from "@remix-run/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Header, { HeaderData } from "~/components/header";
|
||||
import { enhance } from "~/utils/db.server";
|
||||
|
||||
@@ -32,18 +31,16 @@ export default function InventoryLayout() {
|
||||
let revalidator = useRevalidator();
|
||||
|
||||
const [checkouts, setCheckouts] = useState(data.checkouts);
|
||||
let fetcher = useFetcher();
|
||||
|
||||
let shouldFetch = true;
|
||||
const shouldFetchRef = useRef(true);
|
||||
|
||||
// User has switched back to the tab
|
||||
const onFocus = () => {
|
||||
shouldFetch = true;
|
||||
shouldFetchRef.current = true;
|
||||
};
|
||||
|
||||
// User has switched away from the tab (AKA tab is hidden)
|
||||
const onBlur = () => {
|
||||
shouldFetch = false;
|
||||
shouldFetchRef.current = false;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -59,30 +56,44 @@ export default function InventoryLayout() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
const pollCheckouts = async () => {
|
||||
if (!shouldFetchRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(shouldFetch){
|
||||
fetcher.load("/resource/checkouts");
|
||||
try {
|
||||
const response = await fetch("/resource/checkouts", { credentials: "same-origin" });
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(fetcher.state === "idle"){
|
||||
if(!fetcher.data) return;
|
||||
const payload = await response.json() as { checkouts?: number | string | null };
|
||||
const newCheckouts = Number(payload.checkouts);
|
||||
|
||||
let newCheckouts = Number(fetcher.data.checkouts);
|
||||
if(newCheckouts != checkouts){
|
||||
if (!Number.isFinite(newCheckouts)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newCheckouts !== checkouts) {
|
||||
setCheckouts(newCheckouts);
|
||||
|
||||
if (revalidator.state === "idle") {
|
||||
revalidator.revalidate();
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore transient network errors while the connection is down.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const timer = window.setInterval(() => {
|
||||
void pollCheckouts();
|
||||
}, 5000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
});
|
||||
void pollCheckouts();
|
||||
|
||||
return () => window.clearInterval(timer);
|
||||
}, [checkouts, revalidator]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { json, redirect } from "@remix-run/node";
|
||||
import { Form, useActionData, useLoaderData } from "@remix-run/react";
|
||||
import { Alert, Button, Card, Col, Container, Form as BootstrapForm, Row } from "react-bootstrap";
|
||||
|
||||
import { hashPassword, verifyPassword } from "~/auth/validate";
|
||||
import { enhance } from "~/utils/db.server";
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const { session, dbe } = await enhance(request);
|
||||
|
||||
if (!session.has("user_id")) {
|
||||
return redirect("/login");
|
||||
}
|
||||
|
||||
const user = await dbe.user.findUnique({
|
||||
where: { id: Number(session.get("user_id")) },
|
||||
select: { id: true, username: true, type: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return redirect("/logout");
|
||||
}
|
||||
|
||||
return json({ user });
|
||||
}
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const { session, dbe } = await enhance(request);
|
||||
|
||||
if (!session.has("user_id")) {
|
||||
return redirect("/login");
|
||||
}
|
||||
|
||||
const userId = Number(session.get("user_id"));
|
||||
const form = await request.formData();
|
||||
const currentPassword = form.get("currentPassword")?.toString() ?? "";
|
||||
const newPassword = form.get("newPassword")?.toString() ?? "";
|
||||
const confirmPassword = form.get("confirmPassword")?.toString() ?? "";
|
||||
|
||||
const user = await dbe.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { id: true, password: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return json({ error: "User not found." }, { status: 404 });
|
||||
}
|
||||
|
||||
const passwordMatches = verifyPassword(currentPassword, user.password);
|
||||
if (!passwordMatches) {
|
||||
return json({ error: "Current password is incorrect." });
|
||||
}
|
||||
|
||||
if (newPassword.length < 4) {
|
||||
return json({ error: "New password must be at least 4 characters long." });
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
return json({ error: "New passwords do not match." });
|
||||
}
|
||||
|
||||
await dbe.user.update({
|
||||
where: { id: user.id },
|
||||
data: { password: hashPassword(newPassword) },
|
||||
});
|
||||
|
||||
return json({ success: "Password updated successfully." });
|
||||
}
|
||||
|
||||
export default function UsersRoute() {
|
||||
const loaderData = useLoaderData<typeof loader>();
|
||||
const actionData = useActionData<typeof action>();
|
||||
|
||||
return (
|
||||
<Container className="py-3">
|
||||
<Row className="justify-content-center">
|
||||
<Col md={8} lg={6}>
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<Card.Title>Account settings</Card.Title>
|
||||
<Card.Subtitle className="mb-3 text-muted">
|
||||
Manage the password for {loaderData.user.username}
|
||||
</Card.Subtitle>
|
||||
|
||||
{actionData?.error ? (
|
||||
<Alert variant="danger">{actionData.error}</Alert>
|
||||
) : null}
|
||||
|
||||
{actionData?.success ? (
|
||||
<Alert variant="success">{actionData.success}</Alert>
|
||||
) : null}
|
||||
|
||||
<Form method="post">
|
||||
<BootstrapForm.Group className="mb-3">
|
||||
<BootstrapForm.Label>Current password</BootstrapForm.Label>
|
||||
<BootstrapForm.Control type="password" name="currentPassword" required />
|
||||
</BootstrapForm.Group>
|
||||
|
||||
<BootstrapForm.Group className="mb-3">
|
||||
<BootstrapForm.Label>New password</BootstrapForm.Label>
|
||||
<BootstrapForm.Control type="password" name="newPassword" required />
|
||||
</BootstrapForm.Group>
|
||||
|
||||
<BootstrapForm.Group className="mb-3">
|
||||
<BootstrapForm.Label>Confirm new password</BootstrapForm.Label>
|
||||
<BootstrapForm.Control type="password" name="confirmPassword" required />
|
||||
</BootstrapForm.Group>
|
||||
|
||||
<Button type="submit">Save password</Button>
|
||||
</Form>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -34,14 +34,14 @@ export async function createPdf(filename: string, reportId: string, dateStart: s
|
||||
doc.setFontSize(10);
|
||||
doc.text("Selected date range: " + dateStart.slice(0, -8).replace("T", "") + " - " + dateEnd.slice(0,-8).replace("T", ""), 15, 35);
|
||||
|
||||
doc.autoTable({
|
||||
const table = doc.autoTable({
|
||||
startY: 40,
|
||||
head: [['Drink', 'Price', 'Amount', 'Total']],
|
||||
body: body,
|
||||
foot: [["", "", "Total", "€" + (Math.round(total*100)/100)]]
|
||||
});
|
||||
|
||||
let finalY = doc.autoTable.previous.finalY;
|
||||
const finalY = table?.lastAutoTable?.finalY ?? 40;
|
||||
|
||||
doc.text("Generated on " + new Date(Date.now()).toString(), 15, finalY + 10);
|
||||
|
||||
|
||||
@@ -181,5 +181,5 @@ model Report {
|
||||
name String @unique()
|
||||
dateStart DateTime
|
||||
dateEnd DateTime
|
||||
file String
|
||||
file String?
|
||||
}
|
||||
|
||||
+7
-2
@@ -1,13 +1,18 @@
|
||||
import { ContainerType, PrismaClient, UserType } from "@prisma/client";
|
||||
import { enhance } from "@zenstackhq/runtime";
|
||||
import { BeerStyle, Country, Manufacturer, Section, Soda, User, Wine, WineStyle } from "@zenstackhq/runtime/models";
|
||||
|
||||
import { hashPassword } from "../app/auth/validate";
|
||||
|
||||
const pr = new PrismaClient();
|
||||
|
||||
|
||||
async function seed() {
|
||||
|
||||
await pr.user.upsert({ create: {username: "kenneth", password: "asdf1239", type: UserType.Admin}, update: {password: "asdf1239"}, where: {username: "kenneth"} });
|
||||
await pr.user.upsert({ create: {username: "scanner", password: "asdf1239", type: UserType.Scanner}, update: {password: "asdf1239"}, where: {username: "scanner"} });
|
||||
const initialPassword = hashPassword("asdf1239");
|
||||
|
||||
await pr.user.upsert({ create: {username: "kenneth", password: initialPassword, type: UserType.Admin}, update: {password: initialPassword}, where: {username: "kenneth"} });
|
||||
await pr.user.upsert({ create: {username: "scanner", password: initialPassword, type: UserType.Scanner}, update: {password: initialPassword}, where: {username: "scanner"} });
|
||||
const user = await pr.user.findUnique({where: {username: "kenneth"}}) || undefined;
|
||||
|
||||
// Delete all sessions
|
||||
|
||||
+2
-2
@@ -205,7 +205,7 @@ model History {
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
password String @password @omit
|
||||
password String
|
||||
type UserType
|
||||
|
||||
sessions Session[]
|
||||
@@ -243,7 +243,7 @@ model Report {
|
||||
dateStart DateTime
|
||||
dateEnd DateTime
|
||||
|
||||
file String
|
||||
file String?
|
||||
|
||||
@@allow('all', auth().type == Admin)
|
||||
}
|
||||
Reference in New Issue
Block a user