diff --git a/drizzle/0008_fixed_pestilence.sql b/drizzle/0008_fixed_pestilence.sql
new file mode 100644
index 0000000..9cbd8d3
--- /dev/null
+++ b/drizzle/0008_fixed_pestilence.sql
@@ -0,0 +1,23 @@
+CREATE TABLE IF NOT EXISTS "university" (
+ "id" serial PRIMARY KEY NOT NULL,
+ "name" text NOT NULL,
+ CONSTRAINT "university_name_unique" UNIQUE("name")
+);
+--> statement-breakpoint
+ALTER TABLE "person" DROP CONSTRAINT "person_identifier_unique";--> statement-breakpoint
+ALTER TABLE "person" ALTER COLUMN "department" DROP NOT NULL;--> statement-breakpoint
+ALTER TABLE "person" ADD COLUMN "university" text;--> statement-breakpoint
+ALTER TABLE "person_entry" ADD COLUMN "guarantor_id" integer;--> statement-breakpoint
+DO $$ BEGIN
+ ALTER TABLE "person" ADD CONSTRAINT "person_university_university_name_fk" FOREIGN KEY ("university") REFERENCES "public"."university"("name") ON DELETE restrict ON UPDATE cascade;
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+--> statement-breakpoint
+DO $$ BEGIN
+ ALTER TABLE "person_entry" ADD CONSTRAINT "person_entry_guarantor_id_person_id_fk" FOREIGN KEY ("guarantor_id") REFERENCES "public"."person"("id") ON DELETE restrict ON UPDATE cascade;
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+--> statement-breakpoint
+ALTER TABLE "person" ADD CONSTRAINT "person_identifier_university_id_unique" UNIQUE NULLS NOT DISTINCT("identifier","university");
\ No newline at end of file
diff --git a/drizzle/0009_crazy_colossus.sql b/drizzle/0009_crazy_colossus.sql
new file mode 100644
index 0000000..3580e1f
--- /dev/null
+++ b/drizzle/0009_crazy_colossus.sql
@@ -0,0 +1,8 @@
+ALTER TABLE "ratelimit" DROP CONSTRAINT "ratelimit_user_id_user_id_fk";
+--> statement-breakpoint
+ALTER TABLE "ratelimit" ADD CONSTRAINT "ratelimit_user_id_timestamp_pk" PRIMARY KEY("user_id","timestamp");--> statement-breakpoint
+DO $$ BEGIN
+ ALTER TABLE "ratelimit" ADD CONSTRAINT "ratelimit_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE cascade;
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
diff --git a/drizzle/meta/0008_snapshot.json b/drizzle/meta/0008_snapshot.json
new file mode 100644
index 0000000..eeb66e7
--- /dev/null
+++ b/drizzle/meta/0008_snapshot.json
@@ -0,0 +1,454 @@
+{
+ "id": "1e9e2df8-44b5-4721-b653-7db0b99c5a4e",
+ "prevId": "81ee13a7-fe6d-4b61-828e-edba465e15b5",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.building": {
+ "name": "building",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "building_name_unique": {
+ "name": "building_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["name"]
+ }
+ }
+ },
+ "public.department": {
+ "name": "department",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "department_name_unique": {
+ "name": "department_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["name"]
+ }
+ }
+ },
+ "public.person": {
+ "name": "person",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "fname": {
+ "name": "fname",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "lname": {
+ "name": "lname",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "department": {
+ "name": "department",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "university": {
+ "name": "university",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "person_department_department_name_fk": {
+ "name": "person_department_department_name_fk",
+ "tableFrom": "person",
+ "tableTo": "department",
+ "columnsFrom": ["department"],
+ "columnsTo": ["name"],
+ "onDelete": "restrict",
+ "onUpdate": "cascade"
+ },
+ "person_university_university_name_fk": {
+ "name": "person_university_university_name_fk",
+ "tableFrom": "person",
+ "tableTo": "university",
+ "columnsFrom": ["university"],
+ "columnsTo": ["name"],
+ "onDelete": "restrict",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "person_identifier_university_id_unique": {
+ "name": "person_identifier_university_id_unique",
+ "nullsNotDistinct": true,
+ "columns": ["identifier", "university"]
+ }
+ }
+ },
+ "public.person_entry": {
+ "name": "person_entry",
+ "schema": "",
+ "columns": {
+ "person_id": {
+ "name": "person_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "timestamp": {
+ "name": "timestamp",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "building": {
+ "name": "building",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "creator": {
+ "name": "creator",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "guarantor_id": {
+ "name": "guarantor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "person_entry_person_id_person_id_fk": {
+ "name": "person_entry_person_id_person_id_fk",
+ "tableFrom": "person_entry",
+ "tableTo": "person",
+ "columnsFrom": ["person_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "person_entry_building_building_name_fk": {
+ "name": "person_entry_building_building_name_fk",
+ "tableFrom": "person_entry",
+ "tableTo": "building",
+ "columnsFrom": ["building"],
+ "columnsTo": ["name"],
+ "onDelete": "restrict",
+ "onUpdate": "cascade"
+ },
+ "person_entry_creator_user_username_fk": {
+ "name": "person_entry_creator_user_username_fk",
+ "tableFrom": "person_entry",
+ "tableTo": "user",
+ "columnsFrom": ["creator"],
+ "columnsTo": ["username"],
+ "onDelete": "no action",
+ "onUpdate": "cascade"
+ },
+ "person_entry_guarantor_id_person_id_fk": {
+ "name": "person_entry_guarantor_id_person_id_fk",
+ "tableFrom": "person_entry",
+ "tableTo": "person",
+ "columnsFrom": ["guarantor_id"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "person_entry_person_id_timestamp_pk": {
+ "name": "person_entry_person_id_timestamp_pk",
+ "columns": ["person_id", "timestamp"]
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "public.person_exit": {
+ "name": "person_exit",
+ "schema": "",
+ "columns": {
+ "person_id": {
+ "name": "person_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "timestamp": {
+ "name": "timestamp",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "building": {
+ "name": "building",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "creator": {
+ "name": "creator",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "person_exit_person_id_person_id_fk": {
+ "name": "person_exit_person_id_person_id_fk",
+ "tableFrom": "person_exit",
+ "tableTo": "person",
+ "columnsFrom": ["person_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "person_exit_building_building_name_fk": {
+ "name": "person_exit_building_building_name_fk",
+ "tableFrom": "person_exit",
+ "tableTo": "building",
+ "columnsFrom": ["building"],
+ "columnsTo": ["name"],
+ "onDelete": "restrict",
+ "onUpdate": "cascade"
+ },
+ "person_exit_creator_user_username_fk": {
+ "name": "person_exit_creator_user_username_fk",
+ "tableFrom": "person_exit",
+ "tableTo": "user",
+ "columnsFrom": ["creator"],
+ "columnsTo": ["username"],
+ "onDelete": "no action",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "person_exit_person_id_timestamp_pk": {
+ "name": "person_exit_person_id_timestamp_pk",
+ "columns": ["person_id", "timestamp"]
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "public.session": {
+ "name": "session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "building": {
+ "name": "building",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "timestamp": {
+ "name": "timestamp",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "session_building_building_name_fk": {
+ "name": "session_building_building_name_fk",
+ "tableFrom": "session",
+ "tableTo": "building",
+ "columnsFrom": ["building"],
+ "columnsTo": ["name"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "public.university": {
+ "name": "university",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "university_name_unique": {
+ "name": "university_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["name"]
+ }
+ }
+ },
+ "public.ratelimit": {
+ "name": "ratelimit",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "timestamp": {
+ "name": "timestamp",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "lock": {
+ "name": "lock",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "ratelimit_user_id_user_id_fk": {
+ "name": "ratelimit_user_id_user_id_fk",
+ "tableFrom": "ratelimit",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "public.user": {
+ "name": "user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_username_unique": {
+ "name": "user_username_unique",
+ "nullsNotDistinct": false,
+ "columns": ["username"]
+ }
+ }
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/drizzle/meta/0009_snapshot.json b/drizzle/meta/0009_snapshot.json
new file mode 100644
index 0000000..91aeb5d
--- /dev/null
+++ b/drizzle/meta/0009_snapshot.json
@@ -0,0 +1,459 @@
+{
+ "id": "a3bde6c5-8f88-4006-9c29-982c51f2e436",
+ "prevId": "1e9e2df8-44b5-4721-b653-7db0b99c5a4e",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.building": {
+ "name": "building",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "building_name_unique": {
+ "name": "building_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["name"]
+ }
+ }
+ },
+ "public.department": {
+ "name": "department",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "department_name_unique": {
+ "name": "department_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["name"]
+ }
+ }
+ },
+ "public.person": {
+ "name": "person",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "fname": {
+ "name": "fname",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "lname": {
+ "name": "lname",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "department": {
+ "name": "department",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "university": {
+ "name": "university",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "person_department_department_name_fk": {
+ "name": "person_department_department_name_fk",
+ "tableFrom": "person",
+ "tableTo": "department",
+ "columnsFrom": ["department"],
+ "columnsTo": ["name"],
+ "onDelete": "restrict",
+ "onUpdate": "cascade"
+ },
+ "person_university_university_name_fk": {
+ "name": "person_university_university_name_fk",
+ "tableFrom": "person",
+ "tableTo": "university",
+ "columnsFrom": ["university"],
+ "columnsTo": ["name"],
+ "onDelete": "restrict",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "person_identifier_university_id_unique": {
+ "name": "person_identifier_university_id_unique",
+ "nullsNotDistinct": true,
+ "columns": ["identifier", "university"]
+ }
+ }
+ },
+ "public.person_entry": {
+ "name": "person_entry",
+ "schema": "",
+ "columns": {
+ "person_id": {
+ "name": "person_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "timestamp": {
+ "name": "timestamp",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "building": {
+ "name": "building",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "creator": {
+ "name": "creator",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "guarantor_id": {
+ "name": "guarantor_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "person_entry_person_id_person_id_fk": {
+ "name": "person_entry_person_id_person_id_fk",
+ "tableFrom": "person_entry",
+ "tableTo": "person",
+ "columnsFrom": ["person_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "person_entry_building_building_name_fk": {
+ "name": "person_entry_building_building_name_fk",
+ "tableFrom": "person_entry",
+ "tableTo": "building",
+ "columnsFrom": ["building"],
+ "columnsTo": ["name"],
+ "onDelete": "restrict",
+ "onUpdate": "cascade"
+ },
+ "person_entry_creator_user_username_fk": {
+ "name": "person_entry_creator_user_username_fk",
+ "tableFrom": "person_entry",
+ "tableTo": "user",
+ "columnsFrom": ["creator"],
+ "columnsTo": ["username"],
+ "onDelete": "no action",
+ "onUpdate": "cascade"
+ },
+ "person_entry_guarantor_id_person_id_fk": {
+ "name": "person_entry_guarantor_id_person_id_fk",
+ "tableFrom": "person_entry",
+ "tableTo": "person",
+ "columnsFrom": ["guarantor_id"],
+ "columnsTo": ["id"],
+ "onDelete": "restrict",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "person_entry_person_id_timestamp_pk": {
+ "name": "person_entry_person_id_timestamp_pk",
+ "columns": ["person_id", "timestamp"]
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "public.person_exit": {
+ "name": "person_exit",
+ "schema": "",
+ "columns": {
+ "person_id": {
+ "name": "person_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "timestamp": {
+ "name": "timestamp",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "building": {
+ "name": "building",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "creator": {
+ "name": "creator",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "person_exit_person_id_person_id_fk": {
+ "name": "person_exit_person_id_person_id_fk",
+ "tableFrom": "person_exit",
+ "tableTo": "person",
+ "columnsFrom": ["person_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ },
+ "person_exit_building_building_name_fk": {
+ "name": "person_exit_building_building_name_fk",
+ "tableFrom": "person_exit",
+ "tableTo": "building",
+ "columnsFrom": ["building"],
+ "columnsTo": ["name"],
+ "onDelete": "restrict",
+ "onUpdate": "cascade"
+ },
+ "person_exit_creator_user_username_fk": {
+ "name": "person_exit_creator_user_username_fk",
+ "tableFrom": "person_exit",
+ "tableTo": "user",
+ "columnsFrom": ["creator"],
+ "columnsTo": ["username"],
+ "onDelete": "no action",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "person_exit_person_id_timestamp_pk": {
+ "name": "person_exit_person_id_timestamp_pk",
+ "columns": ["person_id", "timestamp"]
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "public.session": {
+ "name": "session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "building": {
+ "name": "building",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "timestamp": {
+ "name": "timestamp",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "session_building_building_name_fk": {
+ "name": "session_building_building_name_fk",
+ "tableFrom": "session",
+ "tableTo": "building",
+ "columnsFrom": ["building"],
+ "columnsTo": ["name"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {}
+ },
+ "public.university": {
+ "name": "university",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "university_name_unique": {
+ "name": "university_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["name"]
+ }
+ }
+ },
+ "public.ratelimit": {
+ "name": "ratelimit",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "timestamp": {
+ "name": "timestamp",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "lock": {
+ "name": "lock",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "ratelimit_user_id_user_id_fk": {
+ "name": "ratelimit_user_id_user_id_fk",
+ "tableFrom": "ratelimit",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {
+ "ratelimit_user_id_timestamp_pk": {
+ "name": "ratelimit_user_id_timestamp_pk",
+ "columns": ["user_id", "timestamp"]
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "public.user": {
+ "name": "user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_username_unique": {
+ "name": "user_username_unique",
+ "nullsNotDistinct": false,
+ "columns": ["username"]
+ }
+ }
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index 04b1183..e2746d9 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -57,6 +57,20 @@
"when": 1735856957839,
"tag": "0007_aspiring_zombie",
"breakpoints": true
+ },
+ {
+ "idx": 8,
+ "version": "7",
+ "when": 1737294850034,
+ "tag": "0008_fixed_pestilence",
+ "breakpoints": true
+ },
+ {
+ "idx": 9,
+ "version": "7",
+ "when": 1737296923867,
+ "tag": "0009_crazy_colossus",
+ "breakpoints": true
}
]
}
diff --git a/eslint.config.js b/eslint.config.js
index aa5987f..1e9e12f 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -30,5 +30,10 @@ export default ts.config(
parser: ts.parser
}
}
+ },
+ {
+ rules: {
+ eqeqeq: ['error', 'always']
+ }
}
);
diff --git a/src/app.d.ts b/src/app.d.ts
index 5de50c2..0398b6e 100644
--- a/src/app.d.ts
+++ b/src/app.d.ts
@@ -1,6 +1,6 @@
import type { Database } from '$lib/server/db/connect';
import type { User } from '$lib/server/db/schema/user';
-import type { Session } from '$lib/server/db/schema/session';
+import type { Session } from '$lib/types/db';
declare global {
namespace App {
diff --git a/src/lib/components/custom/sidebar/admin-sidebar.svelte b/src/lib/components/custom/sidebar/admin-sidebar.svelte
index bd4ae7f..cd3bf12 100644
--- a/src/lib/components/custom/sidebar/admin-sidebar.svelte
+++ b/src/lib/components/custom/sidebar/admin-sidebar.svelte
@@ -11,6 +11,7 @@
import { useSidebar } from '$lib/components/ui/sidebar/context.svelte.js';
import LogoLight from '$lib/assets/images/light.svg';
import LogoDark from '$lib/assets/images/dark.svg';
+ import { University } from 'lucide-svelte';
// Menu items.
const items = [
@@ -19,15 +20,20 @@
url: '/',
icon: House
},
+ {
+ title: 'Create Building',
+ url: '/admin/create/building',
+ icon: Building
+ },
{
title: 'Create Department',
url: '/admin/create/department',
icon: Cuboid
},
{
- title: 'Create Building',
- url: '/admin/create/building',
- icon: Building
+ title: 'Create University',
+ url: '/admin/create/university',
+ icon: University
},
{
title: 'Register User',
diff --git a/src/lib/components/custom/sidebar/app-sidebar.svelte b/src/lib/components/custom/sidebar/app-sidebar.svelte
index dbf1be8..5704776 100644
--- a/src/lib/components/custom/sidebar/app-sidebar.svelte
+++ b/src/lib/components/custom/sidebar/app-sidebar.svelte
@@ -11,6 +11,7 @@
ChartNoAxesCombined,
ClipboardList,
Github,
+ Handshake,
House
} from 'lucide-svelte';
@@ -31,6 +32,11 @@
url: '/create/employee',
icon: Briefcase
},
+ {
+ title: 'Create Guest',
+ url: '/create/guest',
+ icon: Handshake
+ },
{
title: 'Statistics',
url: '/statistics',
diff --git a/src/lib/components/ui/command/command-dialog.svelte b/src/lib/components/ui/command/command-dialog.svelte
new file mode 100644
index 0000000..bacb091
--- /dev/null
+++ b/src/lib/components/ui/command/command-dialog.svelte
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
diff --git a/src/lib/components/ui/command/command-empty.svelte b/src/lib/components/ui/command/command-empty.svelte
new file mode 100644
index 0000000..5359600
--- /dev/null
+++ b/src/lib/components/ui/command/command-empty.svelte
@@ -0,0 +1,12 @@
+
+
+
diff --git a/src/lib/components/ui/command/command-group.svelte b/src/lib/components/ui/command/command-group.svelte
new file mode 100644
index 0000000..fd159c6
--- /dev/null
+++ b/src/lib/components/ui/command/command-group.svelte
@@ -0,0 +1,27 @@
+
+
+
+ {#if heading}
+
+ {heading}
+
+ {/if}
+
+
diff --git a/src/lib/components/ui/command/command-input.svelte b/src/lib/components/ui/command/command-input.svelte
new file mode 100644
index 0000000..e034215
--- /dev/null
+++ b/src/lib/components/ui/command/command-input.svelte
@@ -0,0 +1,25 @@
+
+
+
+
+
+
diff --git a/src/lib/components/ui/command/command-item.svelte b/src/lib/components/ui/command/command-item.svelte
new file mode 100644
index 0000000..edd265d
--- /dev/null
+++ b/src/lib/components/ui/command/command-item.svelte
@@ -0,0 +1,19 @@
+
+
+
diff --git a/src/lib/components/ui/command/command-link-item.svelte b/src/lib/components/ui/command/command-link-item.svelte
new file mode 100644
index 0000000..e608ab5
--- /dev/null
+++ b/src/lib/components/ui/command/command-link-item.svelte
@@ -0,0 +1,19 @@
+
+
+
diff --git a/src/lib/components/ui/command/command-list.svelte b/src/lib/components/ui/command/command-list.svelte
new file mode 100644
index 0000000..a861889
--- /dev/null
+++ b/src/lib/components/ui/command/command-list.svelte
@@ -0,0 +1,16 @@
+
+
+
diff --git a/src/lib/components/ui/command/command-separator.svelte b/src/lib/components/ui/command/command-separator.svelte
new file mode 100644
index 0000000..b9224d6
--- /dev/null
+++ b/src/lib/components/ui/command/command-separator.svelte
@@ -0,0 +1,12 @@
+
+
+
diff --git a/src/lib/components/ui/command/command-shortcut.svelte b/src/lib/components/ui/command/command-shortcut.svelte
new file mode 100644
index 0000000..ace2d5f
--- /dev/null
+++ b/src/lib/components/ui/command/command-shortcut.svelte
@@ -0,0 +1,20 @@
+
+
+
+ {@render children?.()}
+
diff --git a/src/lib/components/ui/command/command.svelte b/src/lib/components/ui/command/command.svelte
new file mode 100644
index 0000000..58642ab
--- /dev/null
+++ b/src/lib/components/ui/command/command.svelte
@@ -0,0 +1,21 @@
+
+
+
diff --git a/src/lib/components/ui/command/index.ts b/src/lib/components/ui/command/index.ts
new file mode 100644
index 0000000..dee2364
--- /dev/null
+++ b/src/lib/components/ui/command/index.ts
@@ -0,0 +1,40 @@
+import { Command as CommandPrimitive } from 'bits-ui';
+
+import Root from './command.svelte';
+import Dialog from './command-dialog.svelte';
+import Empty from './command-empty.svelte';
+import Group from './command-group.svelte';
+import Item from './command-item.svelte';
+import Input from './command-input.svelte';
+import List from './command-list.svelte';
+import Separator from './command-separator.svelte';
+import Shortcut from './command-shortcut.svelte';
+import LinkItem from './command-link-item.svelte';
+
+const Loading = CommandPrimitive.Loading;
+
+export {
+ Root,
+ Dialog,
+ Empty,
+ Group,
+ Item,
+ LinkItem,
+ Input,
+ List,
+ Separator,
+ Shortcut,
+ Loading,
+ //
+ Root as Command,
+ Dialog as CommandDialog,
+ Empty as CommandEmpty,
+ Group as CommandGroup,
+ Item as CommandItem,
+ LinkItem as CommandLinkItem,
+ Input as CommandInput,
+ List as CommandList,
+ Separator as CommandSeparator,
+ Shortcut as CommandShortcut,
+ Loading as CommandLoading
+};
diff --git a/src/lib/components/ui/popover/index.ts b/src/lib/components/ui/popover/index.ts
new file mode 100644
index 0000000..5db432e
--- /dev/null
+++ b/src/lib/components/ui/popover/index.ts
@@ -0,0 +1,17 @@
+import { Popover as PopoverPrimitive } from 'bits-ui';
+import Content from './popover-content.svelte';
+const Root = PopoverPrimitive.Root;
+const Trigger = PopoverPrimitive.Trigger;
+const Close = PopoverPrimitive.Close;
+
+export {
+ Root,
+ Content,
+ Trigger,
+ Close,
+ //
+ Root as Popover,
+ Content as PopoverContent,
+ Trigger as PopoverTrigger,
+ Close as PopoverClose
+};
diff --git a/src/lib/components/ui/popover/popover-content.svelte b/src/lib/components/ui/popover/popover-content.svelte
new file mode 100644
index 0000000..7265cc3
--- /dev/null
+++ b/src/lib/components/ui/popover/popover-content.svelte
@@ -0,0 +1,28 @@
+
+
+
+
+
diff --git a/src/lib/server/db/department.ts b/src/lib/server/db/department.ts
index 3e38b75..0bddb0c 100644
--- a/src/lib/server/db/department.ts
+++ b/src/lib/server/db/department.ts
@@ -3,7 +3,7 @@ import type { Department } from '$lib/types/db';
import { department } from './schema/department';
export async function getDepartments(db: Database): Promise {
- return await db.select().from(department);
+ return await db.select().from(department).orderBy(department.name);
}
export async function createDepartment(db: Database, name: string): Promise {
diff --git a/src/lib/server/db/person.ts b/src/lib/server/db/person.ts
index dd1bdbc..e7db9f7 100644
--- a/src/lib/server/db/person.ts
+++ b/src/lib/server/db/person.ts
@@ -1,5 +1,5 @@
import type { Database } from './connect';
-import { or, eq, and, max, gt, count, isNull } from 'drizzle-orm';
+import { or, eq, and, max, gt, count, isNull, not } from 'drizzle-orm';
import { person, personEntry, personExit } from './schema/person';
import { StateInside, StateOutside, type State } from '$lib/types/state';
import { fuzzySearchFilters } from './fuzzysearch';
@@ -7,39 +7,48 @@ import { sqlConcat, sqlLeast, sqlLevenshteinDistance } from './utils';
import { isInside } from '../isInside';
import { capitalizeString, sanitizeString } from '$lib/utils/sanitize';
import { building } from './schema/building';
-import { isPersonType, type PersonType } from '$lib/types/person';
+import {
+ Employee,
+ Guest,
+ isPersonType,
+ Student,
+ type Person,
+ type PersonType
+} from '$lib/types/person';
+
+type searchOptions = {
+ searchQuery?: string;
+ nonGuestsOnly?: boolean;
+};
// Gets all persons using optional filters
export async function getPersons(
db: Database,
limit: number,
offset: number,
- searchQuery?: string
-): Promise<
- {
- id: number;
- identifier: string;
- type: PersonType;
- fname: string;
- lname: string;
- department: string;
- building: string | null;
- state: State;
- }[]
-> {
+ opts: searchOptions = {}
+): Promise {
// Assert limit is valid
- if (limit === null || limit === undefined || limit <= 0) {
- throw new Error('Invalid limit');
+ if (limit <= 0) {
+ throw new Error('Invalid limit (negative or zero)');
+ }
+
+ // Assert offset is valid
+ if (offset < 0) {
+ throw new Error('Invalid offset (negative)');
}
// Don't search if the search query is empty when trimmed
- const sanitizedSearchQuery = searchQuery ? sanitizeString(searchQuery) : undefined;
+ const sanitizedSearchQuery = opts.searchQuery ? sanitizeString(opts.searchQuery) : undefined;
const nonEmptySearchQuery = sanitizedSearchQuery
? sanitizedSearchQuery !== ''
? sanitizedSearchQuery
: undefined
: undefined;
+ // Wether to search for persons that aren't of type "Guest"
+ const nonGuestsOnly = opts.nonGuestsOnly ?? false;
+
try {
const maxEntrySubquery = db
.select({
@@ -60,8 +69,10 @@ export async function getPersons(
fname: person.fname,
lname: person.lname,
department: person.department,
+ university: person.university,
entryTimestamp: maxEntrySubquery.maxEntryTimestamp,
entryBuilding: personEntry.building,
+ entryGuarantorId: personEntry.guarantorId,
exitTimestamp: max(personExit.timestamp),
leastDistance: sqlLeast([
sqlLevenshteinDistance(sqlConcat([person.identifier]), nonEmptySearchQuery),
@@ -92,18 +103,21 @@ export async function getPersons(
)
.leftJoin(personExit, eq(person.id, personExit.personId))
.where(
- or(
- ...[
- ...fuzzySearchFilters([person.identifier], nonEmptySearchQuery),
- ...fuzzySearchFilters([person.fname], nonEmptySearchQuery, { distance: 5 }),
- ...fuzzySearchFilters([person.lname], nonEmptySearchQuery, { distance: 5 }),
- ...fuzzySearchFilters([person.fname, person.lname], nonEmptySearchQuery, {
- distance: 6
- }),
- ...fuzzySearchFilters([person.lname, person.fname], nonEmptySearchQuery, {
- distance: 6
- })
- ]
+ and(
+ nonGuestsOnly ? not(eq(person.type, Guest)) : undefined,
+ or(
+ ...[
+ ...fuzzySearchFilters([person.identifier], nonEmptySearchQuery),
+ ...fuzzySearchFilters([person.fname], nonEmptySearchQuery, { distance: 5 }),
+ ...fuzzySearchFilters([person.lname], nonEmptySearchQuery, { distance: 5 }),
+ ...fuzzySearchFilters([person.fname, person.lname], nonEmptySearchQuery, {
+ distance: 6
+ }),
+ ...fuzzySearchFilters([person.lname, person.fname], nonEmptySearchQuery, {
+ distance: 6
+ })
+ ]
+ )
)
)
.groupBy(
@@ -113,8 +127,10 @@ export async function getPersons(
person.fname,
person.lname,
person.department,
+ person.university,
maxEntrySubquery.maxEntryTimestamp,
- personEntry.building
+ personEntry.building,
+ personEntry.guarantorId
)
.orderBy(({ leastDistance, leastDistanceIdentifier, identifier }) => [
leastDistance,
@@ -131,8 +147,10 @@ export async function getPersons(
fname: person.fname,
lname: person.lname,
department: person.department,
+ university: person.university,
entryTimestamp: maxEntrySubquery.maxEntryTimestamp,
entryBuilding: personEntry.building,
+ entryGuarantorId: personEntry.guarantorId,
exitTimestamp: max(personExit.timestamp)
})
.from(person)
@@ -145,6 +163,7 @@ export async function getPersons(
)
)
.leftJoin(personExit, eq(person.id, personExit.personId))
+ .where(nonGuestsOnly ? not(eq(person.type, Guest)) : undefined)
.groupBy(
person.id,
person.identifier,
@@ -152,23 +171,32 @@ export async function getPersons(
person.fname,
person.lname,
person.department,
+ person.university,
maxEntrySubquery.maxEntryTimestamp,
- personEntry.building
+ personEntry.building,
+ personEntry.guarantorId
)
.orderBy(({ identifier }) => [identifier])
.limit(limit)
.offset(offset);
- return persons.map((s) => {
+ return persons.map((p) => {
+ if (!isPersonType(p.type)) {
+ throw new Error('Invalid type from DB (not PersonType)');
+ }
+
+ const inside = isInside(p.entryTimestamp, p.exitTimestamp);
return {
- id: s.id,
- identifier: s.identifier,
- type: s.type as PersonType,
- fname: s.fname,
- lname: s.lname,
- department: s.department,
- building: isInside(s.entryTimestamp, s.exitTimestamp) ? s.entryBuilding : null,
- state: isInside(s.entryTimestamp, s.exitTimestamp) ? StateInside : StateOutside
+ id: p.id,
+ identifier: p.identifier,
+ type: p.type,
+ fname: p.fname,
+ lname: p.lname,
+ department: p.department,
+ university: p.university,
+ building: inside ? p.entryBuilding : null,
+ guarantorId: inside ? p.entryGuarantorId : null,
+ state: inside ? StateInside : StateOutside
};
});
} catch (err: unknown) {
@@ -176,6 +204,39 @@ export async function getPersons(
}
}
+// Gets the count of all persons per type
+export async function getPersonsCountPerType(db: Database): Promise<
+ {
+ type: PersonType;
+ count: number;
+ }[]
+> {
+ try {
+ const persons = await db
+ .select({
+ type: person.type,
+ count: count()
+ })
+ .from(person)
+ .groupBy(person.type)
+ .orderBy(({ count }) => count);
+
+ return persons.map((p) => {
+ if (p.type !== null && !isPersonType(p.type)) {
+ throw new Error('Invalid type from DB (not PersonType)');
+ }
+ return {
+ type: p.type,
+ count: p.count
+ };
+ });
+ } catch (err: unknown) {
+ throw new Error(
+ `Failed to get count of persons per type from database: ${(err as Error).message}}`
+ );
+ }
+}
+
// Gets the count of all persons per department
export async function getPersonsCountPerDepartment(db: Database): Promise<
{
@@ -192,7 +253,9 @@ export async function getPersonsCountPerDepartment(db: Database): Promise<
count: count()
})
.from(person)
- .groupBy(person.type, person.department);
+ .where(not(eq(person.type, Guest)))
+ .groupBy(person.type, person.department)
+ .orderBy(({ count }) => count);
return persons.map((p) => {
if (p.type !== null && !isPersonType(p.type)) {
@@ -200,12 +263,51 @@ export async function getPersonsCountPerDepartment(db: Database): Promise<
}
return {
type: p.type,
- department: p.department,
+ department: p.department ?? 'None',
+ count: p.count
+ };
+ });
+ } catch (err: unknown) {
+ throw new Error(
+ `Failed to get count of persons per department from database: ${(err as Error).message}}`
+ );
+ }
+}
+
+// Gets the count of all persons per university
+export async function getPersonsCountPerUniversity(db: Database): Promise<
+ {
+ type: PersonType;
+ university: string;
+ count: number;
+ }[]
+> {
+ try {
+ const persons = await db
+ .select({
+ type: person.type,
+ university: person.university,
+ count: count()
+ })
+ .from(person)
+ .where(eq(person.type, Guest))
+ .groupBy(person.type, person.university)
+ .orderBy(({ count }) => count);
+
+ return persons.map((p) => {
+ if (p.type !== null && !isPersonType(p.type)) {
+ throw new Error('Invalid type from DB (not PersonType)');
+ }
+ return {
+ type: p.type,
+ university: p.university ?? 'None',
count: p.count
};
});
} catch (err: unknown) {
- throw new Error(`Failed to get count of persons from database: ${(err as Error).message}}`);
+ throw new Error(
+ `Failed to get count of persons per university from database: ${(err as Error).message}}`
+ );
}
}
@@ -214,7 +316,7 @@ export async function getPersonsCountPerBuilding(db: Database): Promise<
{
type: PersonType | null;
building: string;
- insideCount: number;
+ count: number;
}[]
> {
try {
@@ -239,11 +341,12 @@ export async function getPersonsCountPerBuilding(db: Database): Promise<
.select({
type: personInsideSubquery.type,
building: building.name,
- insideCount: count(personInsideSubquery.personId)
+ count: count(personInsideSubquery.personId)
})
.from(building)
.leftJoin(personInsideSubquery, eq(building.name, personInsideSubquery.entryBuilding))
- .groupBy(personInsideSubquery.type, building.name);
+ .groupBy(personInsideSubquery.type, building.name)
+ .orderBy(({ count }) => count);
return persons.map((p) => {
if (p.type !== null && !isPersonType(p.type)) {
@@ -252,16 +355,77 @@ export async function getPersonsCountPerBuilding(db: Database): Promise<
return {
type: p.type,
building: p.building,
- insideCount: p.insideCount
+ count: p.count
};
});
} catch (err: unknown) {
throw new Error(
- `Failed to get inside count of persons from database: ${(err as Error).message}}`
+ `Failed to get inside count of persons per building from database: ${(err as Error).message}}`
);
}
}
+// Creates an employee (person)
+export async function createEmployee(
+ db: Database,
+ identifier: string,
+ fname: string,
+ lname: string,
+ department: string | undefined,
+ building: string,
+ creator: string
+): Promise {
+ return await createPerson(db, identifier, Employee, fname, lname, building, creator, {
+ department
+ });
+}
+
+// Creates a student (person)
+export async function createStudent(
+ db: Database,
+ identifier: string,
+ fname: string,
+ lname: string,
+ department: string | undefined,
+ building: string,
+ creator: string
+): Promise {
+ return await createPerson(db, identifier, Student, fname, lname, building, creator, {
+ department
+ });
+}
+
+// Creates a guest (person)
+export async function createGuest(
+ db: Database,
+ identifier: string,
+ fname: string,
+ lname: string,
+ university: string | undefined,
+ building: string,
+ creator: string,
+ guarantorId: number | undefined
+): Promise {
+ return await createPerson(db, identifier, Guest, fname, lname, building, creator, {
+ university,
+ guarantorId
+ });
+}
+
+// Checks if a guarantor exists in the database and is not a guest
+async function validGuarantor(db: Database, guarantorId: number): Promise {
+ const [{ type }] = await db
+ .select({ type: person.type })
+ .from(person)
+ .where(eq(person.id, guarantorId));
+
+ if (!isPersonType(type)) {
+ throw new Error('Invalid type from DB (not PersonType)');
+ }
+
+ return type !== Guest;
+}
+
// Creates a person and the entry timestamp
export async function createPerson(
db: Database,
@@ -269,44 +433,56 @@ export async function createPerson(
type: PersonType,
fnameD: string,
lnameD: string,
- department: string,
building: string,
- creator: string
-): Promise<{
- id: number;
- identifier: string;
- type: PersonType;
- fname: string;
- lname: string;
- department: string;
- building: string;
- state: State;
-}> {
- // Assert fname, lname and identifier are valid
- if (
- identifierD === null ||
- identifierD === undefined ||
- identifierD === '' ||
- type === null ||
- type === undefined ||
- (type as string) === '' ||
- fnameD === null ||
- fnameD === undefined ||
- fnameD === '' ||
- lnameD === null ||
- lnameD === undefined ||
- lnameD === '' ||
- department === null ||
- department === undefined ||
- department === '' ||
- building === null ||
- building === undefined ||
- building === '' ||
- creator === null ||
- creator === undefined ||
- creator === ''
- ) {
- throw new Error('Invalid person data');
+ creator: string,
+ opts: {
+ department?: string;
+ university?: string;
+ guarantorId?: number;
+ }
+): Promise {
+ const department = opts.department ?? null;
+ const university = opts.university ?? null;
+ const guarantorId = opts.guarantorId ?? null;
+
+ // Assert identifier is valid
+ if (identifierD === '') {
+ throw new Error('Invalid identifier (empty)');
+ }
+
+ // Assert type is valid
+ if ((type as string) === '') {
+ throw new Error('Invalid type (empty)');
+ }
+
+ // Assert fname is valid
+ if (fnameD === '') {
+ throw new Error('Invalid fname (empty)');
+ }
+
+ // Assert lname is valid
+ if (lnameD === '') {
+ throw new Error('Invalid lname (empty)');
+ }
+
+ // Assert department is valid
+ if (department && department === '') {
+ throw new Error('Invalid department (empty)');
+ }
+
+ // Assert university is valid
+ if (university && university === '') {
+ throw new Error('Invalid university (empty)');
+ }
+
+ // Assert building is valid
+ if (building === '') {
+ throw new Error('Invalid building (empty)');
+ }
+
+ // Assert creator is valid
+ if (creator === '') {
+ throw new Error('Invalid creator (empty)');
}
const identifier = sanitizeString(identifierD);
@@ -315,17 +491,23 @@ export async function createPerson(
try {
return await db.transaction(async (tx) => {
+ // Check if the guarantor is valid
+ if (guarantorId && !validGuarantor(tx, guarantorId)) {
+ throw new Error('Guarantor not valid');
+ }
+
// Create the person
const [{ id }] = await tx
.insert(person)
- .values({ identifier, type, fname, lname, department })
+ .values({ identifier, type, fname, lname, department, university })
.returning({ id: person.id });
// Create the person entry
await tx.insert(personEntry).values({
personId: id,
building,
- creator
+ creator,
+ guarantorId
});
return {
@@ -335,7 +517,9 @@ export async function createPerson(
fname,
lname,
department,
+ university,
building,
+ guarantorId,
state: StateInside // Because the person was just created, they are inside
};
});
@@ -349,24 +533,26 @@ export async function togglePersonState(
db: Database,
id: number,
building: string,
- creator: string
+ creator: string,
+ guarantorId?: number
): Promise {
- // Assert id, building and creator are valid
- if (
- id === null ||
- id === undefined ||
- building === null ||
- building === undefined ||
- building === '' ||
- creator === null ||
- creator === undefined ||
- creator === ''
- ) {
- throw new Error('Invalid person data');
+ // Assert building is valid
+ if (building === '') {
+ throw new Error('Invalid building (empty)');
+ }
+
+ // Assert creator is valid
+ if (creator === '') {
+ throw new Error('Invalid creator (empty)');
}
try {
return await db.transaction(async (tx) => {
+ // Check if the guarantor is valid
+ if (guarantorId && !validGuarantor(tx, guarantorId)) {
+ throw new Error('Guarantor not valid');
+ }
+
// Get the person entry and exit timestamps
const [{ entryTimestamp, exitTimestamp }] = await tx
.select({
@@ -394,7 +580,8 @@ export async function togglePersonState(
await tx.insert(personEntry).values({
personId: id,
building,
- creator
+ creator,
+ guarantorId
});
return StateInside;
}
@@ -426,6 +613,16 @@ export async function removePersonsFromBuilding(
building: string,
type: string
): Promise {
+ // Assert building is valid
+ if (building === '') {
+ throw new Error('Invalid building (empty)');
+ }
+
+ // Assert type is valid
+ if (type === '') {
+ throw new Error('Invalid type (empty)');
+ }
+
try {
return await db.transaction(async (tx) => {
const personsInside = await tx
diff --git a/src/lib/server/db/schema/person.ts b/src/lib/server/db/schema/person.ts
index 7873a0b..80ae65a 100644
--- a/src/lib/server/db/schema/person.ts
+++ b/src/lib/server/db/schema/person.ts
@@ -1,18 +1,32 @@
-import { pgTable, serial, integer, text, timestamp, primaryKey } from 'drizzle-orm/pg-core';
-import { building } from './building';
+import { pgTable, serial, integer, text, timestamp, primaryKey, unique } from 'drizzle-orm/pg-core';
import { department } from './department';
+import { university } from './university';
+import { building } from './building';
import { userTable } from './user';
-export const person = pgTable('person', {
- id: serial('id').primaryKey(),
- identifier: text('identifier').notNull().unique(),
- type: text('type').notNull(),
- fname: text('fname').notNull(),
- lname: text('lname').notNull(),
- department: text('department')
- .notNull()
- .references(() => department.name, { onDelete: 'restrict', onUpdate: 'cascade' })
-});
+export const person = pgTable(
+ 'person',
+ {
+ id: serial('id').primaryKey(),
+ identifier: text('identifier').notNull(),
+ type: text('type').notNull(),
+ fname: text('fname').notNull(),
+ lname: text('lname').notNull(),
+ department: text('department').references(() => department.name, {
+ onDelete: 'restrict',
+ onUpdate: 'cascade'
+ }),
+ university: text('university').references(() => university.name, {
+ onDelete: 'restrict',
+ onUpdate: 'cascade'
+ })
+ },
+ (table) => ({
+ identifierUniversity: unique('person_identifier_university_id_unique')
+ .on(table.identifier, table.university)
+ .nullsNotDistinct()
+ })
+);
export const personEntry = pgTable(
'person_entry',
@@ -27,13 +41,15 @@ export const personEntry = pgTable(
creator: text('creator').references(() => userTable.username, {
onDelete: 'no action',
onUpdate: 'cascade'
+ }),
+ guarantorId: integer('guarantor_id').references(() => person.id, {
+ onDelete: 'restrict',
+ onUpdate: 'cascade'
})
},
- (table) => {
- return {
- pk: primaryKey({ columns: [table.personId, table.timestamp] })
- };
- }
+ (table) => ({
+ pk: primaryKey({ columns: [table.personId, table.timestamp] })
+ })
);
export const personExit = pgTable(
@@ -51,9 +67,7 @@ export const personExit = pgTable(
onUpdate: 'cascade'
})
},
- (table) => {
- return {
- pk: primaryKey({ columns: [table.personId, table.timestamp] })
- };
- }
+ (table) => ({
+ pk: primaryKey({ columns: [table.personId, table.timestamp] })
+ })
);
diff --git a/src/lib/server/db/schema/session.ts b/src/lib/server/db/schema/session.ts
index 6244a2d..3c70b57 100644
--- a/src/lib/server/db/schema/session.ts
+++ b/src/lib/server/db/schema/session.ts
@@ -1,4 +1,3 @@
-import type { InferSelectModel } from 'drizzle-orm';
import { pgTable, integer, text, timestamp } from 'drizzle-orm/pg-core';
import { userTable } from './user';
import { building } from './building';
@@ -13,5 +12,3 @@ export const sessionTable = pgTable('session', {
.references(() => building.name),
timestamp: timestamp('timestamp').defaultNow().notNull()
});
-
-export type Session = InferSelectModel;
diff --git a/src/lib/server/db/schema/university.ts b/src/lib/server/db/schema/university.ts
new file mode 100644
index 0000000..12409b7
--- /dev/null
+++ b/src/lib/server/db/schema/university.ts
@@ -0,0 +1,6 @@
+import { pgTable, serial, text } from 'drizzle-orm/pg-core';
+
+export const university = pgTable('university', {
+ id: serial('id').primaryKey(),
+ name: text('name').notNull().unique()
+});
diff --git a/src/lib/server/db/schema/user.ts b/src/lib/server/db/schema/user.ts
index 0656147..33ac227 100644
--- a/src/lib/server/db/schema/user.ts
+++ b/src/lib/server/db/schema/user.ts
@@ -1,5 +1,12 @@
-import type { InferSelectModel } from 'drizzle-orm';
-import { boolean, integer, pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
+import {
+ boolean,
+ integer,
+ pgTable,
+ primaryKey,
+ serial,
+ text,
+ timestamp
+} from 'drizzle-orm/pg-core';
export const userTable = pgTable('user', {
id: serial('id').primaryKey(),
@@ -7,12 +14,16 @@ export const userTable = pgTable('user', {
passwordHash: text('password_hash').notNull()
});
-export const ratelimitTable = pgTable('ratelimit', {
- userId: integer('user_id')
- .notNull()
- .references(() => userTable.id, { onDelete: 'cascade' }),
- timestamp: timestamp('timestamp').defaultNow().notNull(),
- lock: boolean('lock').notNull()
-});
-
-export type User = InferSelectModel;
+export const ratelimitTable = pgTable(
+ 'ratelimit',
+ {
+ userId: integer('user_id')
+ .notNull()
+ .references(() => userTable.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
+ timestamp: timestamp('timestamp').defaultNow().notNull(),
+ lock: boolean('lock').notNull()
+ },
+ (table) => ({
+ pk: primaryKey({ columns: [table.userId, table.timestamp] })
+ })
+);
diff --git a/src/lib/server/db/session.ts b/src/lib/server/db/session.ts
index 485f3a9..f828116 100644
--- a/src/lib/server/db/session.ts
+++ b/src/lib/server/db/session.ts
@@ -2,9 +2,14 @@ import type { Database } from './connect';
import { and, desc, eq, notInArray } from 'drizzle-orm';
import { encodeHexLowerCase } from '@oslojs/encoding';
import { sha256 } from '@oslojs/crypto/sha2';
-import { userTable, type User } from './schema/user';
-import { sessionTable, type Session } from './schema/session';
+import { userTable } from './schema/user';
+import { sessionTable } from './schema/session';
import { env } from '$env/dynamic/private';
+import type { Session, User } from '$lib/types/db';
+
+export type SessionValidationResult =
+ | { session: Session; user: User }
+ | { session: null; user: null };
export const inactivityTimeout = Number.parseInt(env.INACTIVITY_TIMEOUT ?? '120') * 60 * 1000;
export const maxActiveSessions = Number.parseInt(env.MAX_ACTIVE_SESSIONS ?? '3');
@@ -16,30 +21,23 @@ export async function createSession(
building: string
): Promise {
// Assert that token is valid
- if (token === null || token === undefined || token === '') {
- throw new Error('Invalid token');
- }
-
- // Assert that userId is valid
- if (userId === null || userId === undefined) {
- throw new Error('Invalid userId');
+ if (token === '') {
+ throw new Error('Invalid token (empty)');
}
// Assert that building is valid
- if (building === null || building === undefined || building === '') {
- throw new Error('Invalid token');
+ if (building === '') {
+ throw new Error('Invalid building (empty)');
}
try {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
- const result = await db
+ const [result] = await db
.insert(sessionTable)
.values({ id: sessionId, userId, building })
.returning();
- if (result.length !== 1) {
- throw new Error('Insert length is not 1');
- }
- return result[0];
+
+ return result;
} catch (err: unknown) {
throw new Error(`Failed to create session in database: ${(err as Error).message}`);
}
@@ -50,8 +48,8 @@ export async function validateSessionToken(
token: string
): Promise {
// Assert that token is valid
- if (token === null || token === undefined || token === '') {
- throw new Error('Invalid token');
+ if (token === '') {
+ throw new Error('Invalid token (empty)');
}
try {
@@ -85,8 +83,8 @@ export async function validateSessionToken(
export async function invalidateSession(db: Database, sessionId: string): Promise {
// Assert that sessionId is valid
- if (sessionId === null || sessionId === undefined || sessionId === '') {
- throw new Error('Invalid sessionId');
+ if (sessionId === '') {
+ throw new Error('Invalid sessionId (empty)');
}
try {
@@ -98,11 +96,6 @@ export async function invalidateSession(db: Database, sessionId: string): Promis
// Invalidate sessions that exceed the maximum number of sessions
export async function invalidateExcessSessions(db: Database, userId: number): Promise {
- // Assert that userId is valid
- if (userId === null || userId === undefined) {
- throw new Error('Invalid userId');
- }
-
const newestSessions = db
.select({
id: sessionTable.id
@@ -116,7 +109,3 @@ export async function invalidateExcessSessions(db: Database, userId: number): Pr
.delete(sessionTable)
.where(and(eq(sessionTable.userId, userId), notInArray(sessionTable.id, newestSessions)));
}
-
-export type SessionValidationResult =
- | { session: Session; user: User }
- | { session: null; user: null };
diff --git a/src/lib/server/db/university.ts b/src/lib/server/db/university.ts
new file mode 100644
index 0000000..d60ffe0
--- /dev/null
+++ b/src/lib/server/db/university.ts
@@ -0,0 +1,18 @@
+import type { Database } from './connect';
+import type { University } from '$lib/types/db';
+import { university } from './schema/university';
+
+export async function getUniversities(db: Database): Promise {
+ return await db.select().from(university).orderBy(university.name);
+}
+
+export async function createUniversity(db: Database, name: string): Promise {
+ // Assert that name is valid
+ if (name === null || name === undefined || name === '') {
+ throw new Error('Invalid name');
+ }
+
+ await db.insert(university).values({
+ name
+ });
+}
diff --git a/src/lib/server/db/user.ts b/src/lib/server/db/user.ts
index 838d592..433d9e4 100644
--- a/src/lib/server/db/user.ts
+++ b/src/lib/server/db/user.ts
@@ -5,13 +5,13 @@ import { hashPassword, verifyPasswordStrength } from '../password';
export async function createUser(db: Database, username: string, password: string): Promise {
// Assert that username is valid
- if (username === undefined || username === null || username === '') {
- throw new Error('Invalid username');
+ if (username === '') {
+ throw new Error('Invalid username (empty)');
}
// Assert that password is valid
- if (password === undefined || password === null || password === '') {
- throw new Error('Invalid password');
+ if (password === '') {
+ throw new Error('Invalid password (empty)');
}
// Check the strength of the password
@@ -36,8 +36,8 @@ export async function getUserIdAndPasswordHash(
username: string
): Promise<{ id: number; passwordHash: string }> {
// Assert that username is valid
- if (username === null || username === undefined || username === '') {
- throw new Error('Invalid username');
+ if (username === '') {
+ throw new Error('Invalid username (empty)');
}
try {
@@ -67,23 +67,14 @@ export async function checkUserRatelimit(
ratelimitMaxAttempts: number,
ratelimitTimeout: number
): Promise {
- // Assert that id is valid
- if (userId === null || userId === undefined) {
- throw new Error('Invalid userId');
- }
-
// Assert that ratelimitMaxAttempts is valid
- if (
- ratelimitMaxAttempts === null ||
- ratelimitMaxAttempts === undefined ||
- ratelimitMaxAttempts <= 0
- ) {
- throw new Error('Invalid ratelimitMaxAttempts');
+ if (ratelimitMaxAttempts <= 0) {
+ throw new Error('Invalid ratelimitMaxAttempts (negative)');
}
// Assert that ratelimitTimeout is valid
- if (ratelimitTimeout === null || ratelimitTimeout === undefined || ratelimitTimeout <= 0) {
- throw new Error('Invalid ratelimitTimeout');
+ if (ratelimitTimeout <= 0) {
+ throw new Error('Invalid ratelimitTimeout (negative)');
}
const ratelimitTimeoutMS = ratelimitTimeout * 1000;
diff --git a/src/lib/types/db.ts b/src/lib/types/db.ts
index 1ba5bb1..792490b 100644
--- a/src/lib/types/db.ts
+++ b/src/lib/types/db.ts
@@ -1,9 +1,12 @@
-export type Building = {
- id: number;
- name: string;
-};
+import type { building } from '$lib/server/db/schema/building';
+import type { department } from '$lib/server/db/schema/department';
+import type { sessionTable } from '$lib/server/db/schema/session';
+import type { university } from '$lib/server/db/schema/university';
+import type { userTable } from '$lib/server/db/schema/user';
+import type { InferSelectModel } from 'drizzle-orm';
-export type Department = {
- id: number;
- name: string;
-};
+export type Building = InferSelectModel;
+export type Department = InferSelectModel;
+export type Session = InferSelectModel;
+export type University = InferSelectModel;
+export type User = InferSelectModel;
diff --git a/src/lib/types/person.ts b/src/lib/types/person.ts
index 3619032..1b26902 100644
--- a/src/lib/types/person.ts
+++ b/src/lib/types/person.ts
@@ -1,13 +1,15 @@
import type { State } from './state';
-export type PersonType = 'Student' | 'Employee';
-export const Student: PersonType = 'Student';
+export type PersonType = 'Employee' | 'Guest' | 'Student';
export const Employee: PersonType = 'Employee';
+export const Guest: PersonType = 'Guest';
+export const Student: PersonType = 'Student';
export function isPersonType(s: string): s is PersonType {
switch (s) {
- case Student:
case Employee:
+ case Guest:
+ case Student:
return true;
default:
return false;
@@ -20,7 +22,9 @@ export type Person = {
identifier: string;
fname: string;
lname: string;
- department: string;
+ department: string | null;
+ university: string | null;
building: string | null;
+ guarantorId: number | null;
state: State;
};
diff --git a/src/lib/types/state.ts b/src/lib/types/state.ts
index e773b17..edacf31 100644
--- a/src/lib/types/state.ts
+++ b/src/lib/types/state.ts
@@ -2,6 +2,12 @@ export type State = 'Inside' | 'Outside';
export const StateInside: State = 'Inside';
export const StateOutside: State = 'Outside';
-export function isStateType(value: string): value is State {
- return value === StateInside || value === StateOutside;
+export function isStateType(s: string): s is State {
+ switch (s) {
+ case StateInside:
+ case StateOutside:
+ return true;
+ default:
+ return false;
+ }
}
diff --git a/src/lib/utils/regexp.ts b/src/lib/utils/regexp.ts
index 69052d0..e22d2e9 100644
--- a/src/lib/utils/regexp.ts
+++ b/src/lib/utils/regexp.ts
@@ -8,10 +8,6 @@ export const indexRegExp = new RegExp(
);
export const indexRegExpMsg = 'String must follow Student ID format';
-export const uppercaseRegExp = /^[A-Z0-9_\-+/\\|]*$/;
-export const uppercaseRegExpMsg =
- 'String can consist of uppercase letters, numbers, and symbols (_, -, +, /, \\, |)';
-
-export const lowercaseRegExp = /^[a-z0-9_\-+/\\|]*$/;
-export const lowercaseRegExpMsg =
- 'String can consist of lowercase letters, numbers, and symbols (_, -, +, /, \\, |)';
+export const wordRegExp = /^[\w\-+/\\|][\w\-+/\\|\s]*$/;
+export const wordRegExpMsg =
+ 'String can consist of letters, numbers, symbols (_, -, +, /, \\, |) and whitespaces';
diff --git a/src/routes/(dashboard)/+layout.svelte b/src/routes/(dashboard)/+layout.svelte
index 8e6c509..37f1af7 100644
--- a/src/routes/(dashboard)/+layout.svelte
+++ b/src/routes/(dashboard)/+layout.svelte
@@ -21,6 +21,8 @@
Create Student
{:else if $page.url.pathname === '/create/employee'}
Create Employee
+ {:else if $page.url.pathname === '/create/guest'}
+ Create Guest
{:else if $page.url.pathname === '/statistics'}
Statistics
{:else if $page.url.pathname === '/instructions'}
diff --git a/src/routes/(dashboard)/+page.server.ts b/src/routes/(dashboard)/+page.server.ts
index c2f4a8d..13207ac 100644
--- a/src/routes/(dashboard)/+page.server.ts
+++ b/src/routes/(dashboard)/+page.server.ts
@@ -33,9 +33,8 @@ export const actions: Actions = {
});
}
- const persons = await getPersons(database, 1000, 0, searchQuery);
+ const persons = await getPersons(database, 1000, 0, { searchQuery });
return {
- searchQuery,
persons
};
} catch (err: unknown) {
@@ -50,7 +49,6 @@ export const actions: Actions = {
try {
const formData = await request.formData();
const idS = formData.get('id');
- const type = formData.get('type');
const searchQuery = formData.get('q');
// Check if the id, type and searchQuery are valid
@@ -59,10 +57,6 @@ export const actions: Actions = {
idS === undefined ||
typeof idS !== 'string' ||
idS === '' ||
- type === null ||
- type === undefined ||
- typeof type !== 'string' ||
- type === '' ||
searchQuery === null ||
searchQuery === undefined ||
typeof searchQuery !== 'string'
@@ -83,7 +77,7 @@ export const actions: Actions = {
const id = Number.parseInt(idS);
await togglePersonState(database, id, building, username);
- const persons = await getPersons(database, 1000, 0, searchQuery);
+ const persons = await getPersons(database, 1000, 0, { searchQuery });
return {
searchQuery,
persons,
diff --git a/src/routes/(dashboard)/+page.svelte b/src/routes/(dashboard)/+page.svelte
index d4a453e..1f52825 100644
--- a/src/routes/(dashboard)/+page.svelte
+++ b/src/routes/(dashboard)/+page.svelte
@@ -11,7 +11,6 @@
import { page } from '$app/stores';
import { columns } from './columns';
import { enhance } from '$app/forms';
- import { goto } from '$app/navigation';
import { searchStore } from '$lib/stores/search.svelte';
let { data, form: actionData } = $props();
@@ -34,6 +33,10 @@
let liveSearch: boolean = $state(true);
let searchQuery = $state('');
+ $effect(() => {
+ searchStore.query = searchQuery;
+ });
+
const persons = $derived(actionData?.persons ?? data.persons ?? []);
@@ -43,19 +46,10 @@
action="?/search"
class="flex gap-2 px-4 py-2"
onreset={() => {
- searchStore.query = '';
- goto('/');
+ searchQuery = '';
+ searchForm?.requestSubmit();
}}
- use:enhance={({ formData }) => {
- const input = formData.get('q');
-
- // Check if the input is valid
- if (input === null || input === undefined || typeof input !== 'string') {
- toast.error('Invalid search query');
- } else {
- searchStore.query = input;
- }
-
+ use:enhance={() => {
return async ({ update }) => {
await update({ reset: false });
};
diff --git a/src/routes/(dashboard)/columns.ts b/src/routes/(dashboard)/columns.ts
index f72a515..c67d811 100644
--- a/src/routes/(dashboard)/columns.ts
+++ b/src/routes/(dashboard)/columns.ts
@@ -24,6 +24,10 @@ export const columns: ColumnDef[] = [
accessorKey: 'department',
header: 'Department'
},
+ {
+ accessorKey: 'university',
+ header: 'University'
+ },
{
accessorKey: 'building',
header: 'Building'
@@ -38,7 +42,7 @@ export const columns: ColumnDef[] = [
cell: ({ row }) => {
return renderComponent(DataTableActions, {
id: row.original.id,
- type: row.original.type
+ guarantorId: row.original.guarantorId
});
},
enableSorting: false
diff --git a/src/routes/(dashboard)/create/employee/+page.server.ts b/src/routes/(dashboard)/create/employee/+page.server.ts
index aa7f8b3..b1479cb 100644
--- a/src/routes/(dashboard)/create/employee/+page.server.ts
+++ b/src/routes/(dashboard)/create/employee/+page.server.ts
@@ -2,12 +2,11 @@ import { fail, type Actions } from '@sveltejs/kit';
import { superValidate } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';
-import { createPerson } from '$lib/server/db/person';
+import { createEmployee } from '$lib/server/db/person';
import { getDepartments } from '$lib/server/db/department';
import { formSchema } from './schema';
import type { PageServerLoad } from './$types';
-import { Employee } from '$lib/types/person';
export const load: PageServerLoad = async ({ locals }) => {
const { database } = locals;
@@ -40,10 +39,9 @@ export const actions: Actions = {
try {
const { identifier, fname, lname, department } = form.data;
- const type = Employee;
const building = locals.session.building;
const creator = locals.user.username;
- await createPerson(database, identifier, type, fname, lname, department, building, creator);
+ await createEmployee(database, identifier, fname, lname, department, building, creator);
} catch (err: unknown) {
console.debug(`Failed to create employee: ${(err as Error).message}`);
return fail(400, {
diff --git a/src/routes/(dashboard)/create/employee/+page.svelte b/src/routes/(dashboard)/create/employee/+page.svelte
index b042278..85973cd 100644
--- a/src/routes/(dashboard)/create/employee/+page.svelte
+++ b/src/routes/(dashboard)/create/employee/+page.svelte
@@ -32,7 +32,9 @@
function constructEmail(): string {
const fname = removeDiacritics($formData.fname).toLowerCase();
const lname = removeDiacritics($formData.lname).toLowerCase();
- const department = removeDiacritics($formData.department).toLowerCase();
+ const department = $formData.department
+ ? removeDiacritics($formData.department).toLowerCase()
+ : 'none';
return `${fname}.${lname}@${department}.uns.ac.rs`;
}
@@ -109,8 +111,8 @@
{$formData.department ?? 'Select the department for the employee'}
- {#each data.departments as department (department.id)}
-
+ {#each data.departments as { id, name } (id)}
+
{/each}
diff --git a/src/routes/(dashboard)/create/employee/schema.ts b/src/routes/(dashboard)/create/employee/schema.ts
index b67927f..5665126 100644
--- a/src/routes/(dashboard)/create/employee/schema.ts
+++ b/src/routes/(dashboard)/create/employee/schema.ts
@@ -2,14 +2,14 @@ import { z } from 'zod';
import { nameRegExp, nameRegExpMsg } from '$lib/utils/regexp';
export const formSchema = z.object({
- fname: z.string().min(2).max(50).regex(nameRegExp, nameRegExpMsg),
- lname: z.string().min(2).max(50).regex(nameRegExp, nameRegExpMsg),
+ fname: z.string().min(1).max(50).regex(nameRegExp, nameRegExpMsg),
+ lname: z.string().min(1).max(50).regex(nameRegExp, nameRegExpMsg),
identifier: z
.string()
.refine((value) => z.string().email().safeParse(value).success || /^\d{4}$/.test(value), {
message: 'Identifier must be a valid email or a 4-digit number'
}),
- department: z.string()
+ department: z.string().optional()
});
export type FormSchema = typeof formSchema;
diff --git a/src/routes/(dashboard)/create/guest/+page.server.ts b/src/routes/(dashboard)/create/guest/+page.server.ts
new file mode 100644
index 0000000..42ec8ab
--- /dev/null
+++ b/src/routes/(dashboard)/create/guest/+page.server.ts
@@ -0,0 +1,97 @@
+import { fail, type Actions } from '@sveltejs/kit';
+import { superValidate } from 'sveltekit-superforms';
+import { zod } from 'sveltekit-superforms/adapters';
+
+import { createGuest, getPersons } from '$lib/server/db/person';
+import { getUniversities } from '$lib/server/db/university';
+
+import { formSchema } from './schema';
+import type { PageServerLoad } from './$types';
+
+export const load: PageServerLoad = async ({ locals }) => {
+ const { database } = locals;
+ const form = await superValidate(zod(formSchema));
+
+ const universitiesP = getUniversities(database);
+ const personsP = getPersons(database, 10, 0, { nonGuestsOnly: true });
+
+ const universities = await universitiesP;
+ const persons = await personsP;
+
+ return {
+ form,
+ universities,
+ persons
+ };
+};
+
+export const actions: Actions = {
+ create: async ({ locals, request }) => {
+ const { database } = locals;
+ const form = await superValidate(request, zod(formSchema));
+ if (!form.valid) {
+ return fail(400, {
+ form,
+ message: 'Invalid form inputs'
+ });
+ }
+
+ if (locals.session === null || locals.user === null) {
+ return fail(401, {
+ form,
+ message: 'Invalid session'
+ });
+ }
+
+ try {
+ const { identifier, fname, lname, university, guarantorId } = form.data;
+ const building = locals.session.building;
+ const creator = locals.user.username;
+ await createGuest(
+ database,
+ identifier,
+ fname,
+ lname,
+ university,
+ building,
+ creator,
+ guarantorId
+ );
+ } catch (err: unknown) {
+ console.debug(`Failed to create guest: ${(err as Error).message}`);
+ return fail(400, {
+ form,
+ message: 'Guest already exists'
+ });
+ }
+
+ return {
+ form,
+ message: 'Guest created successfully!'
+ };
+ },
+ search: async ({ locals, request }) => {
+ const { database } = locals;
+ try {
+ const formData = await request.formData();
+ const searchQuery = formData.get('q');
+
+ // Check if the searchQuery is valid
+ if (searchQuery === null || searchQuery === undefined || typeof searchQuery !== 'string') {
+ return fail(400, {
+ message: 'Invalid search query'
+ });
+ }
+
+ const persons = await getPersons(database, 10, 0, { searchQuery, nonGuestsOnly: true });
+ return {
+ persons
+ };
+ } catch (err: unknown) {
+ console.debug(`Failed to search: ${(err as Error).message}`);
+ return fail(500, {
+ message: 'Failed to search'
+ });
+ }
+ }
+};
diff --git a/src/routes/(dashboard)/create/guest/+page.svelte b/src/routes/(dashboard)/create/guest/+page.svelte
new file mode 100644
index 0000000..3072198
--- /dev/null
+++ b/src/routes/(dashboard)/create/guest/+page.svelte
@@ -0,0 +1,197 @@
+
+
+
+
+
diff --git a/src/routes/(dashboard)/create/guest/schema.ts b/src/routes/(dashboard)/create/guest/schema.ts
new file mode 100644
index 0000000..db2501c
--- /dev/null
+++ b/src/routes/(dashboard)/create/guest/schema.ts
@@ -0,0 +1,12 @@
+import { z } from 'zod';
+import { nameRegExp, nameRegExpMsg } from '$lib/utils/regexp';
+
+export const formSchema = z.object({
+ fname: z.string().min(1).max(50).regex(nameRegExp, nameRegExpMsg),
+ lname: z.string().min(1).max(50).regex(nameRegExp, nameRegExpMsg),
+ identifier: z.string().min(4),
+ university: z.string().optional(),
+ guarantorId: z.number().optional()
+});
+
+export type FormSchema = typeof formSchema;
diff --git a/src/routes/(dashboard)/create/student/+page.server.ts b/src/routes/(dashboard)/create/student/+page.server.ts
index 9f559d5..f30694a 100644
--- a/src/routes/(dashboard)/create/student/+page.server.ts
+++ b/src/routes/(dashboard)/create/student/+page.server.ts
@@ -2,12 +2,11 @@ import { fail, type Actions } from '@sveltejs/kit';
import { superValidate } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';
-import { createPerson } from '$lib/server/db/person';
+import { createStudent } from '$lib/server/db/person';
import { getDepartments } from '$lib/server/db/department';
import { formSchema } from './schema';
import type { PageServerLoad } from './$types';
-import { Student } from '$lib/types/person';
export const load: PageServerLoad = async ({ locals }) => {
const { database } = locals;
@@ -40,10 +39,9 @@ export const actions: Actions = {
try {
const { identifier, fname, lname, department } = form.data;
- const type = Student;
const building = locals.session.building;
const creator = locals.user.username;
- await createPerson(database, identifier, type, fname, lname, department, building, creator);
+ await createStudent(database, identifier, fname, lname, department, building, creator);
} catch (err: unknown) {
console.debug(`Failed to create student: ${(err as Error).message}`);
return fail(400, {
diff --git a/src/routes/(dashboard)/create/student/+page.svelte b/src/routes/(dashboard)/create/student/+page.svelte
index ac62831..4948911 100644
--- a/src/routes/(dashboard)/create/student/+page.svelte
+++ b/src/routes/(dashboard)/create/student/+page.svelte
@@ -32,7 +32,7 @@
Create student
- Create an student who wants to enter the building for the first time.
+ Create a student who wants to enter the building for the first time.
@@ -72,8 +72,8 @@
{$formData.department ?? 'Select the department for the student'}
- {#each data.departments as department (department.id)}
-
+ {#each data.departments as { id, name } (id)}
+
{/each}
diff --git a/src/routes/(dashboard)/create/student/schema.ts b/src/routes/(dashboard)/create/student/schema.ts
index d758952..9d93d75 100644
--- a/src/routes/(dashboard)/create/student/schema.ts
+++ b/src/routes/(dashboard)/create/student/schema.ts
@@ -2,10 +2,10 @@ import { z } from 'zod';
import { indexRegExp, indexRegExpMsg, nameRegExp, nameRegExpMsg } from '$lib/utils/regexp';
export const formSchema = z.object({
- fname: z.string().min(2).max(50).regex(nameRegExp, nameRegExpMsg),
- lname: z.string().min(2).max(50).regex(nameRegExp, nameRegExpMsg),
+ fname: z.string().min(1).max(50).regex(nameRegExp, nameRegExpMsg),
+ lname: z.string().min(1).max(50).regex(nameRegExp, nameRegExpMsg),
identifier: z.string().regex(indexRegExp, indexRegExpMsg),
- department: z.string()
+ department: z.string().optional()
});
export type FormSchema = typeof formSchema;
diff --git a/src/routes/(dashboard)/data-table-actions.svelte b/src/routes/(dashboard)/data-table-actions.svelte
index f13c0d7..58fa024 100644
--- a/src/routes/(dashboard)/data-table-actions.svelte
+++ b/src/routes/(dashboard)/data-table-actions.svelte
@@ -5,12 +5,17 @@
import { Input } from '$lib/components/ui/input';
import { searchStore } from '$lib/stores/search.svelte';
- let { id, type }: { id: number; type: string } = $props();
+ let {
+ id,
+ guarantorId
+ }: {
+ id: number;
+ guarantorId: number | null;
+ } = $props();