From 96bc7c5af1f48defa04e0ce58c27a6cceac85a20 Mon Sep 17 00:00:00 2001 From: GardKalland Date: Wed, 20 Aug 2025 21:32:24 +0200 Subject: [PATCH 1/3] Approval via site --- apps/api/src/models/mod.rs | 4 +- apps/api/src/repositories/mod.rs | 6 +- .../www/migrations/0022_gorgeous_callisto.sql | 10 + apps/www/migrations/meta/0022_snapshot.json | 1007 +++++++++++++++++ apps/www/migrations/meta/_journal.json | 7 + apps/www/package.json | 2 +- apps/www/scripts/frivillig.ts | 35 + apps/www/src/app.d.ts | 1 + apps/www/src/hooks.server.ts | 4 + .../components/portal/PortalSidebar.svelte | 9 +- apps/www/src/lib/db/schemas/index.ts | 1 + .../lib/db/schemas/pending-applications.ts | 20 + .../services/pending-application.service.ts | 60 + .../src/routes/auth/feide/callback/+server.ts | 16 + .../pending-applications/+page.server.ts | 53 + .../admin/pending-applications/+page.svelte | 100 ++ 16 files changed, 1327 insertions(+), 8 deletions(-) create mode 100644 apps/www/migrations/0022_gorgeous_callisto.sql create mode 100644 apps/www/migrations/meta/0022_snapshot.json create mode 100644 apps/www/scripts/frivillig.ts create mode 100644 apps/www/src/lib/db/schemas/pending-applications.ts create mode 100644 apps/www/src/lib/services/pending-application.service.ts create mode 100644 apps/www/src/routes/portal/admin/pending-applications/+page.server.ts create mode 100644 apps/www/src/routes/portal/admin/pending-applications/+page.svelte diff --git a/apps/api/src/models/mod.rs b/apps/api/src/models/mod.rs index 9f0c8884..a04344aa 100644 --- a/apps/api/src/models/mod.rs +++ b/apps/api/src/models/mod.rs @@ -1,4 +1,3 @@ -pub mod user; pub mod claimed_credit; pub mod contact_submission; pub mod event; @@ -12,5 +11,6 @@ pub mod product_product_type; pub mod product_type; pub mod session; pub mod shift; +pub mod user; pub mod user_shift; -pub mod users_group; \ No newline at end of file +pub mod users_group; diff --git a/apps/api/src/repositories/mod.rs b/apps/api/src/repositories/mod.rs index 065660ec..a04344aa 100644 --- a/apps/api/src/repositories/mod.rs +++ b/apps/api/src/repositories/mod.rs @@ -1,5 +1,3 @@ -pub mod user; -pub mod session; pub mod claimed_credit; pub mod contact_submission; pub mod event; @@ -11,6 +9,8 @@ pub mod producer; pub mod product; pub mod product_product_type; pub mod product_type; +pub mod session; pub mod shift; +pub mod user; pub mod user_shift; -pub mod users_group; \ No newline at end of file +pub mod users_group; diff --git a/apps/www/migrations/0022_gorgeous_callisto.sql b/apps/www/migrations/0022_gorgeous_callisto.sql new file mode 100644 index 00000000..722344aa --- /dev/null +++ b/apps/www/migrations/0022_gorgeous_callisto.sql @@ -0,0 +1,10 @@ +CREATE TABLE `pending_application` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `email` text NOT NULL, + `feide_id` text NOT NULL, + `created_at` integer NOT NULL +); +--> statement-breakpoint +CREATE INDEX `pending_application_email_idx` ON `pending_application` (`email`);--> statement-breakpoint +CREATE INDEX `pending_application_feide_id_idx` ON `pending_application` (`feide_id`); \ No newline at end of file diff --git a/apps/www/migrations/meta/0022_snapshot.json b/apps/www/migrations/meta/0022_snapshot.json new file mode 100644 index 00000000..4cb4bd96 --- /dev/null +++ b/apps/www/migrations/meta/0022_snapshot.json @@ -0,0 +1,1007 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "909bc89a-0da2-4330-9fc3-64b6de72ef53", + "prevId": "3d643972-c02b-4390-af85-a4d6cd8e7776", + "tables": { + "claimed_credits": { + "name": "claimed_credits", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credit_cost": { + "name": "credit_cost", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "claimed_credits_user_id_user_id_fk": { + "name": "claimed_credits_user_id_user_id_fk", + "tableFrom": "claimed_credits", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "contact_submission": { + "name": "contact_submission", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "submitted_at": { + "name": "submitted_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "event": { + "name": "event", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "group": { + "name": "group", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invitation": { + "name": "invitation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification": { + "name": "notification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_notifications_user_id": { + "name": "idx_notifications_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_notifications_archived_at": { + "name": "idx_notifications_archived_at", + "columns": [ + "archived_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "notification_user_id_user_id_fk": { + "name": "notification_user_id_user_id_fk", + "tableFrom": "notification", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shift": { + "name": "shift", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "start_at": { + "name": "start_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "end_at": { + "name": "end_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "shift_event_id_event_id_fk": { + "name": "shift_event_id_event_id_fk", + "tableFrom": "shift", + "tableTo": "event", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_shift": { + "name": "user_shift", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "shift_id": { + "name": "shift_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_beer_claimed": { + "name": "is_beer_claimed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'accepted'" + } + }, + "indexes": {}, + "foreignKeys": { + "user_shift_user_id_user_id_fk": { + "name": "user_shift_user_id_user_id_fk", + "tableFrom": "user_shift", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_shift_shift_id_shift_id_fk": { + "name": "user_shift_shift_id_shift_id_fk", + "tableFrom": "user_shift", + "tableTo": "shift", + "columnsFrom": [ + "shift_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users_groups": { + "name": "users_groups", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "users_groups_user_id_user_id_fk": { + "name": "users_groups_user_id_user_id_fk", + "tableFrom": "users_groups", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "users_groups_group_id_group_id_fk": { + "name": "users_groups_group_id_group_id_fk", + "tableFrom": "users_groups", + "tableTo": "group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "feide_id": { + "name": "feide_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'normal'" + }, + "additional_beers": { + "name": "additional_beers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "alt_email": { + "name": "alt_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_deleted": { + "name": "is_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "email_idx": { + "name": "email_idx", + "columns": [ + "email" + ], + "isUnique": true + }, + "feide_id_idx": { + "name": "feide_id_idx", + "columns": [ + "feide_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "producers": { + "name": "producers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "product_types": { + "name": "product_types", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "products": { + "name": "products", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "sku": { + "name": "sku", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_sold_out": { + "name": "is_sold_out", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "ordinary_price": { + "name": "ordinary_price", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "student_price": { + "name": "student_price", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "internal_price": { + "name": "internal_price", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "volume": { + "name": "volume", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "alcohol_content": { + "name": "alcohol_content", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "variants": { + "name": "variants", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image_id": { + "name": "image_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "producer_id": { + "name": "producer_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "products_producer_id_producers_id_fk": { + "name": "products_producer_id_producers_id_fk", + "tableFrom": "products", + "tableTo": "producers", + "columnsFrom": [ + "producer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "product_product_types": { + "name": "product_product_types", + "columns": { + "product_id": { + "name": "product_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "product_type_id": { + "name": "product_type_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "product_product_types_product_id_products_id_fk": { + "name": "product_product_types_product_id_products_id_fk", + "tableFrom": "product_product_types", + "tableTo": "products", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "product_product_types_product_type_id_product_types_id_fk": { + "name": "product_product_types_product_type_id_product_types_id_fk", + "tableFrom": "product_product_types", + "tableTo": "product_types", + "columnsFrom": [ + "product_type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "images": { + "name": "images", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "pending_application": { + "name": "pending_application", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "feide_id": { + "name": "feide_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "pending_application_email_idx": { + "name": "pending_application_email_idx", + "columns": [ + "email" + ], + "isUnique": false + }, + "pending_application_feide_id_idx": { + "name": "pending_application_feide_id_idx", + "columns": [ + "feide_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/apps/www/migrations/meta/_journal.json b/apps/www/migrations/meta/_journal.json index 2c030e12..6cb18b1e 100644 --- a/apps/www/migrations/meta/_journal.json +++ b/apps/www/migrations/meta/_journal.json @@ -155,6 +155,13 @@ "when": 1755665199729, "tag": "0021_broken_professor_monster", "breakpoints": true + }, + { + "idx": 22, + "version": "6", + "when": 1755703977027, + "tag": "0022_gorgeous_callisto", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/www/package.json b/apps/www/package.json index 26c9108d..96ee1b3f 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -82,4 +82,4 @@ "zod": "4.0.17", "zod-form-data": "3.0.0" } -} \ No newline at end of file +} diff --git a/apps/www/scripts/frivillig.ts b/apps/www/scripts/frivillig.ts new file mode 100644 index 00000000..ec359b0d --- /dev/null +++ b/apps/www/scripts/frivillig.ts @@ -0,0 +1,35 @@ +import { faker } from '@faker-js/faker'; +import { pendingApplications } from '../src/lib/db/schemas'; +import { nanoid } from 'nanoid'; +import { setup } from './setup'; + +/// Creates frivillige users, via the "bli-frivillig" button on the homepage. +async function main() { + const { db } = await setup(); + + const fakeApplications = Array.from({ length: 10 }, () => ({ + id: nanoid(), + name: faker.person.fullName(), + email: faker.internet.email(), + feideId: faker.string.uuid(), + createdAt: new Date() + })); + + for (const application of fakeApplications) { + await db.insert(pendingApplications).values(application); + } + + console.log(`Added ${fakeApplications.length} pending volunteer applications.`); + console.log( + `You can now view and approve/deny these applications in the admin panel at /portal/admin/pending-applications` + ); +} + +main() + .then(() => { + process.exit(0); + }) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/apps/www/src/app.d.ts b/apps/www/src/app.d.ts index a8f923f9..ef36f791 100644 --- a/apps/www/src/app.d.ts +++ b/apps/www/src/app.d.ts @@ -37,6 +37,7 @@ declare global { productTypeService: import('$lib/services/product-type.service').ProductTypeService; productService: import('$lib/services/product.service').ProductService; imageService: import('$lib/services/image.service').ImageService; + pendingApplicationService: import('$lib/services/pending-application.service').PendingApplicationService; } // interface PageData {} // interface PageState {} diff --git a/apps/www/src/hooks.server.ts b/apps/www/src/hooks.server.ts index 77160e1d..836d3aa4 100644 --- a/apps/www/src/hooks.server.ts +++ b/apps/www/src/hooks.server.ts @@ -19,6 +19,7 @@ import { UserService } from '$lib/services/user.service'; import type { Handle } from '@sveltejs/kit'; import { Resend } from 'resend'; import { ImageService } from '$lib/services/image.service'; +import { PendingApplicationService } from '$lib/services/pending-application.service'; export const handle: Handle = async ({ event, resolve }) => { const STATUS_KV = event.platform!.env.STATUS_KV; @@ -103,6 +104,9 @@ export const handle: Handle = async ({ event, resolve }) => { const imageService = new ImageService(R2_BUCKET, db); event.locals.imageService = imageService; + const pendingApplicationService = new PendingApplicationService(db); + event.locals.pendingApplicationService = pendingApplicationService; + // Validate auth const sessionId = event.cookies.get(auth.sessionCookieName); diff --git a/apps/www/src/lib/components/portal/PortalSidebar.svelte b/apps/www/src/lib/components/portal/PortalSidebar.svelte index 0761e75e..5fd59e6a 100644 --- a/apps/www/src/lib/components/portal/PortalSidebar.svelte +++ b/apps/www/src/lib/components/portal/PortalSidebar.svelte @@ -11,7 +11,8 @@ Menu, X, Shield, - Database + Database, + UserCheck } from '@lucide/svelte'; import { page } from '$app/state'; import { getUser } from '$lib/context/user.context'; @@ -28,7 +29,6 @@ let isSidebarOpen = $state(false); let isMobile = $state(true); // Start as mobile to prevent flash - // Show full layout when: opened on mobile OR on desktop const showFullLayout = $derived((isSidebarOpen && isMobile) || !isMobile); // Main portal navigation items @@ -73,6 +73,11 @@ name: 'Admin Panel', href: '/portal/admin', icon: Shield + }, + { + name: 'Søknader', + href: '/portal/admin/pending-applications', + icon: UserCheck } ] : [] diff --git a/apps/www/src/lib/db/schemas/index.ts b/apps/www/src/lib/db/schemas/index.ts index ad632a4c..f7dd49ef 100644 --- a/apps/www/src/lib/db/schemas/index.ts +++ b/apps/www/src/lib/db/schemas/index.ts @@ -14,3 +14,4 @@ export * from './product-types'; export * from './products'; export * from './product-product-types'; export * from './images'; +export * from './pending-applications'; diff --git a/apps/www/src/lib/db/schemas/pending-applications.ts b/apps/www/src/lib/db/schemas/pending-applications.ts new file mode 100644 index 00000000..13ecd064 --- /dev/null +++ b/apps/www/src/lib/db/schemas/pending-applications.ts @@ -0,0 +1,20 @@ +import { sqliteTable, text, integer, index } from 'drizzle-orm/sqlite-core'; +import { type InferSelectModel, type InferInsertModel } from 'drizzle-orm'; + +export const pendingApplications = sqliteTable( + 'pending_application', + { + id: text().notNull().primaryKey(), + name: text().notNull(), + email: text().notNull(), + feideId: text().notNull(), + createdAt: integer({ mode: 'timestamp' }).notNull() + }, + (t) => [ + index('pending_application_email_idx').on(t.email), + index('pending_application_feide_id_idx').on(t.feideId) + ] +); + +export type PendingApplication = InferSelectModel; +export type PendingApplicationInsert = InferInsertModel; diff --git a/apps/www/src/lib/services/pending-application.service.ts b/apps/www/src/lib/services/pending-application.service.ts new file mode 100644 index 00000000..6776a669 --- /dev/null +++ b/apps/www/src/lib/services/pending-application.service.ts @@ -0,0 +1,60 @@ +import type { Database } from '$lib/db/drizzle'; +import { pendingApplications } from '$lib/db/schemas'; +import { eq } from 'drizzle-orm'; +import { nanoid } from 'nanoid'; + +export class PendingApplicationService { + #db: Database; + + constructor(db: Database) { + this.#db = db; + } + + async create(data: { name: string; email: string; feideId: string }) { + return await this.#db + .insert(pendingApplications) + .values({ + id: nanoid(), + name: data.name, + email: data.email.toLowerCase(), + feideId: data.feideId, + createdAt: new Date() + }) + .returning() + .get(); + } + + async findByEmail(email: string) { + return await this.#db.query.pendingApplications.findFirst({ + where: (row, { eq }) => eq(row.email, email.toLowerCase()) + }); + } + + async findByFeideId(feideId: string) { + return await this.#db.query.pendingApplications.findFirst({ + where: (row, { eq }) => eq(row.feideId, feideId) + }); + } + + async findAll() { + return await this.#db.query.pendingApplications.findMany({ + orderBy: (row, { desc }) => desc(row.createdAt) + }); + } + + async delete(id: string) { + return await this.#db.delete(pendingApplications).where(eq(pendingApplications.id, id)); + } + + async deleteByEmail(email: string) { + return await this.#db + .delete(pendingApplications) + .where(eq(pendingApplications.email, email.toLowerCase())); + } + + async deleteByFeideId(feideId: string) { + return await this.#db + .delete(pendingApplications) + .where(eq(pendingApplications.feideId, feideId)); + } +} diff --git a/apps/www/src/routes/auth/feide/callback/+server.ts b/apps/www/src/routes/auth/feide/callback/+server.ts index c003cb22..947b86af 100644 --- a/apps/www/src/routes/auth/feide/callback/+server.ts +++ b/apps/www/src/routes/auth/feide/callback/+server.ts @@ -39,6 +39,22 @@ export const GET: RequestHandler = async ({ locals, cookies, url }) => { }); } + const existingApplication = await locals.pendingApplicationService.findByFeideId(feideUser.id); + if (existingApplication) { + return new Response(null, { + status: 302, + headers: { + location: `/bli-frivillig?error=${ERROR_SEARCH_PARAM_ALREADY_REGISTERED}` + } + }); + } + + await locals.pendingApplicationService.create({ + name: feideUser.username, + email: feideUser.email, + feideId: feideUser.id + }); + await locals.emailService.sendVolunteerRequestEmail({ name: feideUser.username, email: feideUser.email diff --git a/apps/www/src/routes/portal/admin/pending-applications/+page.server.ts b/apps/www/src/routes/portal/admin/pending-applications/+page.server.ts new file mode 100644 index 00000000..cf0e21d5 --- /dev/null +++ b/apps/www/src/routes/portal/admin/pending-applications/+page.server.ts @@ -0,0 +1,53 @@ +import type { PageServerLoad, Actions } from './$types'; +import { nanoid } from 'nanoid'; + +export const load: PageServerLoad = async ({ locals }) => { + const pendingApplications = await locals.pendingApplicationService.findAll(); + + return { + pendingApplications + }; +}; + +export const actions: Actions = { + approve: async ({ locals, request }) => { + const data = await request.formData(); + const applicationId = data.get('applicationId') as string; + const name = data.get('name') as string; + const email = data.get('email') as string; + const feideId = data.get('feideId') as string; + + const userId = nanoid(); + await locals.userService.create({ + id: userId, + name, + email, + feideId + }); + + await locals.pendingApplicationService.delete(applicationId); + + return { success: true, message: 'Application approved and user created' }; + }, + + deny: async ({ locals, request }) => { + const data = await request.formData(); + const applicationId = data.get('applicationId') as string; + const email = data.get('email') as string; + const feideId = data.get('feideId') as string; + + await locals.pendingApplicationService.delete(applicationId); + + const existingUser = await locals.userService.findByFeideId(feideId); + if (existingUser) { + await locals.userService.deleteUser(existingUser.id); + } + + const existingInvitation = await locals.invitationService.findByEmail(email); + if (existingInvitation) { + await locals.invitationService.delete(existingInvitation.id); + } + + return { success: true, message: 'Application denied and data removed' }; + } +}; diff --git a/apps/www/src/routes/portal/admin/pending-applications/+page.svelte b/apps/www/src/routes/portal/admin/pending-applications/+page.svelte new file mode 100644 index 00000000..367e05fb --- /dev/null +++ b/apps/www/src/routes/portal/admin/pending-applications/+page.svelte @@ -0,0 +1,100 @@ + + + + Admin - Pending Applications + + +
+ Frivillig-søknader + + {#if form?.success} +
+

{form.message}

+
+ {/if} + + {#if data.pendingApplications.length === 0} +
+

Ingen ventende søknader

+
+ {:else} +
+ {#each data.pendingApplications as application (application.id)} +
+
+
+

{application.name}

+

{application.email}

+

+ Søkte: {normalDate(application.createdAt)} +

+
+ +
+
{ + processingId = application.id; + return async ({ update }) => { + await update(); + await invalidateAll(); + processingId = null; + }; + }} + > + + + + + +
+ +
{ + processingId = application.id; + return async ({ update }) => { + await update(); + await invalidateAll(); + processingId = null; + }; + }} + > + + + + +
+
+
+
+ {/each} +
+ {/if} +
From 705913f7a674151ceaf28b1e053752f48ad49d79 Mon Sep 17 00:00:00 2001 From: GardKalland Date: Wed, 20 Aug 2025 21:38:32 +0200 Subject: [PATCH 2/3] format --- .../admin/pending-applications/+page.server.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/www/src/routes/portal/admin/pending-applications/+page.server.ts b/apps/www/src/routes/portal/admin/pending-applications/+page.server.ts index cf0e21d5..eeb44065 100644 --- a/apps/www/src/routes/portal/admin/pending-applications/+page.server.ts +++ b/apps/www/src/routes/portal/admin/pending-applications/+page.server.ts @@ -1,7 +1,12 @@ import type { PageServerLoad, Actions } from './$types'; import { nanoid } from 'nanoid'; +import { redirect, fail } from '@sveltejs/kit'; export const load: PageServerLoad = async ({ locals }) => { + if (!locals.user || locals.user.role !== 'board') { + throw redirect(303, '/portal'); + } + const pendingApplications = await locals.pendingApplicationService.findAll(); return { @@ -11,6 +16,10 @@ export const load: PageServerLoad = async ({ locals }) => { export const actions: Actions = { approve: async ({ locals, request }) => { + if (!locals.user || locals.user.role !== 'board') { + return fail(401, { error: 'Unauthorized' }); + } + const data = await request.formData(); const applicationId = data.get('applicationId') as string; const name = data.get('name') as string; @@ -31,6 +40,10 @@ export const actions: Actions = { }, deny: async ({ locals, request }) => { + if (!locals.user || locals.user.role !== 'board') { + return fail(401, { error: 'Unauthorized' }); + } + const data = await request.formData(); const applicationId = data.get('applicationId') as string; const email = data.get('email') as string; From 6c5e18b7d68001c385a4ddae7ce0d68580510030 Mon Sep 17 00:00:00 2001 From: GardKalland Date: Wed, 20 Aug 2025 21:53:33 +0200 Subject: [PATCH 3/3] name change --- .../src/routes/portal/admin/pending-applications/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/www/src/routes/portal/admin/pending-applications/+page.svelte b/apps/www/src/routes/portal/admin/pending-applications/+page.svelte index 367e05fb..df9c45e5 100644 --- a/apps/www/src/routes/portal/admin/pending-applications/+page.svelte +++ b/apps/www/src/routes/portal/admin/pending-applications/+page.svelte @@ -11,7 +11,7 @@ - Admin - Pending Applications + Admin - Søkere