diff --git a/.env.example b/.env.example
index 6810739..4cf7769 100644
--- a/.env.example
+++ b/.env.example
@@ -12,4 +12,5 @@
# Prisma
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
DATABASE_URL="file:./db.sqlite"
-JWT_SECRET="well, now the secret is spoiled, isn't it?"
\ No newline at end of file
+JWT_SECRET="well, now the secret is spoiled, isn't it?"
+TZ="Asia/Taipei"
\ No newline at end of file
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 43aa967..384f4c3 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -17,7 +17,9 @@ model User {
name String
username String @unique
password String
+ rfid String? @unique
isAdmin Boolean @default(false)
+ rosterOnly Boolean @default(false)
periods Period[]
@@ -31,6 +33,10 @@ model User {
model TimePeriod {
id Int @id
name String
+
+ start String
+ end String
+
periods Period[]
createdAt DateTime @default(now())
diff --git a/src/components/DashboardHeader.tsx b/src/components/DashboardHeader.tsx
index f500649..ed52d73 100644
--- a/src/components/DashboardHeader.tsx
+++ b/src/components/DashboardHeader.tsx
@@ -14,6 +14,11 @@ export default function DashboardHeader({ url }: { url: string }) {
}
}, [logout.isSuccess])
+ useEffect(() => {
+ if (session.data?.rosterOnly && !url.startsWith("/dash/admin/roster"))
+ push("/dash/admin/roster");
+ }, [session.data])
+
return (
@@ -28,49 +33,57 @@ export default function DashboardHeader({ url }: { url: string }) {
-
-
-
-
-
-
+ {
+ !session?.data?.rosterOnly && (<>
+
+
+
+
+
+
+ >)
+ }
{
session?.data?.isAdmin && (
<>
-
-
-
-
-
-
+ {
+ !session?.data?.rosterOnly && (<>
+
+
+
+
+
+
+ >)
+ }
>
)
}
diff --git a/src/components/admin/Config.tsx b/src/components/admin/Config.tsx
new file mode 100644
index 0000000..bdd092f
--- /dev/null
+++ b/src/components/admin/Config.tsx
@@ -0,0 +1,79 @@
+import { zodResolver } from "@hookform/resolvers/zod";
+import { SubmitHandler, useForm } from "react-hook-form";
+import { z } from "zod";
+import { api } from "~/utils/api";
+import { AddTimePeriodSchema } from "~/utils/types";
+
+export default function Config() {
+ const timePeriods = api.admin.getTimePeriods.useQuery();
+ const addTimePeriod = api.admin.addTimePeriod.useMutation();
+ const deleteTimePeriod = api.admin.deleteTimePeriod.useMutation();
+
+ type AddTimePeriodSchemaType = z.infer
;
+ const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(AddTimePeriodSchema) });
+ const onSubmit: SubmitHandler = async (data, event) => addTimePeriod.mutateAsync(data).then(() => {
+ event?.target.reset();
+ timePeriods.refetch();
+ }).catch((err) => console.log(err));
+
+ return (
+
+ {addTimePeriod.isError &&
{addTimePeriod.error.message}
}
+
)
+}
\ No newline at end of file
diff --git a/src/components/admin/Periods.tsx b/src/components/admin/Periods.tsx
new file mode 100644
index 0000000..496ba5e
--- /dev/null
+++ b/src/components/admin/Periods.tsx
@@ -0,0 +1,93 @@
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useEffect, useState } from "react";
+import { SubmitHandler, useForm } from "react-hook-form";
+import { z } from "zod";
+import { api } from "~/utils/api";
+import { AddPeriods, AddTimePeriodSchema } from "~/utils/types";
+
+export default function Periods() {
+ const timePeriods = api.admin.getTimePeriods.useQuery();
+ const periods = api.admin.getPeriods.useQuery();
+ const addPeriods = api.admin.addPeriods.useMutation();
+ const enablePeriod = api.admin.enablePeriod.useMutation();
+ const disablePeriod = api.admin.disablePeriod.useMutation();
+ type AddPeriodsType = z.infer;
+ const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(AddPeriods) });
+ const onSubmit: SubmitHandler = async (data, event) => addPeriods.mutateAsync(data).then(() => {
+ event?.target.reset();
+ periods.refetch();
+ }).catch((err) => console.log(err));
+
+ let periodCnt = 0;
+ const [periodsDataId, setPeriodsDataId] = useState([]);
+
+ useEffect(() => {
+ const periodsDataId: number[] = [];
+ timePeriods.data?.forEach((timePeriod) => {
+ periodsDataId.push(timePeriod.id);
+ })
+ setPeriodsDataId(periodsDataId);
+ }, [timePeriods.data])
+
+ return (
+
+
+
+ Date |
+ {
+ timePeriods.data?.map((timePeriods) => {
+ return ({timePeriods.name} ({timePeriods.start} ~ {timePeriods.end}) | )
+ })
+ }
+
+
+
+ {
+ Object.keys(periods.data || {}).map((date) => {
+ periodCnt = 0;
+ return (
+
+ {date} |
+ {
+ timePeriods.data?.map((timePeriod) => {
+ const thisPeriodCnt = periodCnt;
+ if (periods.data![date]![periodCnt]?.timePeriodId == timePeriod.id) {
+ periodCnt++;
+ return ( disablePeriod.mutateAsync(periods.data![date]![thisPeriodCnt]!.id!).then(() => periods.refetch())}>
+ Disable
+ | )
+ } else {
+ return ( enablePeriod.mutateAsync({
+ date: date,
+ timePeriodId: periodsDataId[thisPeriodCnt]!
+ }).then(() => periods.refetch())}>
+ Enable
+ | )
+ }
+ })
+ }
+
+ )
+ })
+ }
+
+
+
+
Add Date
+
+
)
+}
\ No newline at end of file
diff --git a/src/components/admin/Users.tsx b/src/components/admin/Users.tsx
new file mode 100644
index 0000000..66e3e3e
--- /dev/null
+++ b/src/components/admin/Users.tsx
@@ -0,0 +1,107 @@
+import { zodResolver } from "@hookform/resolvers/zod";
+import Link from "next/link";
+import { SubmitHandler, useForm } from "react-hook-form";
+import { z } from "zod";
+import { api } from "~/utils/api";
+import { AddTimePeriodSchema, AddUserSchema } from "~/utils/types";
+
+export default function Users() {
+ const users = api.admin.getAllUsers.useQuery();
+ const addUser = api.admin.addUser.useMutation();
+ const resetPassword = api.admin.resetPassword.useMutation();
+ const toggleAdmin = api.admin.toggleUserIsAdmin.useMutation();
+ const toggleRosterOnly = api.admin.toggleUserRosterOnly.useMutation();
+
+ type AddUserSchemaType = z.infer;
+ const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(AddUserSchema) });
+ const onSubmit: SubmitHandler = async (data, event) => addUser.mutateAsync(data).then(() => {
+ event?.target.reset();
+ users.refetch();
+ }).catch((err) => console.log(err));
+
+ return (
+
+
+
+
+
+ Username |
+ Grade |
+ Class |
+ Number |
+ Name |
+ RFID |
+ Admin? |
+ Roster Only? |
+ Operation |
+
+
+
+ {
+ users.data?.map((user) => (
+
+ {user.username} |
+ {user.grade} |
+ {user.class} |
+ {user.number} |
+ {user.name} |
+ {user.rfid} |
+ toggleAdmin.mutateAsync(user.username).then(() => users.refetch())}
+ >{user.isAdmin ? "V" : "X"} |
+ toggleRosterOnly.mutateAsync(user.username).then(() => users.refetch())}
+ >{user.rosterOnly ? "V" : "X"} |
+
+
+ |
+
+ ))
+ }
+
+
+
+
+
For editing rows, please use backend SQL.
+
Roster Only users must also be Admin.
+
+
Create new user
+
+ {addUser.isError &&
{addUser.error.message}
}
+
)
+}
\ No newline at end of file
diff --git a/src/pages/api/rfid.ts b/src/pages/api/rfid.ts
new file mode 100644
index 0000000..2aa9cb8
--- /dev/null
+++ b/src/pages/api/rfid.ts
@@ -0,0 +1,26 @@
+import type { NextApiRequest, NextApiResponse } from 'next'
+import { z } from 'zod'
+import { setLastRfid } from '~/utils/rfid'
+
+type ResponseData = {
+ message: string
+}
+
+export default function handler(
+ req: NextApiRequest,
+ res: NextApiResponse
+) {
+ if ("uid" in req.body) {
+ try {
+ z.string().regex(/^[0-9a-f]{8}$/i).parse(req.body.uid)
+ } catch (e) {
+ res.status(200).json({ message: 'Invalid UID!' })
+ return
+ }
+ setLastRfid(req.body.uid)
+ res.status(200).json({ message: 'Received!' })
+ }
+ else {
+ res.status(200).json({ message: 'No UID!' })
+ }
+}
diff --git a/src/pages/dash/admin/index.tsx b/src/pages/dash/admin/index.tsx
new file mode 100644
index 0000000..5905a66
--- /dev/null
+++ b/src/pages/dash/admin/index.tsx
@@ -0,0 +1,53 @@
+import Head from "next/head";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+import DashboardHeader from "~/components/DashboardHeader";
+import Config from "~/components/admin/Config";
+import Periods from "~/components/admin/Periods";
+import Users from "~/components/admin/Users";
+import { api } from "~/utils/api";
+
+export default function Dash() {
+ const { push } = useRouter();
+ const isLoggedIn = api.admin.isLoggedIn.useQuery();
+
+ const [currentPage, setCurrentPage] = useState("config");
+
+ useEffect(() => {
+ if (isLoggedIn.failureCount > 0) {
+ push("/");
+ }
+ }, [isLoggedIn.failureCount])
+
+ if (isLoggedIn.isLoading) return <>>
+
+ return (<>
+
+ Savage Tracking
+
+
+
+
+
+
+
+ { currentPage === "config" &&
}
+ { currentPage === "users" &&
}
+ { currentPage === "periods" &&
}
+
+
+ >)
+}
\ No newline at end of file
diff --git a/src/pages/dash/admin/rfid.tsx b/src/pages/dash/admin/rfid.tsx
new file mode 100644
index 0000000..042d641
--- /dev/null
+++ b/src/pages/dash/admin/rfid.tsx
@@ -0,0 +1,79 @@
+import Head from "next/head";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+import DashboardHeader from "~/components/DashboardHeader";
+import { api } from "~/utils/api";
+
+export default function Dash() {
+ const { push } = useRouter();
+ const isLoggedIn = api.admin.isLoggedIn.useQuery();
+ const users = api.admin.getAllUsers.useQuery();
+ const lastRfid = api.admin.getLastRfid.useQuery();
+ const setUserRfid = api.admin.setUserRfid.useMutation();
+
+ useEffect(() => {
+ if (isLoggedIn.failureCount > 0) {
+ push("/");
+ }
+ }, [isLoggedIn.failureCount])
+
+ useEffect(() => {
+ if (lastRfid.isSuccess) {
+ const interval = setInterval(() => {
+ lastRfid.refetch();
+ }, 2000);
+ return () => clearInterval(interval);
+ }
+ }, [])
+
+ if (isLoggedIn.isLoading) return <>>
+
+ return (<>
+
+ Savage Tracking
+
+
+
+
+
+
+
+
+
+ Username |
+ Grade |
+ Class |
+ Number |
+ Name |
+ RFID |
+ Operation |
+
+
+
+ {
+ users.data?.map((user) => (
+
+ {user.username} |
+ {user.grade} |
+ {user.class} |
+ {user.number} |
+ {user.name} |
+ {user.rfid} |
+ {
+ setUserRfid.mutateAsync({
+ username: user.username,
+ rfid: lastRfid.data || ""
+ }).then(() => users.refetch())
+ }}>
+ Select
+ |
+
+ ))
+ }
+
+
+
Last RFID: {lastRfid.data}
+
+
+ >)
+}
\ No newline at end of file
diff --git a/src/pages/dash/admin/roster.tsx b/src/pages/dash/admin/roster.tsx
new file mode 100644
index 0000000..20e7a93
--- /dev/null
+++ b/src/pages/dash/admin/roster.tsx
@@ -0,0 +1,30 @@
+import Head from "next/head";
+import { useRouter } from "next/navigation";
+import { useEffect } from "react";
+import DashboardHeader from "~/components/DashboardHeader";
+import { api } from "~/utils/api";
+
+export default function Dash() {
+ const { push } = useRouter();
+ const isLoggedIn = api.admin.isLoggedIn.useQuery();
+
+
+ useEffect(() => {
+ if (isLoggedIn.failureCount > 0) {
+ push("/");
+ }
+ }, [isLoggedIn.failureCount])
+
+ if (isLoggedIn.isLoading) return <>>
+
+ return (<>
+
+ Savage Tracking
+
+
+
+
+
+
+ >)
+}
\ No newline at end of file
diff --git a/src/pages/dash/chgPassword.tsx b/src/pages/dash/chgPassword.tsx
new file mode 100644
index 0000000..a16eea5
--- /dev/null
+++ b/src/pages/dash/chgPassword.tsx
@@ -0,0 +1,63 @@
+import { zodResolver } from "@hookform/resolvers/zod";
+import Head from "next/head";
+import { useRouter } from "next/navigation";
+import { useEffect } from "react";
+import { SubmitHandler, useForm } from "react-hook-form";
+import { z } from "zod";
+import DashboardHeader from "~/components/DashboardHeader";
+import { api } from "~/utils/api";
+import { ChgPasswordSchema } from "~/utils/types";
+
+export default function Dash() {
+ const { push } = useRouter();
+ const isLoggedIn = api.admin.isLoggedIn.useQuery();
+ const chgPassword = api.admin.chgPassword.useMutation();
+ const logout = api.admin.logout.useMutation();
+ type ChgPasswordSchemaType = z.infer;
+ const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(ChgPasswordSchema) });
+ const onSubmit: SubmitHandler = async (data) => chgPassword.mutateAsync(data).catch((err) => console.log(err));
+
+ useEffect(() => {
+ if (isLoggedIn.failureCount > 1) {
+ push("/");
+ }
+ }, [isLoggedIn.failureCount])
+
+ if (isLoggedIn.isLoading) return <>>
+
+ useEffect(() => {
+ if (chgPassword.isSuccess) {
+ logout.mutateAsync().then(() => push("/"));
+ }
+ }, [chgPassword.isSuccess])
+
+ return (<>
+
+ Savage Tracking
+
+
+
+
+
+
+
+ >)
+}
\ No newline at end of file
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index cf9f2b1..7cb358b 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -13,13 +13,12 @@ export default function Home() {
const { push } = useRouter();
type LoginSchemaType = z.infer;
const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(LoginSchema) });
- const onSubmit: SubmitHandler = async (data) =>
- login.mutateAsync(data).catch((err) => console.log(err))
+ const onSubmit: SubmitHandler = async (data) => login.mutateAsync(data).catch((err) => console.log(err));
useEffect(() => {
if (login.isSuccess) {
push("/dash")
}
- }, [login.isSuccess])
+ }, [login.isSuccess]);
return (
<>
diff --git a/src/server/api/routers/admin.ts b/src/server/api/routers/admin.ts
index b64ad76..e9d0af7 100644
--- a/src/server/api/routers/admin.ts
+++ b/src/server/api/routers/admin.ts
@@ -1,38 +1,40 @@
-import { compare, compareSync } from "bcrypt";
-import { z } from "zod";
+import { compareSync, hashSync } from "bcrypt";
import jwt from "jsonwebtoken";
-import { createTRPCRouter, loggedInProcedure, publicProcedure } from "~/server/api/trpc";
-import { LoginSchema, PublicUserType } from "~/utils/types";
+import { adminProcedure, createTRPCRouter, loggedInProcedure, publicProcedure } from "~/server/api/trpc";
+import { AddPeriods, AddTimePeriodSchema, AddUserSchema, ChgPasswordSchema, LoginSchema, PublicUserType } from "~/utils/types";
import { TRPCError } from "@trpc/server";
+import { z } from "zod";
+import { Period } from "@prisma/client";
+import { lastGotRfid } from "~/utils/rfid";
export const adminRouter = createTRPCRouter({
isLoggedIn: loggedInProcedure.query(() => true),
login: publicProcedure
.input(LoginSchema)
.mutation(async ({ input, ctx }) => await ctx.db.user.findUnique({
- where: {
- username: input.username,
- }
- }).then((user) => {
- const result = compareSync(input.password, user?.password || "");
- if (result) {
- const session = PublicUserType.parse(user)!;
- const token = jwt.sign(session, process.env.JWT_SECRET || "", { expiresIn: "1d" });
- ctx.res.setHeader("Set-Cookie", `token=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${60 * 60 * 24};`);
- return {
- status: "success",
- message: "Login successful.",
- };
- } else {
- throw new Error("Please check your username and password.");
- }
- }).catch((err) => {
- throw new TRPCError({
- code: "FORBIDDEN",
- message: "Please check your username and password.",
- })
+ where: {
+ username: input.username,
+ }
+ }).then((user) => {
+ const result = compareSync(input.password, user?.password || "");
+ if (result) {
+ const session = PublicUserType.parse(user)!;
+ const token = jwt.sign(session, process.env.JWT_SECRET || "", { expiresIn: "1d" });
+ ctx.res.setHeader("Set-Cookie", `token=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${60 * 60 * 24};`);
+ return {
+ status: "success",
+ message: "Login successful.",
+ };
+ } else {
+ throw new Error("Please check your username and password.");
+ }
+ }).catch((err) => {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "Please check your username and password.",
})
+ })
),
logout: loggedInProcedure
.mutation(async ({ ctx }) => {
@@ -46,4 +48,240 @@ export const adminRouter = createTRPCRouter({
.query(async ({ ctx }) => {
return ctx.session;
}),
+ chgPassword: loggedInProcedure
+ .input(ChgPasswordSchema)
+ .mutation(async ({ input, ctx }) => {
+ await ctx.db.user.findUnique({
+ where: {
+ username: ctx.session?.username,
+ }
+ }).then(async (user) => {
+ const result = compareSync(input.oldPassword, user?.password || "");
+ if (result) {
+ await ctx.db.user.update({
+ where: {
+ username: ctx.session?.username,
+ },
+ data: {
+ password: hashSync(input.newPassword, 10),
+ },
+ });
+ return {
+ status: "success",
+ message: "Password changed successfully.",
+ };
+ } else {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "Please check your old password.",
+ })
+ }
+ }).catch((err) => {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message: "Please check your old password.",
+ })
+ })
+ }),
+ getTimePeriods: loggedInProcedure
+ .query(async ({ ctx }) => {
+ return await ctx.db.timePeriod.findMany({
+ orderBy: {
+ id: "asc",
+ },
+ });
+ }),
+ addTimePeriod: adminProcedure
+ .input(AddTimePeriodSchema)
+ .mutation(async ({ input, ctx }) => {
+ return await ctx.db.timePeriod.create({
+ data: {
+ id: input.id,
+ name: input.name,
+ start: input.startTime,
+ end: input.endTime,
+ },
+ });
+ }),
+ deleteTimePeriod: adminProcedure
+ .input(z.number().int())
+ .mutation(async ({ input, ctx }) => {
+ return await ctx.db.timePeriod.delete({
+ where: {
+ id: input,
+ },
+ });
+ }),
+ getAllUsers: adminProcedure
+ .query(async ({ ctx }) => {
+ return await ctx.db.user.findMany({
+ orderBy: {
+ username: "asc",
+ },
+ select: {
+ username: true,
+ name: true,
+ grade: true,
+ class: true,
+ number: true,
+ isAdmin: true,
+ rosterOnly: true,
+ rfid: true,
+ }
+ });
+ }),
+ resetPassword: adminProcedure
+ .input(z.string())
+ .mutation(async ({ input, ctx }) => {
+ return await ctx.db.user.update({
+ where: {
+ username: input,
+ },
+ data: {
+ password: hashSync("password", 10),
+ },
+ });
+ }),
+ toggleUserIsAdmin: adminProcedure
+ .input(z.string())
+ .mutation(async ({ input, ctx }) => {
+ return await ctx.db.user.update({
+ where: {
+ username: input,
+ },
+ data: {
+ isAdmin: await ctx.db.user.findUnique({
+ where: {
+ username: input,
+ },
+ }).then((user) => {
+ return !user?.isAdmin;
+ }),
+ },
+ });
+ }),
+ toggleUserRosterOnly: adminProcedure
+ .input(z.string())
+ .mutation(async ({ input, ctx }) => {
+ return await ctx.db.user.update({
+ where: {
+ username: input,
+ },
+ data: {
+ rosterOnly: await ctx.db.user.findUnique({
+ where: {
+ username: input,
+ },
+ }).then((user) => {
+ return !user?.rosterOnly;
+ }),
+ },
+ });
+ }),
+ addUser: adminProcedure
+ .input(AddUserSchema)
+ .mutation(async ({ input, ctx }) => {
+ return await ctx.db.user.create({
+ data: {
+ username: input.username,
+ name: input.name,
+ grade: input.grade,
+ class: input.class,
+ number: input.number,
+ password: hashSync("password", 10),
+ },
+ });
+ }),
+ getPeriods: loggedInProcedure
+ .query(async ({ ctx }) => {
+ return await ctx.db.period.findMany({
+ orderBy: [{
+ date: "asc",
+ }, {
+ timePeriodId: "asc",
+ }],
+ }).then((periods) => {
+ const groupedPeriods: { [key: string]: Period[] } = {};
+ periods.forEach((period) => {
+ const utcDate = period.date.toLocaleDateString();
+ if (groupedPeriods[utcDate] === undefined) {
+ groupedPeriods[utcDate] = [];
+ }
+ groupedPeriods[utcDate]?.push(period);
+ });
+ return groupedPeriods;
+ }).catch((err) => {
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: "There was an error getting the periods.",
+ })
+ })
+ }),
+ addPeriods: adminProcedure
+ .input(AddPeriods)
+ .mutation(async ({ input, ctx }) => {
+ await ctx.db.timePeriod.findMany().then(
+ async (timePeriods) => {
+ timePeriods.forEach(async (timePeriod) => {
+ await ctx.db.period.create({
+ data: {
+ date: new Date(input.date),
+ timePeriodId: timePeriod.id,
+ },
+ });
+ });
+ }
+ ).then(() => {
+ return {
+ status: "success",
+ message: "Periods added successfully.",
+ };
+ }).catch((err) => {
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: "There was an error adding the periods.",
+ })
+ })
+ }),
+ enablePeriod: adminProcedure
+ .input(z.object({
+ date: z.string(),
+ timePeriodId: z.number().int(),
+ }))
+ .mutation(async ({ input, ctx }) => {
+ return await ctx.db.period.create({
+ data: {
+ date: new Date(input.date),
+ timePeriodId: input.timePeriodId,
+ },
+ })
+ }),
+ disablePeriod: adminProcedure
+ .input(z.number().int())
+ .mutation(async ({ input, ctx }) => {
+ return await ctx.db.period.delete({
+ where: {
+ id: input,
+ },
+ })
+ }),
+ getLastRfid: adminProcedure
+ .query(() => {
+ return lastGotRfid;
+ }),
+ setUserRfid: adminProcedure
+ .input(z.object({
+ username: z.string(),
+ rfid: z.string(),
+ }))
+ .mutation(async ({ input, ctx }) => {
+ return await ctx.db.user.update({
+ where: {
+ username: input.username,
+ },
+ data: {
+ rfid: input.rfid,
+ },
+ })
+ }),
});
diff --git a/src/utils/config.ts b/src/utils/config.ts
new file mode 100644
index 0000000..fec1723
--- /dev/null
+++ b/src/utils/config.ts
@@ -0,0 +1,3 @@
+export const config = {
+ latestSignup: "2024-01-01T00:00:00.000Z",
+}
\ No newline at end of file
diff --git a/src/utils/rfid.ts b/src/utils/rfid.ts
new file mode 100644
index 0000000..5cf6ebb
--- /dev/null
+++ b/src/utils/rfid.ts
@@ -0,0 +1,4 @@
+export let lastGotRfid = "";
+export const setLastRfid = (uid: string) => {
+ lastGotRfid = uid;
+};
\ No newline at end of file
diff --git a/src/utils/types.ts b/src/utils/types.ts
index 9a44d8f..527fa68 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -6,9 +6,34 @@ export const PublicUserType = z.object({
name: z.string(),
username: z.string(),
isAdmin: z.boolean(),
+ rosterOnly: z.boolean().default(false),
}).or(z.undefined());
export const LoginSchema = z.object({
username: z.string().min(1, { message: "Username cannot be empty." }),
password: z.string(),
+})
+
+export const ChgPasswordSchema = z.object({
+ oldPassword: z.string().min(1, { message: "Old password cannot be empty." }),
+ newPassword: z.string().min(8, { message: "New password must be at least 8 characters long." }),
+})
+
+export const AddTimePeriodSchema = z.object({
+ id: z.coerce.number().int({ message: "ID must be an integer." }).min(1, { message: "ID must be greater than 0." }).max(999, { message: "ID must be less than 1000." }),
+ name: z.string().min(1, { message: "Name cannot be empty." }),
+ startTime: z.string().regex(/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/, { message: "Start time must be in the format HH:MM."}),
+ endTime: z.string().regex(/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/, { message: "End time must be in the format HH:MM."}),
+})
+
+export const AddUserSchema = z.object({
+ username: z.string().min(1, { message: "Username cannot be empty." }),
+ name: z.string().min(1, { message: "Name cannot be empty." }),
+ grade: z.coerce.number().int().nullable(),
+ class: z.string().nullable(),
+ number: z.coerce.number().int().nullable(),
+})
+
+export const AddPeriods = z.object({
+ date: z.string().regex(/^20[2-3][0-9]\/(0?[1-9]|1[0-2]){1}\/(0?[1-9]|1[0-9]|2[0-9]|3[0-1]){1}$/, { message: "Periods must be in the format YYYY/MM/DD."})
})
\ No newline at end of file