From aa81c1016d2e929c7ccfc8851d1f3fa6138935b8 Mon Sep 17 00:00:00 2001
From: Gard
Date: Wed, 4 Jun 2025 23:41:06 +0200
Subject: [PATCH 1/8] alt email option
---
apps/www/migrations/0015_adorable_glorian.sql | 1 +
.../0016_nasty_princess_powerful.sql | 17 +
apps/www/migrations/meta/0015_snapshot.json | 564 +++++++++++++++++
apps/www/migrations/meta/0016_snapshot.json | 565 ++++++++++++++++++
apps/www/migrations/meta/_journal.json | 14 +
.../src/lib/components/cards/UserCard.svelte | 7 +-
.../src/lib/components/portal/Header.svelte | 4 +
apps/www/src/lib/db/schemas/users.ts | 3 +-
apps/www/src/lib/services/email.service.tsx | 11 +-
apps/www/src/lib/services/user.service.ts | 9 +
apps/www/src/lib/validators.ts | 3 +-
apps/www/src/routes/api/events/+server.ts | 2 +-
.../src/routes/portal/profil/+page.server.ts | 45 ++
.../www/src/routes/portal/profil/+page.svelte | 70 +++
14 files changed, 1300 insertions(+), 15 deletions(-)
create mode 100644 apps/www/migrations/0015_adorable_glorian.sql
create mode 100644 apps/www/migrations/0016_nasty_princess_powerful.sql
create mode 100644 apps/www/migrations/meta/0015_snapshot.json
create mode 100644 apps/www/migrations/meta/0016_snapshot.json
create mode 100644 apps/www/src/routes/portal/profil/+page.server.ts
create mode 100644 apps/www/src/routes/portal/profil/+page.svelte
diff --git a/apps/www/migrations/0015_adorable_glorian.sql b/apps/www/migrations/0015_adorable_glorian.sql
new file mode 100644
index 00000000..c412a4cc
--- /dev/null
+++ b/apps/www/migrations/0015_adorable_glorian.sql
@@ -0,0 +1 @@
+ALTER TABLE `user` ADD `alt_email` text;
\ No newline at end of file
diff --git a/apps/www/migrations/0016_nasty_princess_powerful.sql b/apps/www/migrations/0016_nasty_princess_powerful.sql
new file mode 100644
index 00000000..34bb06d5
--- /dev/null
+++ b/apps/www/migrations/0016_nasty_princess_powerful.sql
@@ -0,0 +1,17 @@
+PRAGMA foreign_keys=OFF;--> statement-breakpoint
+CREATE TABLE `__new_user` (
+ `id` text PRIMARY KEY NOT NULL,
+ `name` text NOT NULL,
+ `email` text NOT NULL,
+ `feide_id` text,
+ `role` text DEFAULT 'normal' NOT NULL,
+ `additional_beers` integer DEFAULT 0 NOT NULL,
+ `alt_email` text DEFAULT ''
+);
+--> statement-breakpoint
+INSERT INTO `__new_user`("id", "name", "email", "feide_id", "role", "additional_beers", "alt_email") SELECT "id", "name", "email", "feide_id", "role", "additional_beers", "alt_email" FROM `user`;--> statement-breakpoint
+DROP TABLE `user`;--> statement-breakpoint
+ALTER TABLE `__new_user` RENAME TO `user`;--> statement-breakpoint
+PRAGMA foreign_keys=ON;--> statement-breakpoint
+CREATE UNIQUE INDEX `email_idx` ON `user` (`email`);--> statement-breakpoint
+CREATE UNIQUE INDEX `feide_id_idx` ON `user` (`feide_id`);
\ No newline at end of file
diff --git a/apps/www/migrations/meta/0015_snapshot.json b/apps/www/migrations/meta/0015_snapshot.json
new file mode 100644
index 00000000..77f16ee9
--- /dev/null
+++ b/apps/www/migrations/meta/0015_snapshot.json
@@ -0,0 +1,564 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "43ce48b8-c491-4e7e-a1b1-f021301b3032",
+ "prevId": "829e0db2-f6a7-40d1-8d6c-119515a76118",
+ "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": {}
+ },
+ "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
+ }
+ },
+ "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": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/apps/www/migrations/meta/0016_snapshot.json b/apps/www/migrations/meta/0016_snapshot.json
new file mode 100644
index 00000000..a9bbbc41
--- /dev/null
+++ b/apps/www/migrations/meta/0016_snapshot.json
@@ -0,0 +1,565 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "d6fb2eb6-0085-4aeb-aa4a-360c72f5f49e",
+ "prevId": "43ce48b8-c491-4e7e-a1b1-f021301b3032",
+ "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": {}
+ },
+ "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,
+ "default": "''"
+ }
+ },
+ "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": {}
+ }
+ },
+ "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 758c3510..2b52aff3 100644
--- a/apps/www/migrations/meta/_journal.json
+++ b/apps/www/migrations/meta/_journal.json
@@ -106,6 +106,20 @@
"when": 1746103110959,
"tag": "0014_wandering_vulture",
"breakpoints": true
+ },
+ {
+ "idx": 15,
+ "version": "6",
+ "when": 1748776203402,
+ "tag": "0015_adorable_glorian",
+ "breakpoints": true
+ },
+ {
+ "idx": 16,
+ "version": "6",
+ "when": 1749073238658,
+ "tag": "0016_nasty_princess_powerful",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/apps/www/src/lib/components/cards/UserCard.svelte b/apps/www/src/lib/components/cards/UserCard.svelte
index 4dbd8f8e..95322915 100644
--- a/apps/www/src/lib/components/cards/UserCard.svelte
+++ b/apps/www/src/lib/components/cards/UserCard.svelte
@@ -14,6 +14,8 @@
onSelect?.(user);
}
+ let mail = '';
+
function handleKeyDown(event: KeyboardEvent) {
if (event.key === 'Enter' || event.key === ' ') {
handleClick();
@@ -31,7 +33,6 @@
onkeydown={handleKeyDown}
aria-label={`Click to view details of ${user.name}`}
>
-
diff --git a/apps/www/src/lib/components/portal/Header.svelte b/apps/www/src/lib/components/portal/Header.svelte
index 31f1fd8d..2f4f8326 100644
--- a/apps/www/src/lib/components/portal/Header.svelte
+++ b/apps/www/src/lib/components/portal/Header.svelte
@@ -35,6 +35,10 @@
name: 'Brukere',
href: '/portal/brukere'
},
+ {
+ name: 'Min Profil',
+ href: '/portal/profil'
+ },
{
name: 'Admin',
href: '/portal/admin',
diff --git a/apps/www/src/lib/db/schemas/users.ts b/apps/www/src/lib/db/schemas/users.ts
index 1225ec44..c70b42ec 100644
--- a/apps/www/src/lib/db/schemas/users.ts
+++ b/apps/www/src/lib/db/schemas/users.ts
@@ -15,7 +15,8 @@ export const users = sqliteTable(
role: text({ enum: ['board', 'normal'] })
.notNull()
.default('normal'),
- additionalBeers: integer().default(0).notNull()
+ additionalBeers: integer().default(0).notNull(),
+ altEmail: text().default('')
},
(t) => [uniqueIndex('email_idx').on(t.email), uniqueIndex('feide_id_idx').on(t.feideId)]
);
diff --git a/apps/www/src/lib/services/email.service.tsx b/apps/www/src/lib/services/email.service.tsx
index 9742c789..9052bf48 100644
--- a/apps/www/src/lib/services/email.service.tsx
+++ b/apps/www/src/lib/services/email.service.tsx
@@ -7,6 +7,7 @@ import {
} from '@programmerbar/emails';
import type { CreateEmailOptions, Resend } from 'resend';
import { render } from '@react-email/render';
+import { formatDate } from '$lib/date';
const PROGRAMMERBAR_EMAIL = 'styret@programmerbar.no';
const FROM_EMAIL = 'ikkesvar@programmer.bar';
@@ -49,16 +50,6 @@ function generateICS(shift: {
}): string {
const uid = shift.id;
- const formatDate = (dateStr: string) => {
- const date = new Date(dateStr);
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- const hours = String(date.getHours()).padStart(2, '0');
- const minutes = String(date.getMinutes()).padStart(2, '0');
- const seconds = String(date.getSeconds()).padStart(2, '0');
- return `${year}${month}${day}T${hours}${minutes}${seconds}`;
- };
const dtstamp = formatDate(new Date().toISOString());
const dtstart = formatDate(shift.startAt);
const dtend = formatDate(shift.endAt);
diff --git a/apps/www/src/lib/services/user.service.ts b/apps/www/src/lib/services/user.service.ts
index 5c1f83aa..47d7a38b 100644
--- a/apps/www/src/lib/services/user.service.ts
+++ b/apps/www/src/lib/services/user.service.ts
@@ -36,6 +36,15 @@ export class UserService {
.then((rows) => rows[0]);
}
+ async updateAltEmail(userId: string, email: string) {
+ return await this.#db
+ .update(users)
+ .set({ altEmail: email })
+ .where(eq(users.id, userId))
+ .returning()
+ .then((rows) => rows[0]);
+ }
+
async findById(userId: string) {
const user = await this.#db
.select()
diff --git a/apps/www/src/lib/validators.ts b/apps/www/src/lib/validators.ts
index b4c4f8c4..85b791b8 100644
--- a/apps/www/src/lib/validators.ts
+++ b/apps/www/src/lib/validators.ts
@@ -26,7 +26,8 @@ export const CreateInvitationSchema = z.object({
export const CreateEmailShiftSchema = z.object({
user: z.object({
name: z.string().min(1),
- email: z.string().email()
+ email: z.string().email(),
+ altEmail: z.string().email()
}),
shift: z.object({
startAt: z.coerce.date(),
diff --git a/apps/www/src/routes/api/events/+server.ts b/apps/www/src/routes/api/events/+server.ts
index f0c732b5..1abf53d4 100644
--- a/apps/www/src/routes/api/events/+server.ts
+++ b/apps/www/src/routes/api/events/+server.ts
@@ -43,7 +43,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
const emailData: ShiftEmailProps = {
user: {
name: user.name || 'Frivillig',
- email: user.email
+ email: user.altEmail || user.email
},
shift: {
id: shift.id,
diff --git a/apps/www/src/routes/portal/profil/+page.server.ts b/apps/www/src/routes/portal/profil/+page.server.ts
new file mode 100644
index 00000000..e9aad87a
--- /dev/null
+++ b/apps/www/src/routes/portal/profil/+page.server.ts
@@ -0,0 +1,45 @@
+import { fail, redirect } from '@sveltejs/kit';
+import type { Actions, PageServerLoad } from './$types';
+
+export const load: PageServerLoad = async ({ locals }) => {
+ if (!locals.user) {
+ throw redirect(307, '/login');
+ }
+ const user = await locals.userService.findById(locals.user.id);
+ return {
+ user
+ };
+};
+
+export const actions: Actions = {
+ save: async ({ request, locals }) => {
+ if (!locals.user) {
+ throw redirect(307, '/login');
+ }
+ const data = await request.formData();
+ const altEmail = data.get('altEmail') as string;
+
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(altEmail)) {
+ return fail(400, {
+ success: false,
+ message: 'Ugyldig e-postadresse',
+ altEmail
+ });
+ }
+
+ const success = await locals.userService.updateAltEmail(locals.user.id, altEmail);
+ if (!success) {
+ return fail(500, {
+ success: false,
+ message: 'Kunne ikke oppdatere e-postadresse',
+ altEmail
+ });
+ }
+
+ return {
+ success: true,
+ message: 'E-postadresse er oppdatert!'
+ };
+ }
+};
diff --git a/apps/www/src/routes/portal/profil/+page.svelte b/apps/www/src/routes/portal/profil/+page.svelte
new file mode 100644
index 00000000..a175715d
--- /dev/null
+++ b/apps/www/src/routes/portal/profil/+page.svelte
@@ -0,0 +1,70 @@
+
+
+
+ Min Profil
+
+
+
+
+
+
+
Profilinnstillinger
+
Oppdater din informasjon og innstillinger
+
+
+
+
+ {#if form?.message}
+
+ {/if}
+
+
+
+
+
From d469260277e3bf59da23fb44d8960dd50988d6ad Mon Sep 17 00:00:00 2001
From: Gard
Date: Wed, 4 Jun 2025 23:52:09 +0200
Subject: [PATCH 2/8] format time email
---
apps/www/src/lib/services/email.service.tsx | 20 ++++++++++++++++----
1 file changed, 16 insertions(+), 4 deletions(-)
diff --git a/apps/www/src/lib/services/email.service.tsx b/apps/www/src/lib/services/email.service.tsx
index 9052bf48..c9634897 100644
--- a/apps/www/src/lib/services/email.service.tsx
+++ b/apps/www/src/lib/services/email.service.tsx
@@ -7,7 +7,7 @@ import {
} from '@programmerbar/emails';
import type { CreateEmailOptions, Resend } from 'resend';
import { render } from '@react-email/render';
-import { formatDate } from '$lib/date';
+import { formatDate, normalDate } from '$lib/date';
const PROGRAMMERBAR_EMAIL = 'styret@programmerbar.no';
const FROM_EMAIL = 'ikkesvar@programmer.bar';
@@ -51,9 +51,21 @@ function generateICS(shift: {
const uid = shift.id;
const dtstamp = formatDate(new Date().toISOString());
- const dtstart = formatDate(shift.startAt);
- const dtend = formatDate(shift.endAt);
-
+ const dtstart = normalDate(shift.startAt);
+ const dtend = normalDate(shift.endAt);
+
+ console.log(dtstart, dtend);
+
+ // const formatDate = (dateStr: string) => {
+ // const date = new Date(dateStr);
+ // const year = date.getFullYear();
+ // const month = String(date.getMonth() + 1).padStart(2, '0');
+ // const day = String(date.getDate()).padStart(2, '0');
+ // const hours = String(date.getHours()).padStart(2, '0');
+ // const minutes = String(date.getMinutes()).padStart(2, '0');
+ // const seconds = String(date.getSeconds()).padStart(2, '0');
+ // return `${year}${month}${day}T${hours}${minutes}${seconds}`;
+ // };
return `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Programmerbar//Shift Notification//EN
From 23ab146054aa7fa0cfca7343abeb0b403025fb43 Mon Sep 17 00:00:00 2001
From: Gard
Date: Wed, 4 Jun 2025 23:54:22 +0200
Subject: [PATCH 3/8] removed log
---
apps/www/src/lib/services/email.service.tsx | 12 ------------
1 file changed, 12 deletions(-)
diff --git a/apps/www/src/lib/services/email.service.tsx b/apps/www/src/lib/services/email.service.tsx
index c9634897..6018c58e 100644
--- a/apps/www/src/lib/services/email.service.tsx
+++ b/apps/www/src/lib/services/email.service.tsx
@@ -54,18 +54,6 @@ function generateICS(shift: {
const dtstart = normalDate(shift.startAt);
const dtend = normalDate(shift.endAt);
- console.log(dtstart, dtend);
-
- // const formatDate = (dateStr: string) => {
- // const date = new Date(dateStr);
- // const year = date.getFullYear();
- // const month = String(date.getMonth() + 1).padStart(2, '0');
- // const day = String(date.getDate()).padStart(2, '0');
- // const hours = String(date.getHours()).padStart(2, '0');
- // const minutes = String(date.getMinutes()).padStart(2, '0');
- // const seconds = String(date.getSeconds()).padStart(2, '0');
- // return `${year}${month}${day}T${hours}${minutes}${seconds}`;
- // };
return `BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Programmerbar//Shift Notification//EN
From 256d188b74b74fd772e07dc580bf66b27472ca97 Mon Sep 17 00:00:00 2001
From: Gard
Date: Thu, 5 Jun 2025 00:25:37 +0200
Subject: [PATCH 4/8] fixed css
---
apps/www/src/routes/portal/profil/+page.svelte | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/apps/www/src/routes/portal/profil/+page.svelte b/apps/www/src/routes/portal/profil/+page.svelte
index a175715d..aaebb23a 100644
--- a/apps/www/src/routes/portal/profil/+page.svelte
+++ b/apps/www/src/routes/portal/profil/+page.svelte
@@ -31,7 +31,6 @@
{form.message}
{/if}
-
From 00e411e7a57fd23df7b806425fd34bb7f854aaff Mon Sep 17 00:00:00 2001
From: Gard
Date: Thu, 5 Jun 2025 00:44:48 +0200
Subject: [PATCH 6/8] cleaner code
---
apps/www/src/lib/components/cards/UserCard.svelte | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/apps/www/src/lib/components/cards/UserCard.svelte b/apps/www/src/lib/components/cards/UserCard.svelte
index 95322915..3293088e 100644
--- a/apps/www/src/lib/components/cards/UserCard.svelte
+++ b/apps/www/src/lib/components/cards/UserCard.svelte
@@ -14,8 +14,6 @@
onSelect?.(user);
}
- let mail = '';
-
function handleKeyDown(event: KeyboardEvent) {
if (event.key === 'Enter' || event.key === ' ') {
handleClick();
@@ -53,8 +51,8 @@
{/if}
-
- {mail ? user.email : user.altEmail || user.email}
+
+ {user.altEmail || user.email}
From edf178ed2d3d5e460737a6a82781adf8928954eb Mon Sep 17 00:00:00 2001
From: Gard
Date: Thu, 5 Jun 2025 00:59:20 +0200
Subject: [PATCH 7/8] using zod parser
---
apps/www/src/routes/portal/profil/+page.server.ts | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/apps/www/src/routes/portal/profil/+page.server.ts b/apps/www/src/routes/portal/profil/+page.server.ts
index e9aad87a..b507b522 100644
--- a/apps/www/src/routes/portal/profil/+page.server.ts
+++ b/apps/www/src/routes/portal/profil/+page.server.ts
@@ -1,4 +1,5 @@
import { fail, redirect } from '@sveltejs/kit';
+import { z } from 'zod';
import type { Actions, PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => {
@@ -19,8 +20,9 @@ export const actions: Actions = {
const data = await request.formData();
const altEmail = data.get('altEmail') as string;
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
- if (!emailRegex.test(altEmail)) {
+ const isEmail = (x: unknown) => z.string().email().parse(x);
+
+ if (!isEmail(altEmail)) {
return fail(400, {
success: false,
message: 'Ugyldig e-postadresse',
From 12203bb53f013baa469f38bc0142dc2c38edc8cb Mon Sep 17 00:00:00 2001
From: Gard
Date: Thu, 5 Jun 2025 01:24:50 +0200
Subject: [PATCH 8/8] move to validators
---
apps/www/src/lib/components/cards/UserCard.svelte | 5 ++++-
apps/www/src/lib/validators.ts | 2 ++
apps/www/src/routes/portal/profil/+page.server.ts | 6 ++----
3 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/apps/www/src/lib/components/cards/UserCard.svelte b/apps/www/src/lib/components/cards/UserCard.svelte
index 3293088e..65a575c3 100644
--- a/apps/www/src/lib/components/cards/UserCard.svelte
+++ b/apps/www/src/lib/components/cards/UserCard.svelte
@@ -51,7 +51,10 @@
{/if}
-
+
{user.altEmail || user.email}
diff --git a/apps/www/src/lib/validators.ts b/apps/www/src/lib/validators.ts
index 85b791b8..5b912283 100644
--- a/apps/www/src/lib/validators.ts
+++ b/apps/www/src/lib/validators.ts
@@ -23,6 +23,8 @@ export const CreateInvitationSchema = z.object({
email: z.string().email()
});
+export const isValidEmail = (x: unknown): boolean => z.string().email().safeParse(x).success;
+
export const CreateEmailShiftSchema = z.object({
user: z.object({
name: z.string().min(1),
diff --git a/apps/www/src/routes/portal/profil/+page.server.ts b/apps/www/src/routes/portal/profil/+page.server.ts
index b507b522..f86d398e 100644
--- a/apps/www/src/routes/portal/profil/+page.server.ts
+++ b/apps/www/src/routes/portal/profil/+page.server.ts
@@ -1,5 +1,5 @@
import { fail, redirect } from '@sveltejs/kit';
-import { z } from 'zod';
+import { isValidEmail } from '$lib/validators';
import type { Actions, PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => {
@@ -20,9 +20,7 @@ export const actions: Actions = {
const data = await request.formData();
const altEmail = data.get('altEmail') as string;
- const isEmail = (x: unknown) => z.string().email().parse(x);
-
- if (!isEmail(altEmail)) {
+ if (!isValidEmail(altEmail)) {
return fail(400, {
success: false,
message: 'Ugyldig e-postadresse',