diff --git a/components.json b/components.json index 767bdc4..8d5d8e6 100644 --- a/components.json +++ b/components.json @@ -1,14 +1,14 @@ { - "$schema": "https://shadcn-svelte.com/schema.json", - "style": "default", - "tailwind": { - "config": "tailwind.config.js", - "css": "src/app.postcss", - "baseColor": "slate" - }, - "aliases": { - "components": "$lib/components", - "utils": "$lib/utils" - }, + "$schema": "https://shadcn-svelte.com/schema.json", + "style": "default", + "tailwind": { + "config": "tailwind.config.js", + "css": "src/lib/styles/app.pcss", + "baseColor": "slate" + }, + "aliases": { + "components": "$lib/components", + "utils": "$lib/utils/ui" + }, "typescript": true -} \ No newline at end of file +} diff --git a/drizzle.config.ts b/drizzle.config.ts index 3f213c5..b90a261 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,6 +1,6 @@ import 'dotenv/config' +import env from '$lib/server/api/common/env' import { defineConfig } from 'drizzle-kit' -import env from './src/env' export default defineConfig({ dialect: 'postgresql', diff --git a/package.json b/package.json index 68033c1..e4997a2 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@faker-js/faker": "^8.4.1", "@melt-ui/pp": "^0.3.2", "@melt-ui/svelte": "^0.83.0", - "@playwright/test": "^1.46.1", + "@playwright/test": "^1.47.0", "@sveltejs/adapter-auto": "^3.2.4", "@sveltejs/enhanced-img": "^0.3.4", "@sveltejs/kit": "^2.5.26", @@ -41,7 +41,7 @@ "drizzle-kit": "^0.23.2", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.43.0", + "eslint-plugin-svelte": "2.36.0-next.13", "just-clone": "^6.2.0", "just-debounce-it": "^3.2.0", "lucia": "3.2.0", @@ -95,7 +95,7 @@ "arctic": "^1.9.2", "bits-ui": "^0.21.13", "boardgamegeekclient": "^1.9.1", - "bullmq": "^5.12.13", + "bullmq": "^5.12.14", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cookie": "^0.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1dc57a..c00d4b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,8 +66,8 @@ importers: specifier: ^1.9.1 version: 1.9.1 bullmq: - specifier: ^5.12.13 - version: 5.12.13 + specifier: ^5.12.14 + version: 5.12.14 class-variance-authority: specifier: ^0.7.0 version: 0.7.0 @@ -181,8 +181,8 @@ importers: specifier: ^0.83.0 version: 0.83.0(svelte@5.0.0-next.175) '@playwright/test': - specifier: ^1.46.1 - version: 1.46.1 + specifier: ^1.47.0 + version: 1.47.0 '@sveltejs/adapter-auto': specifier: ^3.2.4 version: 3.2.4(@sveltejs/kit@2.5.26(@sveltejs/vite-plugin-svelte@3.1.2(svelte@5.0.0-next.175)(vite@5.4.3(@types/node@20.16.5)(sass@1.78.0)))(svelte@5.0.0-next.175)(vite@5.4.3(@types/node@20.16.5)(sass@1.78.0))) @@ -223,8 +223,8 @@ importers: specifier: ^9.1.0 version: 9.1.0(eslint@8.57.0) eslint-plugin-svelte: - specifier: ^2.43.0 - version: 2.43.0(eslint@8.57.0)(svelte@5.0.0-next.175)(ts-node@10.9.2(@types/node@20.16.5)(typescript@5.5.4)) + specifier: 2.36.0-next.13 + version: 2.36.0-next.13(eslint@8.57.0)(svelte@5.0.0-next.175)(ts-node@10.9.2(@types/node@20.16.5)(typescript@5.5.4)) just-clone: specifier: ^6.2.0 version: 6.2.0 @@ -1706,8 +1706,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@playwright/test@1.46.1': - resolution: {integrity: sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==} + '@playwright/test@1.47.0': + resolution: {integrity: sha512-SgAdlSwYVpToI4e/IH19IHHWvoijAYH5hu2MWSXptRypLSnzj51PcGD+rsOXFayde4P9ZLi+loXVwArg6IUkCA==} engines: {node: '>=18'} hasBin: true @@ -2295,8 +2295,8 @@ packages: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} - bullmq@5.12.13: - resolution: {integrity: sha512-bFk0s1U9eQ8vKrhH9zYg/1H0+puSLVXuuq/pIW2jxgUmtLebRUBZr0cHJx35azTf2oPUJ+xXfpfHWaUtm4ZveA==} + bullmq@5.12.14: + resolution: {integrity: sha512-mcSQHq9EY+DKtAP6XSmkP+0f1ifFithcpLTwo8WmUauArE9dxk45Gae3Fls1Nwf0Er9MoaDhPcglfe6LV/XCOg==} bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} @@ -2769,12 +2769,12 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-svelte@2.43.0: - resolution: {integrity: sha512-REkxQWvg2pp7QVLxQNa+dJ97xUqRe7Y2JJbSWkHSuszu0VcblZtXkPBPckkivk99y5CdLw4slqfPylL2d/X4jQ==} + eslint-plugin-svelte@2.36.0-next.13: + resolution: {integrity: sha512-N4bLGdFkGbbAQiKvX17kLfBgnZ+Em00khOY3AReppO7fkP9jaSxwjdgTCcWf+Q5/uZWor58g4GleRqHcb2Dk2w==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0-0 || ^9.0.0-0 - svelte: ^3.37.0 || ^4.0.0 || ^5.0.0-next.191 + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0-next.73 peerDependenciesMeta: svelte: optional: true @@ -3212,8 +3212,8 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} - known-css-properties@0.34.0: - resolution: {integrity: sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ==} + known-css-properties@0.30.0: + resolution: {integrity: sha512-VSWXYUnsPu9+WYKkfmJyLKtIvaRJi1kXUqVmBACORXZQxT5oZDsoZ2vQP+bQFDnWtpI/4eq3MLoRMjI2fnLzTQ==} levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} @@ -3673,13 +3673,13 @@ packages: pkg-types@1.2.0: resolution: {integrity: sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==} - playwright-core@1.46.1: - resolution: {integrity: sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==} + playwright-core@1.47.0: + resolution: {integrity: sha512-1DyHT8OqkcfCkYUD9zzUTfg7EfTd+6a8MkD/NWOvjo0u/SCNd5YmY/lJwFvUZOxJbWNds+ei7ic2+R/cRz/PDg==} engines: {node: '>=18'} hasBin: true - playwright@1.46.1: - resolution: {integrity: sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==} + playwright@1.47.0: + resolution: {integrity: sha512-jOWiRq2pdNAX/mwLiwFYnPHpEZ4rM+fRSQpRHwEwZlP2PUANvL3+aJOF/bvISMhFD30rqMxUB4RJx9aQbfh4Ww==} engines: {node: '>=18'} hasBin: true @@ -5888,9 +5888,9 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@playwright/test@1.46.1': + '@playwright/test@1.47.0': dependencies: - playwright: 1.46.1 + playwright: 1.47.0 '@polka/url@1.0.0-next.25': {} @@ -6508,7 +6508,7 @@ snapshots: builtin-modules@3.3.0: {} - bullmq@5.12.13: + bullmq@5.12.14: dependencies: cron-parser: 4.9.0 ioredis: 5.4.1 @@ -6936,14 +6936,15 @@ snapshots: dependencies: eslint: 8.57.0 - eslint-plugin-svelte@2.43.0(eslint@8.57.0)(svelte@5.0.0-next.175)(ts-node@10.9.2(@types/node@20.16.5)(typescript@5.5.4)): + eslint-plugin-svelte@2.36.0-next.13(eslint@8.57.0)(svelte@5.0.0-next.175)(ts-node@10.9.2(@types/node@20.16.5)(typescript@5.5.4)): dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) '@jridgewell/sourcemap-codec': 1.5.0 + debug: 4.3.6 eslint: 8.57.0 eslint-compat-utils: 0.5.1(eslint@8.57.0) esutils: 2.0.3 - known-css-properties: 0.34.0 + known-css-properties: 0.30.0 postcss: 8.4.45 postcss-load-config: 3.1.4(postcss@8.4.45)(ts-node@10.9.2(@types/node@20.16.5)(typescript@5.5.4)) postcss-safe-parser: 6.0.0(postcss@8.4.45) @@ -6953,6 +6954,7 @@ snapshots: optionalDependencies: svelte: 5.0.0-next.175 transitivePeerDependencies: + - supports-color - ts-node eslint-scope@7.2.2: @@ -7479,7 +7481,7 @@ snapshots: kleur@4.1.5: {} - known-css-properties@0.34.0: {} + known-css-properties@0.30.0: {} levn@0.4.1: dependencies: @@ -7882,11 +7884,11 @@ snapshots: mlly: 1.7.1 pathe: 1.1.2 - playwright-core@1.46.1: {} + playwright-core@1.47.0: {} - playwright@1.46.1: + playwright@1.47.0: dependencies: - playwright-core: 1.46.1 + playwright-core: 1.47.0 optionalDependencies: fsevents: 2.3.2 diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte index 5768ad5..f614316 100644 --- a/src/lib/components/Header.svelte +++ b/src/lib/components/Header.svelte @@ -1,20 +1,20 @@ @@ -40,10 +40,10 @@ Account - + - Profile + Settings diff --git a/src/lib/components/LeftNav.svelte b/src/lib/components/LeftNav.svelte index 40f2ac0..5117a72 100644 --- a/src/lib/components/LeftNav.svelte +++ b/src/lib/components/LeftNav.svelte @@ -1,20 +1,20 @@ + + Settings + + {#each routes as { href, label }} - + {label} @@ -62,8 +62,8 @@ let { children, routes }: { children: unknown; routes: Route[] } = $props() } &.active { - color: #23527c; - font-weight: bold; + color: var(--color-link-hover); + font-weight: 600; background-color: #e9ecef; } } diff --git a/src/lib/components/ui/badge/badge.svelte b/src/lib/components/ui/badge/badge.svelte index 58ec3b0..f581743 100644 --- a/src/lib/components/ui/badge/badge.svelte +++ b/src/lib/components/ui/badge/badge.svelte @@ -1,6 +1,6 @@ - - - {@render children()} - diff --git a/src/routes/(app)/(protected)/profile/security/mfa/+page.svelte b/src/routes/(app)/(protected)/profile/security/mfa/+page.svelte deleted file mode 100644 index 0182587..0000000 --- a/src/routes/(app)/(protected)/profile/security/mfa/+page.svelte +++ /dev/null @@ -1,83 +0,0 @@ - - - -Two-Factor Authentication - -{#if twoFactorEnabled} - Currently you have two factor authentication enabled - To disable two factor authentication, please enter your current password. - - - - Current Password - - - Please enter your current password. - - - Disable Two Factor Authentication - -{:else} - Please scan the following QR Code - - - - - Enter Code - - - This is the code from your authenticator app. - - - - - Enter Password - - - Please enter your current password. - - - Submit - - Secret: {secret} -{/if} - - - \ No newline at end of file diff --git a/src/routes/(app)/(protected)/settings/+layout.svelte b/src/routes/(app)/(protected)/settings/+layout.svelte new file mode 100644 index 0000000..c4620d3 --- /dev/null +++ b/src/routes/(app)/(protected)/settings/+layout.svelte @@ -0,0 +1,15 @@ + + + + {@render children()} + \ No newline at end of file diff --git a/src/routes/(app)/(protected)/settings/+page.server.ts b/src/routes/(app)/(protected)/settings/+page.server.ts new file mode 100644 index 0000000..38077bd --- /dev/null +++ b/src/routes/(app)/(protected)/settings/+page.server.ts @@ -0,0 +1,8 @@ +// +page.server.ts +import { redirect } from '@sveltejs/kit' +import type { PageServerLoad } from './$types' + +export const load: PageServerLoad = async () => { + // Redirect to a different page + throw redirect(307, '/settings/profile') +} diff --git a/src/routes/(app)/(protected)/profile/+page.server.ts b/src/routes/(app)/(protected)/settings/profile/+page.server.ts similarity index 88% rename from src/routes/(app)/(protected)/profile/+page.server.ts rename to src/routes/(app)/(protected)/settings/profile/+page.server.ts index 15e4e46..77cab00 100644 --- a/src/routes/(app)/(protected)/profile/+page.server.ts +++ b/src/routes/(app)/(protected)/settings/profile/+page.server.ts @@ -1,9 +1,6 @@ -import { updateEmailDto } from '$lib/dtos/update-email.dto' -import { updateProfileDto } from '$lib/dtos/update-profile.dto' import { notSignedInMessage } from '$lib/flashMessages' import { usersTable } from '$lib/server/api/databases/tables' import { db } from '$lib/server/api/packages/drizzle' -import { changeEmailSchema, profileSchema } from '$lib/validations/account' import { type Actions, fail } from '@sveltejs/kit' import { eq } from 'drizzle-orm' import { redirect } from 'sveltekit-flash-message/server' @@ -11,6 +8,7 @@ import { zod } from 'sveltekit-superforms/adapters' import { message, setError, superValidate } from 'sveltekit-superforms/server' import { z } from 'zod' import type { PageServerLoad } from './$types' +import { updateEmailSchema, updateProfileSchema } from './schemas' export const load: PageServerLoad = async (event) => { const { locals } = event @@ -28,14 +26,14 @@ export const load: PageServerLoad = async (event) => { // where: eq(usersTable.id, user!.id!), // }); - const profileForm = await superValidate(zod(profileSchema), { + const profileForm = await superValidate(zod(updateProfileSchema), { defaults: { firstName: authedUser?.firstName ?? '', lastName: authedUser?.lastName ?? '', username: authedUser?.username ?? '', }, }) - const emailForm = await superValidate(zod(changeEmailSchema), { + const emailForm = await superValidate(zod(updateEmailSchema), { defaults: { email: authedUser?.email ?? '', }, @@ -66,7 +64,7 @@ export const actions: Actions = { redirect(302, '/login', notSignedInMessage, event) } - const form = await superValidate(event, zod(updateProfileDto)) + const form = await superValidate(event, zod(updateProfileSchema)) const { error } = await locals.api.me.update.profile.$put({ json: form.data }).then(locals.parseApiResponse) console.log('data from profile update', error) @@ -84,7 +82,7 @@ export const actions: Actions = { return message(form, { type: 'success', message: 'Profile updated successfully!' }) }, changeEmail: async (event) => { - const form = await superValidate(event, zod(updateEmailDto)) + const form = await superValidate(event, zod(updateEmailSchema)) const newEmail = form.data?.email if (!form.valid || !newEmail || (newEmail !== '' && !changeEmailIfNotEmpty.safeParse(form.data).success)) { diff --git a/src/routes/(app)/(protected)/profile/+page.svelte b/src/routes/(app)/(protected)/settings/profile/+page.svelte similarity index 73% rename from src/routes/(app)/(protected)/profile/+page.svelte rename to src/routes/(app)/(protected)/settings/profile/+page.svelte index 7434d86..7943447 100644 --- a/src/routes/(app)/(protected)/profile/+page.svelte +++ b/src/routes/(app)/(protected)/settings/profile/+page.svelte @@ -1,27 +1,24 @@ + + + {#if !hasSetupTwoFactor} + Multi Factor Authentication is: Disabled + + + Setup Multi-factor Authentication + + {:else} + Multi Factor Authentication is: Enabled + + + Disable Multi-factor Authentication + + {/if} + + + + + Change Password + + \ No newline at end of file diff --git a/src/routes/(app)/(protected)/profile/security/password/change/+page.server.ts b/src/routes/(app)/(protected)/settings/security/change/password/+page.server.ts similarity index 94% rename from src/routes/(app)/(protected)/profile/security/password/change/+page.server.ts rename to src/routes/(app)/(protected)/settings/security/change/password/+page.server.ts index c2b9f75..e708aa5 100644 --- a/src/routes/(app)/(protected)/profile/security/password/change/+page.server.ts +++ b/src/routes/(app)/(protected)/settings/security/change/password/+page.server.ts @@ -1,6 +1,6 @@ import { notSignedInMessage } from '$lib/flashMessages' +import { usersTable } from '$lib/server/api/databases/tables' import { db } from '$lib/server/api/packages/drizzle' -import { changeUserPasswordSchema } from '$lib/validations/account' import { type Actions, fail } from '@sveltejs/kit' import { eq } from 'drizzle-orm' import type { Cookie } from 'lucia' @@ -8,8 +8,8 @@ import { Argon2id } from 'oslo/password' import { redirect } from 'sveltekit-flash-message/server' import { zod } from 'sveltekit-superforms/adapters' import { setError, superValidate } from 'sveltekit-superforms/server' -import type { PageServerLoad } from '../../../$types' -import { usersTable } from '../../../../../../../lib/server/api/databases/tables' +import type { PageServerLoad } from './$types' +import { changeUserPasswordSchema } from './schemas' export const load: PageServerLoad = async (event) => { const { locals } = event diff --git a/src/routes/(app)/(protected)/profile/security/password/change/+page.svelte b/src/routes/(app)/(protected)/settings/security/change/password/+page.svelte similarity index 67% rename from src/routes/(app)/(protected)/profile/security/password/change/+page.svelte rename to src/routes/(app)/(protected)/settings/security/change/password/+page.svelte index e5c7422..302379a 100644 --- a/src/routes/(app)/(protected)/profile/security/password/change/+page.svelte +++ b/src/routes/(app)/(protected)/settings/security/change/password/+page.svelte @@ -1,22 +1,22 @@ diff --git a/src/routes/(app)/(protected)/settings/security/change/password/schemas.ts b/src/routes/(app)/(protected)/settings/security/change/password/schemas.ts new file mode 100644 index 0000000..ffb4ed8 --- /dev/null +++ b/src/routes/(app)/(protected)/settings/security/change/password/schemas.ts @@ -0,0 +1,81 @@ +import { z } from 'zod' + +export const changeUserPasswordSchema = z + .object({ + current_password: z.string({ required_error: 'Current Password is required' }), + password: z.string({ required_error: 'Password is required' }).trim(), + confirm_password: z.string({ required_error: 'Confirm Password is required' }).trim(), + }) + .superRefine(({ confirm_password, password }, ctx) => { + refinePasswords(confirm_password, password, ctx) + }) + +export type ChangeUserPasswordSchema = typeof changeUserPasswordSchema + +const refinePasswords = async (confirm_password: string, password: string, ctx: z.RefinementCtx) => { + comparePasswords(confirm_password, password, ctx) + checkPasswordStrength(password, ctx) +} + +const comparePasswords = async (confirm_password: string, password: string, ctx: z.RefinementCtx) => { + if (confirm_password !== password) { + ctx.addIssue({ + code: 'custom', + message: 'Password and Confirm Password must match', + path: ['confirm_password'], + }) + } +} + +const checkPasswordStrength = async (password: string, ctx: z.RefinementCtx) => { + const minimumLength = password.length < 8 + const maximumLength = password.length > 128 + const containsUppercase = (ch: string) => /[A-Z]/.test(ch) + const containsLowercase = (ch: string) => /[a-z]/.test(ch) + const containsSpecialChar = (ch: string) => /[`!@#$%^&*()_\-+=\[\]{};':"\\|,.<>\/?~ ]/.test(ch) + let countOfUpperCase = 0 + let countOfLowerCase = 0 + let countOfNumbers = 0 + let countOfSpecialChar = 0 + for (let i = 0; i < password.length; i++) { + const char = password.charAt(i) + if (!Number.isNaN(+char)) { + countOfNumbers++ + } else if (containsUppercase(char)) { + countOfUpperCase++ + } else if (containsLowercase(char)) { + countOfLowerCase++ + } else if (containsSpecialChar(char)) { + countOfSpecialChar++ + } + } + + let errorMessage = 'Your password:' + + if (countOfLowerCase < 1) { + errorMessage = ' Must have at least one lowercase letter. ' + } + if (countOfNumbers < 1) { + errorMessage += ' Must have at least one number. ' + } + if (countOfUpperCase < 1) { + errorMessage += ' Must have at least one uppercase letter. ' + } + if (countOfSpecialChar < 1) { + errorMessage += ' Must have at least one special character.' + } + if (minimumLength) { + errorMessage += ' Be at least 8 characters long.' + } + if (maximumLength) { + errorMessage += ' Be less than 128 characters long.' + } + + if (errorMessage.length > 'Your password:'.length) { + ctx.addIssue({ + code: 'custom', + message: errorMessage, + path: ['password'], + }) + } +} diff --git a/src/routes/(app)/(protected)/settings/security/mfa/+page.server.ts b/src/routes/(app)/(protected)/settings/security/mfa/+page.server.ts new file mode 100644 index 0000000..e87d84b --- /dev/null +++ b/src/routes/(app)/(protected)/settings/security/mfa/+page.server.ts @@ -0,0 +1,25 @@ +import { notSignedInMessage } from '$lib/flashMessages' +import env from '$lib/server/api/common/env' +import { addTwoFactorSchema, removeTwoFactorSchema } from '$lib/validations/account' +import { type Actions, fail } from '@sveltejs/kit' +import kebabCase from 'just-kebab-case' +import { base32, decodeHex } from 'oslo/encoding' +import { createTOTPKeyURI } from 'oslo/otp' +import QRCode from 'qrcode' +import { redirect } from 'sveltekit-flash-message/server' +import { zod } from 'sveltekit-superforms/adapters' +import { setError, superValidate } from 'sveltekit-superforms/server' +import type { PageServerLoad } from '../../$types' + +export const load: PageServerLoad = async (event) => { + const { locals } = event + + const authedUser = await locals.getAuthedUser() + if (!authedUser) { + throw redirect(302, '/login', notSignedInMessage, event) + } + + return {} +} + +export const actions: Actions = {} diff --git a/src/routes/(app)/(protected)/settings/security/mfa/+page.svelte b/src/routes/(app)/(protected)/settings/security/mfa/+page.svelte new file mode 100644 index 0000000..0ed96a6 --- /dev/null +++ b/src/routes/(app)/(protected)/settings/security/mfa/+page.svelte @@ -0,0 +1,57 @@ + + +Two-factor authentication + + + + Two-Factor Methods + + + + + + Authenticator app {#if hardwareTokenEnabled}Configured{/if} + Use an authenticator app or browser extension to get two-factor authentication codes when prompted. + + Edit + + + + Security Keys {#if hardwareTokenEnabled}Configured{/if} + + Security keys are webauthn credentials that can only be used as a second factor of authentication. + + + Edit + + + + + + \ No newline at end of file diff --git a/src/routes/(app)/(protected)/profile/security/mfa/recovery-codes/+page.server.ts b/src/routes/(app)/(protected)/settings/security/mfa/recovery-codes/+page.server.ts similarity index 100% rename from src/routes/(app)/(protected)/profile/security/mfa/recovery-codes/+page.server.ts rename to src/routes/(app)/(protected)/settings/security/mfa/recovery-codes/+page.server.ts diff --git a/src/routes/(app)/(protected)/profile/security/mfa/recovery-codes/+page.svelte b/src/routes/(app)/(protected)/settings/security/mfa/recovery-codes/+page.svelte similarity index 100% rename from src/routes/(app)/(protected)/profile/security/mfa/recovery-codes/+page.svelte rename to src/routes/(app)/(protected)/settings/security/mfa/recovery-codes/+page.svelte diff --git a/src/routes/(app)/(protected)/profile/security/mfa/+page.server.ts b/src/routes/(app)/(protected)/settings/security/mfa/totp/+page.server.ts similarity index 95% rename from src/routes/(app)/(protected)/profile/security/mfa/+page.server.ts rename to src/routes/(app)/(protected)/settings/security/mfa/totp/+page.server.ts index c7fb19e..4d6577a 100644 --- a/src/routes/(app)/(protected)/profile/security/mfa/+page.server.ts +++ b/src/routes/(app)/(protected)/settings/security/mfa/totp/+page.server.ts @@ -1,6 +1,5 @@ import { notSignedInMessage } from '$lib/flashMessages' -import { addTwoFactorSchema, removeTwoFactorSchema } from '$lib/validations/account' -import env from '$src/env' +import env from '$lib/server/api/common/env' import { type Actions, fail } from '@sveltejs/kit' import kebabCase from 'just-kebab-case' import { base32, decodeHex } from 'oslo/encoding' @@ -10,6 +9,7 @@ import { redirect } from 'sveltekit-flash-message/server' import { zod } from 'sveltekit-superforms/adapters' import { setError, superValidate } from 'sveltekit-superforms/server' import type { PageServerLoad } from '../../$types' +import { addTwoFactorSchema, removeTwoFactorSchema } from './schemas' export const load: PageServerLoad = async (event) => { const { locals } = event @@ -125,7 +125,7 @@ export const actions: Actions = { return setError(addTwoFactorForm, 'two_factor_code', 'Invalid code') } - redirect(302, '/profile/security/mfa/recovery-codes') + redirect(302, '/settings/security/mfa/recovery-codes') }, disableTotp: async (event) => { const { locals } = event @@ -162,7 +162,7 @@ export const actions: Actions = { redirect( 302, - '/profile/security/mfa', + '/settings/security/mfa', { type: 'success', message: 'Two-Factor Authentication has been disabled.', diff --git a/src/routes/(app)/(protected)/settings/security/mfa/totp/+page.svelte b/src/routes/(app)/(protected)/settings/security/mfa/totp/+page.svelte new file mode 100644 index 0000000..b52ba0b --- /dev/null +++ b/src/routes/(app)/(protected)/settings/security/mfa/totp/+page.svelte @@ -0,0 +1,83 @@ + + + + Two-Factor Authentication + + {#if twoFactorEnabled} + Currently you have two factor authentication enabled + To disable two factor authentication, please enter your current password. + + + + Current Password + + + Please enter your current password. + + + Disable Two Factor Authentication + + {:else} + Please scan the following QR Code + + + + + Enter Code + + + This is the code from your authenticator app. + + + + + Enter Password + + + Please enter your current password. + + + Submit + + Secret: {secret} + {/if} + + + \ No newline at end of file diff --git a/src/routes/(app)/(protected)/settings/security/mfa/totp/schemas.ts b/src/routes/(app)/(protected)/settings/security/mfa/totp/schemas.ts new file mode 100644 index 0000000..fd14c28 --- /dev/null +++ b/src/routes/(app)/(protected)/settings/security/mfa/totp/schemas.ts @@ -0,0 +1,14 @@ +import { z } from 'zod' + +export const addTwoFactorSchema = z.object({ + current_password: z.string({ required_error: 'Current Password is required' }), + two_factor_code: z.string({ required_error: 'Two Factor Code is required' }).trim(), +}) + +export type AddTwoFactorSchema = typeof addTwoFactorSchema + +export const removeTwoFactorSchema = addTwoFactorSchema.pick({ + current_password: true, +}) + +export type RemoveTwoFactorSchema = typeof removeTwoFactorSchema diff --git a/src/routes/(app)/(protected)/wishlists/+page.server.ts b/src/routes/(app)/(protected)/wishlists/+page.server.ts index d461cfc..f211518 100644 --- a/src/routes/(app)/(protected)/wishlists/+page.server.ts +++ b/src/routes/(app)/(protected)/wishlists/+page.server.ts @@ -90,18 +90,20 @@ export const actions: Actions = { // Create new wishlist create: async (event) => { const { locals } = event - const { user, session } = locals - if (userNotAuthenticated(user, session)) { - return fail(401) + + const authedUser = await locals.getAuthedUser() + if (!authedUser) { + throw redirect(302, '/login', notSignedInMessage, event) } return error(405, 'Method not allowed') }, // Delete a wishlist delete: async (event) => { const { locals } = event - const { user, session } = locals - if (userNotAuthenticated(user, session)) { - return fail(401) + + const authedUser = await locals.getAuthedUser() + if (!authedUser) { + throw redirect(302, '/login', notSignedInMessage, event) } return error(405, 'Method not allowed') }, @@ -116,7 +118,7 @@ export const actions: Actions = { const form = await superValidate(event, zod(modifyListGameSchema)) try { - const game = await db.query.games.findFirst({ + const game = await db.query.gamesTable.findFirst({ where: eq(gamesTable.id, form.data.id), }) @@ -131,7 +133,7 @@ export const actions: Actions = { } if (game) { - const wishlist = await db.query.wishlists.findFirst({ + const wishlist = await db.query.wishlistsTable.findFirst({ where: eq(wishlistsTable.user_id, authedUser.id), }) diff --git a/src/routes/(app)/(protected)/wishlists/[cuid]/+page.server.ts b/src/routes/(app)/(protected)/wishlists/[cuid]/+page.server.ts index e622159..2ec8923 100644 --- a/src/routes/(app)/(protected)/wishlists/[cuid]/+page.server.ts +++ b/src/routes/(app)/(protected)/wishlists/[cuid]/+page.server.ts @@ -51,14 +51,15 @@ export const actions: Actions = { // Add game to a wishlist add: async (event) => { const { locals } = event - const { user, session } = locals - if (userNotAuthenticated(user, session)) { - return fail(401) + + const authedUser = await locals.getAuthedUser() + if (!authedUser) { + throw redirect(302, '/login', notSignedInMessage, event) } const form = await superValidate(event, zod(modifyListGameSchema)) try { - const game = await db.query.games.findFirst({ + const game = await db.query.gamesTable.findFirst({ where: eq(gamesTable.id, form.data.id), }) @@ -73,8 +74,8 @@ export const actions: Actions = { } if (game) { - const wishlist = await db.query.wishlists.findFirst({ - where: eq(wishlistsTable.user_id, user!.id!), + const wishlist = await db.query.wishlistsTable.findFirst({ + where: eq(wishlistsTable.user_id, authedUser.id), }) if (!wishlist) { @@ -99,32 +100,35 @@ export const actions: Actions = { // Create new wishlist create: async (event) => { const { locals } = event - const { user, session } = locals - if (userNotAuthenticated(user, session)) { - return fail(401) + + const authedUser = await locals.getAuthedUser() + if (!authedUser) { + throw redirect(302, '/login', notSignedInMessage, event) } return error(405, 'Method not allowed') }, // Delete a wishlist delete: async (event) => { const { locals } = event - const { user, session } = locals - if (userNotAuthenticated(user, session)) { - return fail(401) + + const authedUser = await locals.getAuthedUser() + if (!authedUser) { + throw redirect(302, '/login', notSignedInMessage, event) } return error(405, 'Method not allowed') }, // Remove game from a wishlist remove: async (event) => { const { locals } = event - const { user, session } = locals - if (userNotAuthenticated(user, session)) { - return fail(401) + + const authedUser = await locals.getAuthedUser() + if (!authedUser) { + throw redirect(302, '/login', notSignedInMessage, event) } const form = await superValidate(event, zod(modifyListGameSchema)) try { - const game = await db.query.games.findFirst({ + const game = await db.query.gamesTable.findFirst({ where: eq(gamesTable.id, form.data.id), }) @@ -139,8 +143,8 @@ export const actions: Actions = { } if (game) { - const wishlist = await db.query.wishlists.findFirst({ - where: eq(wishlistsTable.user_id, user!.id!), + const wishlist = await db.query.wishlistsTable.findFirst({ + where: eq(wishlistsTable.user_id, authedUser.id), }) if (!wishlist) { diff --git a/src/routes/(auth)/login/+page.server.ts b/src/routes/(auth)/login/+page.server.ts index 3672bd7..a2b90e9 100644 --- a/src/routes/(auth)/login/+page.server.ts +++ b/src/routes/(auth)/login/+page.server.ts @@ -47,7 +47,9 @@ export const actions: Actions = { const form = await superValidate(event, zod(signinUsernameDto)) const { error } = await locals.api.login.$post({ json: form.data }).then(locals.parseApiResponse) - if (error) return setError(form, 'username', error) + if (error) { + return setError(form, 'username', error) + } if (!form.valid) { form.data.password = '' diff --git a/src/routes/(auth)/password/reset/+page.server.ts b/src/routes/(auth)/password/reset/+page.server.ts index 6b2a20f..70de114 100644 --- a/src/routes/(auth)/password/reset/+page.server.ts +++ b/src/routes/(auth)/password/reset/+page.server.ts @@ -1,57 +1,56 @@ -import { fail, error, type Actions } from '@sveltejs/kit'; -import { zod } from 'sveltekit-superforms/adapters'; -import { setError, superValidate } from 'sveltekit-superforms/server'; -import { redirect } from 'sveltekit-flash-message/server'; -import type { PageServerLoad } from './$types'; -import {resetPasswordEmailSchema, resetPasswordTokenSchema} from "$lib/validations/auth"; -import {StatusCodes} from "$lib/constants/status-codes"; -import {userFullyAuthenticated} from "$lib/server/auth-utils"; +import { StatusCodes } from '$lib/constants/status-codes' +import { notSignedInMessage } from '$lib/flashMessages' +import { resetPasswordEmailSchema, resetPasswordTokenSchema } from '$lib/validations/auth' +import { type Actions, error, fail } from '@sveltejs/kit' +import { redirect } from 'sveltekit-flash-message/server' +import { zod } from 'sveltekit-superforms/adapters' +import { setError, superValidate } from 'sveltekit-superforms/server' +import type { PageServerLoad } from './$types' export const load: PageServerLoad = async () => { return { emailForm: await superValidate(zod(resetPasswordEmailSchema)), tokenForm: await superValidate(zod(resetPasswordTokenSchema)), - }; -}; + } +} export const actions = { passwordReset: async (event) => { - const { request, locals } = event; - const { user, session } = locals; + const { request, locals } = event - if (userFullyAuthenticated(user, session)) { - const message = { type: 'success', message: 'You are already signed in' } as const; - throw redirect('/', message, event); + const authedUser = await locals.getAuthedUser() + if (!authedUser) { + throw redirect(302, '/login', notSignedInMessage, event) } - const emailForm = await superValidate(request, zod(resetPasswordEmailSchema)); + const emailForm = await superValidate(request, zod(resetPasswordEmailSchema)) if (!emailForm.valid) { - return fail(StatusCodes.BAD_REQUEST, { emailForm }); + return fail(StatusCodes.BAD_REQUEST, { emailForm }) } // const error = {}; // // const { error } = await locals.api.iam.login.request.$post({ json: emailRegisterForm.data }).then(locals.parseApiResponse); // if (error) { // return setError(emailForm, 'email', error); // } - return { emailForm }; + return { emailForm } }, verifyToken: async (event) => { - const { request, locals } = event; - const { user, session } = locals; - if (userFullyAuthenticated(user, session)) { - const message = { type: 'success', message: 'You are already signed in' } as const; - throw redirect('/', message, event); + const { request, locals } = event + + const authedUser = await locals.getAuthedUser() + if (!authedUser) { + throw redirect(302, '/login', notSignedInMessage, event) } - const tokenForm = await superValidate(request, zod(resetPasswordTokenSchema)); + const tokenForm = await superValidate(request, zod(resetPasswordTokenSchema)) if (!tokenForm.valid) { - return fail(StatusCodes.BAD_REQUEST, { tokenForm }); + return fail(StatusCodes.BAD_REQUEST, { tokenForm }) } - const error = {}; + const error = {} // const { error } = await locals.api.iam.login.verify.$post({ json: emailSignInForm.data }).then(locals.parseApiResponse) if (error) { - return setError(tokenForm, 'token', error); + return setError(tokenForm, 'token', error) } - redirect(301, '/'); - } -}; + redirect(301, '/') + }, +} diff --git a/src/routes/(auth)/totp/+page.server.ts b/src/routes/(auth)/totp/+page.server.ts index 480b36b..9b05025 100644 --- a/src/routes/(auth)/totp/+page.server.ts +++ b/src/routes/(auth)/totp/+page.server.ts @@ -1,4 +1,5 @@ import { notSignedInMessage } from '$lib/flashMessages' +import env from '$lib/server/api/common/env' import { twoFactorTable, usersTable } from '$lib/server/api/databases/tables' import { db } from '$lib/server/api/packages/drizzle' import { recoveryCodeSchema, totpSchema } from '$lib/validations/auth' @@ -9,7 +10,6 @@ import { redirect } from 'sveltekit-flash-message/server' import { RateLimiter } from 'sveltekit-rate-limiter/server' import { zod } from 'sveltekit-superforms/adapters' import { superValidate } from 'sveltekit-superforms/server' -import env from '../../../env' import type { PageServerLoad, RequestEvent } from './$types' export const load: PageServerLoad = async (event) => { diff --git a/svelte.config.js b/svelte.config.js index 7ead23b..71dd300 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -25,6 +25,7 @@ const config = { $db: './src/lib/server/api/infrastructure/database', $server: './src/server', $lib: './src/lib', + $routes: './src/routes', $src: './src', $state: './src/state', $styles: './src/styles',
To disable two factor authentication, please enter your current password.
Multi Factor Authentication is: Disabled
Multi Factor Authentication is: Enabled
Use an authenticator app or browser extension to get two-factor authentication codes when prompted.
+ Security keys are webauthn credentials that can only be used as a second factor of authentication. +