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 (
+
+
+
+

Current time periods:

+ + + + + + + + + + + + { + timePeriods.data?.map((period) => ( + + + + + + + + )) + } + + + + + + + + +
IDNameStartEndOperation
{period.id}{period.name}{period.start}{period.end} + +
+ + + + + + + + + +
+ {errors.id &&

{errors.id.message}

} + {errors.startTime &&

{errors.startTime.message}

} + {errors.endTime &&

{errors.endTime.message}

} + {errors.name &&

{errors.name.message}

} +
+
+
+ {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 (
+ + + + + { + timePeriods.data?.map((timePeriods) => { + return () + }) + } + + + + { + Object.keys(periods.data || {}).map((date) => { + periodCnt = 0; + return ( + + + { + timePeriods.data?.map((timePeriod) => { + const thisPeriodCnt = periodCnt; + if (periods.data![date]![periodCnt]?.timePeriodId == timePeriod.id) { + periodCnt++; + return () + } else { + return () + } + }) + } + + ) + }) + } + +
Date{timePeriods.name} ({timePeriods.start} ~ {timePeriods.end})
{date} disablePeriod.mutateAsync(periods.data![date]![thisPeriodCnt]!.id!).then(() => periods.refetch())}> + Disable + enablePeriod.mutateAsync({ + date: date, + timePeriodId: periodsDataId[thisPeriodCnt]! + }).then(() => periods.refetch())}> + Enable +
+
+

Add Date

+
+
+
+ + + {errors.date && {errors.date.message}} +
+ +
+ {addPeriods.error &&

{addPeriods.error.message}

} + {addPeriods.isSuccess &&

Add periods success!

} +
+
) +} \ 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 (
+
+
+ + + + + + + + + + + + + + + + { + users.data?.map((user) => ( + + + + + + + + + + + + )) + } + +
UsernameGradeClassNumberNameRFIDAdmin?Roster Only?Operation
{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

+
+
+ + + {errors.username && {errors.username.message}} +
+
+ + + {errors.grade && {errors.grade.message}} +
+
+ + + {errors.class && {errors.class.message}} +
+
+ + + {errors.number && {errors.number.message}} +
+
+ + + {errors.name && {errors.name.message}} +
+ +

Default password is password.

+
+ {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 + + + +
+ +
+ + + + + + + + + + + + + + { + users.data?.map((user) => ( + + + + + + + + + + )) + } + +
UsernameGradeClassNumberNameRFIDOperation
{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 + + + +
+ +
+
+
+ + + {errors.oldPassword && {errors.oldPassword.message}} +
+
+ + + {errors.newPassword && {errors.newPassword.message}} +
+ {chgPassword.isError &&
{chgPassword.error.message}
} + {chgPassword.isSuccess &&
Success! Please sign in again...
} + +
+
+
+ ) +} \ 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