diff --git a/nuxt.config.ts b/nuxt.config.ts index f7de71c..69fc68d 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -6,6 +6,7 @@ export default defineNuxtConfig({ cookieSecretKey: "", vapidPrivateKey: "", public: { + defaultTimezone: "Europe/Oslo", vapidPublicKey: "", } } diff --git a/package.json b/package.json index 503e65c..bd7dbc8 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "postinstall": "nuxt prepare" }, "dependencies": { + "luxon": "^3.5.0", "nuxt": "^3.15.4", "vue": "latest", "vue-router": "latest", @@ -23,6 +24,7 @@ ] }, "devDependencies": { + "@types/luxon": "^3.4.2", "@types/web-push": "^3.6.4" } } diff --git a/pages/account/settings.vue b/pages/account/settings.vue index e1283d4..41dcd69 100644 --- a/pages/account/settings.vue +++ b/pages/account/settings.vue @@ -5,6 +5,15 @@ Name: {{ session?.account.name }}

Access: {{ session?.account.type }}

+
+ + +

@@ -26,6 +35,22 @@ definePageMeta({ const { data: session } = useAccountSession(); const { refresh: sessionRefresh } = useAccountSession(); +const timezone = ref(session.value?.account.timezone ?? ""); + +async function changeSettings() { + try { + await $fetch("/api/account", { + method: "patch", + body: { + timezone: timezone.value, + } + }); + await sessionRefresh(); + } catch (err: any) { + alert(err.data.message); + } +} + async function deleteAccount() { try { await $fetch.raw("/api/account", { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 361b39c..2108bdd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + luxon: + specifier: ^3.5.0 + version: 3.5.0 nuxt: specifier: ^3.15.4 version: 3.15.4(@parcel/watcher@2.5.1)(@types/node@22.13.8)(db0@0.2.4)(ioredis@5.5.0)(magicast@0.3.5)(rollup@4.34.9)(terser@5.39.0)(typescript@5.8.2)(vite@6.2.0(@types/node@22.13.8)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))(yaml@2.7.0) @@ -21,6 +24,9 @@ importers: specifier: ^3.6.7 version: 3.6.7 devDependencies: + '@types/luxon': + specifier: ^3.4.2 + version: 3.4.2 '@types/web-push': specifier: ^3.6.4 version: 3.6.4 @@ -873,6 +879,9 @@ packages: '@types/http-proxy@1.17.16': resolution: {integrity: sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==} + '@types/luxon@3.4.2': + resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} + '@types/node@22.13.8': resolution: {integrity: sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==} @@ -1943,6 +1952,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + luxon@3.5.0: + resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} + engines: {node: '>=12'} + magic-string-ast@0.7.0: resolution: {integrity: sha512-686fgAHaJY7wLTFEq7nnKqeQrhqmXB19d1HnqT35Ci7BN6hbAYLZUezTQ062uUHM7ggZEQlqJ94Ftls+KDXU8Q==} engines: {node: '>=16.14.0'} @@ -4036,6 +4049,8 @@ snapshots: dependencies: '@types/node': 22.13.8 + '@types/luxon@3.4.2': {} + '@types/node@22.13.8': dependencies: undici-types: 6.20.0 @@ -5210,6 +5225,8 @@ snapshots: dependencies: yallist: 3.1.1 + luxon@3.5.0: {} + magic-string-ast@0.7.0: dependencies: magic-string: 0.30.17 diff --git a/server/api/account.patch.ts b/server/api/account.patch.ts index 3b27cd8..9059d6f 100644 --- a/server/api/account.patch.ts +++ b/server/api/account.patch.ts @@ -1,12 +1,16 @@ import { Account } from "~/shared/types/account"; import { readAccounts, writeAccounts } from "~/server/database"; +import { DateTime } from "luxon"; export default defineEventHandler(async (event) => { const session = await requireAccountSession(event); - const body: Pick = await readBody(event); + const body: Pick = await readBody(event); if ( - !(body.interestedIds instanceof Array) - || !body.interestedIds.every(id => typeof id === "string") + body.interestedIds !== undefined + && !( + !(body.interestedIds instanceof Array) + || !body.interestedIds.every(id => typeof id === "string") + ) ) { throw createError({ status: 400, @@ -14,16 +18,42 @@ export default defineEventHandler(async (event) => { }); } + if (body.timezone !== undefined) { + if (typeof body.timezone !== "string") { + throw createError({ + status: 400, + message: "Invalid timezone", + }); + } + if (body.timezone.length) { + const zonedTime = DateTime.local().setZone(body.timezone); + if (!zonedTime.isValid) { + throw createError({ + status: 400, + message: "Invalid timezone: " + zonedTime.invalidExplanation, + }); + } + } + } + const accounts = await readAccounts(); const sessionAccount = accounts.find(account => account.id === session.accountId); if (!sessionAccount) { throw Error("Account does not exist"); } - if (body.interestedIds.length) { - sessionAccount.interestedIds = body.interestedIds; - } else { - delete sessionAccount.interestedIds; + if (body.interestedIds !== undefined) { + if (body.interestedIds.length) { + sessionAccount.interestedIds = body.interestedIds; + } else { + delete sessionAccount.interestedIds; + } + } + if (body.timezone !== undefined) { + if (body.timezone) + sessionAccount.timezone = body.timezone; + else + delete sessionAccount.timezone; } await writeAccounts(accounts); diff --git a/shared/types/account.d.ts b/shared/types/account.d.ts index 9a3780a..8662628 100644 --- a/shared/types/account.d.ts +++ b/shared/types/account.d.ts @@ -4,6 +4,7 @@ export interface Account { /** Name of the account. Not present on anonymous accounts */ name?: string, interestedIds?: string[], + timezone?: string, } export interface Subscription {