Adding mfa page instead of 2FA, starting controller based password verification and totp generation.

This commit is contained in:
Bradley Shellnut 2024-08-29 16:12:40 -07:00
parent ead20829e4
commit df582f1534
44 changed files with 443 additions and 332 deletions

View file

@ -1,6 +1,6 @@
import { error, json } from '@sveltejs/kit';
export async function GET({ url, locals, params }) {
const searchParams = Object.fromEntries(url.searchParams);
return json({});
}
import { error, json } from '@sveltejs/kit';
export async function GET({ url, locals, params }) {
const searchParams = Object.fromEntries(url.searchParams);
return json({});
}

View file

@ -1,14 +1,14 @@
import { error, json } from '@sveltejs/kit';
import { eq } from 'drizzle-orm';
import db from '../../../../../db';
import {db} from '$lib/server/api/infrastructure/database';
import { collection_items, usersTable } from '$db/schema';
// Search a user's collection
export async function GET({ url, locals, params }) {
const searchParams = Object.fromEntries(url.searchParams);
const q = searchParams?.q || '';
const limit = parseInt(searchParams?.limit) || 10;
const skip = parseInt(searchParams?.skip) || 0;
const limit = Number.parseInt(searchParams?.limit) || 10;
const skip = Number.parseInt(searchParams?.skip) || 0;
const order = searchParams?.order || 'asc';
const sort = searchParams?.sort || 'name';
const collection_id = params.id;

View file

@ -1,7 +1,7 @@
import db from '../../../../db';
import { db } from '$lib/server/api/infrastructure/database';
import { error } from '@sveltejs/kit';
import { eq } from 'drizzle-orm';
import { usersTable } from '$db/schema';
import { usersTable } from '$lib/server/api/infrastructure/database/tables';
import { createPasswordResetToken } from '$lib/server/auth-utils.js';
import { PUBLIC_SITE_URL } from '$env/static/public';

View file

@ -1,9 +1,8 @@
import { eq } from 'drizzle-orm';
import { password_reset_tokens, usersTable } from '$db/schema';
import { password_reset_tokens } from '$lib/server/api/infrastructure/database/tables';
import { isWithinExpirationDate } from 'oslo';
import { lucia } from '$lib/server/auth.js';
import { Argon2id } from 'oslo/password';
import db from '$db';
// import { lucia } from '$lib/server/lucia';
import {db} from '$lib/server/api/infrastructure/database';
export async function POST({ request, params }) {
const { password } = await request.json();
@ -32,12 +31,12 @@ export async function POST({ request, params }) {
});
}
await lucia.invalidateUserSessions(token.user_id);
const hashPassword = await new Argon2id().hash(password);
// await db.update(usersTable).set({ hashed_password: hashPassword }).where(eq(usersTable.id, token.user_id));
const session = await lucia.createSession(token.user_id, {});
const sessionCookie = lucia.createSessionCookie(session.id);
// await lucia.invalidateUserSessions(token.user_id);
// const hashPassword = await new Argon2id().hash(password);
// // await db.update(usersTable).set({ hashed_password: hashPassword }).where(eq(usersTable.id, token.user_id));
//
// const session = await lucia.createSession(token.user_id, {});
// const sessionCookie = lucia.createSessionCookie(session.id);
return new Response(null, {
status: 302,

View file

@ -1,6 +1,6 @@
import { error, json } from '@sveltejs/kit';
export async function GET({ url, locals, params }) {
const searchParams = Object.fromEntries(url.searchParams);
return json({});
}
import { error, json } from '@sveltejs/kit';
export async function GET({ url, locals, params }) {
const searchParams = Object.fromEntries(url.searchParams);
return json({});
}

View file

@ -59,7 +59,7 @@
"svelte": "5.0.0-next.175",
"svelte-check": "^3.8.6",
"svelte-headless-table": "^0.18.2",
"svelte-meta-tags": "^3.1.3",
"svelte-meta-tags": "^3.1.4",
"svelte-preprocess": "^6.0.2",
"svelte-sequential-preprocessor": "^2.0.1",
"sveltekit-flash-message": "^2.4.4",
@ -68,7 +68,7 @@
"tailwindcss": "^3.4.10",
"ts-node": "^10.9.2",
"tslib": "^2.7.0",
"tsx": "^4.18.0",
"tsx": "^4.19.0",
"typescript": "^5.5.4",
"vite": "^5.4.2",
"vitest": "^1.6.0",
@ -106,7 +106,7 @@
"feather-icons": "^4.29.2",
"formsnap": "^1.0.1",
"handlebars": "^4.7.8",
"hono": "^4.5.8",
"hono": "^4.5.9",
"hono-rate-limiter": "^0.4.0",
"html-entities": "^2.5.2",
"iconify-icon": "^2.1.0",

View file

@ -13,13 +13,13 @@ importers:
version: 5.0.14
'@hono/swagger-ui':
specifier: ^0.4.1
version: 0.4.1(hono@4.5.8)
version: 0.4.1(hono@4.5.9)
'@hono/zod-openapi':
specifier: ^0.15.3
version: 0.15.3(hono@4.5.8)(zod@3.23.8)
version: 0.15.3(hono@4.5.9)(zod@3.23.8)
'@hono/zod-validator':
specifier: ^0.2.2
version: 0.2.2(hono@4.5.8)(zod@3.23.8)
version: 0.2.2(hono@4.5.9)(zod@3.23.8)
'@iconify-icons/line-md':
specifier: ^1.2.30
version: 1.2.30
@ -99,11 +99,11 @@ importers:
specifier: ^4.7.8
version: 4.7.8
hono:
specifier: ^4.5.8
version: 4.5.8
specifier: ^4.5.9
version: 4.5.9
hono-rate-limiter:
specifier: ^0.4.0
version: 0.4.0(hono@4.5.8)
version: 0.4.0(hono@4.5.9)
html-entities:
specifier: ^2.5.2
version: 2.5.2
@ -248,7 +248,7 @@ importers:
version: 16.1.0(postcss@8.4.41)
postcss-load-config:
specifier: ^5.1.0
version: 5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.18.0)
version: 5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0)
postcss-preset-env:
specifier: ^9.6.0
version: 9.6.0(postcss@8.4.41)
@ -272,16 +272,16 @@ importers:
version: 5.0.0-next.175
svelte-check:
specifier: ^3.8.6
version: 3.8.6(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.18.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175)
version: 3.8.6(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175)
svelte-headless-table:
specifier: ^0.18.2
version: 0.18.2(svelte@5.0.0-next.175)
svelte-meta-tags:
specifier: ^3.1.3
version: 3.1.3(svelte@5.0.0-next.175)(typescript@5.5.4)
specifier: ^3.1.4
version: 3.1.4(svelte@5.0.0-next.175)(typescript@5.5.4)
svelte-preprocess:
specifier: ^6.0.2
version: 6.0.2(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.18.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175)(typescript@5.5.4)
version: 6.0.2(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175)(typescript@5.5.4)
svelte-sequential-preprocessor:
specifier: ^2.0.1
version: 2.0.1
@ -304,8 +304,8 @@ importers:
specifier: ^2.7.0
version: 2.7.0
tsx:
specifier: ^4.18.0
version: 4.18.0
specifier: ^4.19.0
version: 4.19.0
typescript:
specifier: ^5.5.4
version: 5.5.4
@ -3178,8 +3178,8 @@ packages:
peerDependencies:
hono: ^4.1.1
hono@4.5.8:
resolution: {integrity: sha512-pqpSlcdqGkpTTRpLYU1PnCz52gVr0zVR9H5GzMyJWuKQLLEBQxh96q45QizJ2PPX8NATtz2mu31/PKW/Jt+90Q==}
hono@4.5.9:
resolution: {integrity: sha512-zz8ktqMDRrZETjxBrv8C5PQRFbrTRCLNVAjD1SNQyOzv4VjmX68Uxw83xQ6oxdAB60HiWnGEatiKA8V3SZLDkQ==}
engines: {node: '>=16.0.0'}
html-entities@2.5.2:
@ -4493,8 +4493,8 @@ packages:
svelte-lazy-loader@1.0.0:
resolution: {integrity: sha512-AZD6R60vksyojn21FgXLglmBiBB9K5Dkdu0hdGrLbCaRCYT68IsWkZfRUqKhMx1IfzqWcZQ8X9y/f+Ih0oNQkQ==}
svelte-meta-tags@3.1.3:
resolution: {integrity: sha512-iIdJgxKdMUqFGR4m88jBE9KTSO2jdKE5CRjyRtAjdevW51jL4TtDZwL7GOtr5Fd2dw/+jyQIPD7APATP191qIA==}
svelte-meta-tags@3.1.4:
resolution: {integrity: sha512-TUIfhut0iVeTm7f5v/ZuU/tZ9XsNig9bNN8yK0t2x2WL9qw6AxAVRe9i5XddYJE0SuVwkoDCzjoSg5hXv7oWbQ==}
peerDependencies:
svelte: ^3.55.0 || ^4.0.0
@ -4737,8 +4737,8 @@ packages:
tslib@2.7.0:
resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==}
tsx@4.18.0:
resolution: {integrity: sha512-a1jaKBSVQkd6yEc1/NI7G6yHFfefIcuf3QJST7ZEyn4oQnxLYrZR5uZAM8UrwUa3Ge8suiZHcNS1gNrEvmobqg==}
tsx@4.19.0:
resolution: {integrity: sha512-bV30kM7bsLZKZIOCHeMNVMJ32/LuJzLVajkQI/qf92J2Qr08ueLQvW00PUZGiuLPP760UINwupgUj8qrSCPUKg==}
engines: {node: '>=18.0.0'}
hasBin: true
@ -5695,20 +5695,20 @@ snapshots:
'@hapi/hoek': 9.3.0
optional: true
'@hono/swagger-ui@0.4.1(hono@4.5.8)':
'@hono/swagger-ui@0.4.1(hono@4.5.9)':
dependencies:
hono: 4.5.8
hono: 4.5.9
'@hono/zod-openapi@0.15.3(hono@4.5.8)(zod@3.23.8)':
'@hono/zod-openapi@0.15.3(hono@4.5.9)(zod@3.23.8)':
dependencies:
'@asteasolutions/zod-to-openapi': 7.1.1(zod@3.23.8)
'@hono/zod-validator': 0.2.2(hono@4.5.8)(zod@3.23.8)
hono: 4.5.8
'@hono/zod-validator': 0.2.2(hono@4.5.9)(zod@3.23.8)
hono: 4.5.9
zod: 3.23.8
'@hono/zod-validator@0.2.2(hono@4.5.8)(zod@3.23.8)':
'@hono/zod-validator@0.2.2(hono@4.5.9)(zod@3.23.8)':
dependencies:
hono: 4.5.8
hono: 4.5.9
zod: 3.23.8
'@humanwhocodes/config-array@0.11.14':
@ -7562,11 +7562,11 @@ snapshots:
hex-rgb@4.3.0: {}
hono-rate-limiter@0.4.0(hono@4.5.8):
hono-rate-limiter@0.4.0(hono@4.5.9):
dependencies:
hono: 4.5.8
hono: 4.5.9
hono@4.5.8: {}
hono@4.5.9: {}
html-entities@2.5.2: {}
@ -8301,14 +8301,14 @@ snapshots:
postcss: 8.4.41
ts-node: 10.9.2(@types/node@20.16.1)(typescript@5.5.4)
postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.18.0):
postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0):
dependencies:
lilconfig: 3.1.1
yaml: 2.4.2
optionalDependencies:
jiti: 1.21.6
postcss: 8.4.41
tsx: 4.18.0
tsx: 4.19.0
postcss-logical@7.0.1(postcss@8.4.41):
dependencies:
@ -8878,14 +8878,14 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
svelte-check@3.8.6(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.18.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175):
svelte-check@3.8.6(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175):
dependencies:
'@jridgewell/trace-mapping': 0.3.25
chokidar: 3.6.0
picocolors: 1.0.0
sade: 1.8.1
svelte: 5.0.0-next.175
svelte-preprocess: 5.1.4(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.18.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175)(typescript@5.5.4)
svelte-preprocess: 5.1.4(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175)(typescript@5.5.4)
typescript: 5.5.4
transitivePeerDependencies:
- '@babel/core'
@ -8930,7 +8930,7 @@ snapshots:
svelte-lazy-loader@1.0.0: {}
svelte-meta-tags@3.1.3(svelte@5.0.0-next.175)(typescript@5.5.4):
svelte-meta-tags@3.1.4(svelte@5.0.0-next.175)(typescript@5.5.4):
dependencies:
schema-dts: 1.1.2(typescript@5.5.4)
svelte: 5.0.0-next.175
@ -8941,7 +8941,7 @@ snapshots:
dependencies:
svelte: 5.0.0-next.175
svelte-preprocess@5.1.4(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.18.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175)(typescript@5.5.4):
svelte-preprocess@5.1.4(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175)(typescript@5.5.4):
dependencies:
'@types/pug': 2.0.10
detect-indent: 6.1.0
@ -8951,16 +8951,16 @@ snapshots:
svelte: 5.0.0-next.175
optionalDependencies:
postcss: 8.4.41
postcss-load-config: 5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.18.0)
postcss-load-config: 5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0)
sass: 1.77.8
typescript: 5.5.4
svelte-preprocess@6.0.2(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.18.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175)(typescript@5.5.4):
svelte-preprocess@6.0.2(postcss-load-config@5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0))(postcss@8.4.41)(sass@1.77.8)(svelte@5.0.0-next.175)(typescript@5.5.4):
dependencies:
svelte: 5.0.0-next.175
optionalDependencies:
postcss: 8.4.41
postcss-load-config: 5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.18.0)
postcss-load-config: 5.1.0(jiti@1.21.6)(postcss@8.4.41)(tsx@4.19.0)
sass: 1.77.8
typescript: 5.5.4
@ -9174,7 +9174,7 @@ snapshots:
tslib@2.7.0: {}
tsx@4.18.0:
tsx@4.19.0:
dependencies:
esbuild: 0.23.0
get-tsconfig: 4.7.5

View file

@ -92,7 +92,7 @@
</DropdownMenu.Root>
{:else}
<a href="/login"> <span class="flex-auto">Login</span></a>
<a href="/sign-up"> <span class="flex-auto">Sign Up</span></a>
<a href="/signup"> <span class="flex-auto">Sign Up</span></a>
{/if}
</nav>
</header>

View file

@ -13,7 +13,7 @@
// $: termsValue = $form.terms as Writable<boolean>;
</script>
<form method="POST" action="/sign-up" use:enhance>
<form method="POST" action="/signup" use:enhance>
<h1>Signup user</h1>
<label class="label">
<span class="sr-only">First Name</span>

View file

@ -0,0 +1,7 @@
import { z } from 'zod'
export const verifyPasswordDto = z.object({
password: z.string({ required_error: 'Password is required' }).trim(),
})
export type VerifyPasswordDto = z.infer<typeof verifyPasswordDto>

View file

@ -11,6 +11,7 @@ import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware'
import { updateProfileDto } from '$lib/dtos/update-profile.dto'
import { updateEmailDto } from '$lib/dtos/update-email.dto'
import { StatusCodes } from '$lib/constants/status-codes'
import { verifyPasswordDto } from '$lib/dtos/verify-password.dto'
@injectable()
export class IamController implements Controller {
@ -36,6 +37,15 @@ export class IamController implements Controller {
}
return c.json({ user: updatedUser }, StatusCodes.OK)
})
.post('/verify/password', requireAuth, zValidator('json', verifyPasswordDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
const user = c.var.user
const { password } = c.req.valid('json')
const passwordVerified = await this.iamService.verifyPassword(user.id, { password })
if (!passwordVerified) {
return c.json('Incorrect password', StatusCodes.BAD_REQUEST)
}
return c.json({ }, StatusCodes.OK)
})
.post('/update/email', requireAuth, zValidator('json', updateEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
const user = c.var.user
const { email } = c.req.valid('json')

View file

@ -0,0 +1,39 @@
import 'reflect-metadata';
import { Hono } from 'hono';
import { inject, injectable } from 'tsyringe';
import { requireAuth } from "../middleware/auth.middleware";
import type { HonoTypes } from '../types';
import type { Controller } from '$lib/server/api/interfaces/controller.interface';
import { TotpService } from '$lib/server/api/services/totp.service';
import {StatusCodes} from "$lib/constants/status-codes";
@injectable()
export class MfaController implements Controller {
controller = new Hono<HonoTypes>();
constructor(
@inject(TotpService) private readonly totpService: TotpService
) {
}
routes() {
return this.controller
.get('/totp', requireAuth, async (c) => {
const user = c.var.user;
const totpCredential = await this.totpService.findOneByUserId(user.id);
return c.json({ totpCredential });
})
.post('/totp', requireAuth, async (c) => {
const user = c.var.user;
const totpCredential = await this.totpService.create(user.id);
return c.json({ totpCredential })
})
.delete('/totp', requireAuth, async (c) => {
const user = c.var.user;
await this.totpService.deleteOneByUserId(user.id);
return c.status(StatusCodes.NO_CONTENT);
});
}
}

View file

@ -8,6 +8,7 @@ import { config } from './common/config';
import { container } from 'tsyringe';
import { IamController } from './controllers/iam.controller';
import { LoginController } from './controllers/login.controller';
import { MfaController} from "$lib/server/api/controllers/mfa.controller";
import {UserController} from "$lib/server/api/controllers/user.controller";
import {SignupController} from "$lib/server/api/controllers/signup.controller";
import {WishlistController} from "$lib/server/api/controllers/wishlist.controller";
@ -44,6 +45,7 @@ const routes = app
.route('/signup', container.resolve(SignupController).routes())
.route('/wishlists', container.resolve(WishlistController).routes())
.route('/collections', container.resolve(CollectionController).routes())
.route('/mfa', container.resolve(MfaController).routes())
.get('/', (c) => c.json({ message: 'Server is healthy' }));
/* -------------------------------------------------------------------------- */

View file

@ -31,7 +31,7 @@ for (const table of [
schema.publishers,
schema.publishersToExternalIds,
schema.publishers_to_games,
schema.recovery_codes,
schema.recoveryCodesTable,
schema.roles,
schema.sessionsTable,
schema.twoFactorTable,

View file

@ -16,7 +16,7 @@ export * from './passwordResetTokens';
export * from './publishers';
export * from './publishersToExternalIds';
export * from './publishersToGames';
export * from './recoveryCodes';
export * from './recovery-codes.table';
export * from './roles';
export * from './sessions.table';
export * from './two-factor.table';

View file

@ -3,7 +3,7 @@ import type { InferSelectModel } from 'drizzle-orm';
import { usersTable } from './users.table';
import { timestamps } from '../utils';
export const recovery_codes = pgTable('recovery_codes', {
export const recoveryCodesTable = pgTable('recovery_codes', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id')
.notNull()
@ -13,4 +13,4 @@ export const recovery_codes = pgTable('recovery_codes', {
...timestamps,
});
export type RecoveryCodes = InferSelectModel<typeof recovery_codes>;
export type RecoveryCodesTable = InferSelectModel<typeof recoveryCodesTable>;

View file

@ -17,6 +17,15 @@ export class CredentialsRepository {
});
}
async findOneByUserIdAndType(userId: string, type: CredentialsType) {
return this.db.query.credentialsTable.findFirst({
where: and(
eq(credentialsTable.user_id, userId),
eq(credentialsTable.type, type)
)
});
}
async findPasswordCredentialsByUserId(userId: string) {
return this.db.query.credentialsTable.findFirst({
where: and(
@ -59,4 +68,16 @@ export class CredentialsRepository {
.returning()
.then(takeFirstOrThrow);
}
async delete(id: string) {
return this.db
.delete(credentialsTable)
.where(eq(credentialsTable.id, id));
}
async deleteByUserId(userId: string) {
return this.db
.delete(credentialsTable)
.where(eq(credentialsTable.user_id, userId));
}
}

View file

@ -66,4 +66,13 @@ export class IamService {
email,
});
}
async verifyPassword(userId: string, data: VerifyPasswordDto) {
const user = await this.usersService.findOneById(userId);
if (!user) {
return null;
}
const { password } = data;
return this.usersService.verifyPassword(userId, { password });
}
}

View file

@ -0,0 +1,36 @@
import { inject, injectable } from "tsyringe";
import { HMAC } from 'oslo/crypto';
import { encodeHex } from 'oslo/encoding';
import {CredentialsRepository} from "$lib/server/api/repositories/credentials.repository";
@injectable()
export class TotpService {
constructor(
@inject(CredentialsRepository) private readonly credentialsRepository: CredentialsRepository
) {
}
async findOneByUserId(userId: string) {
return this.credentialsRepository.findTOTPCredentialsByUserId(userId);
}
async create(userId: string) {
const twoFactorSecret = await new HMAC('SHA-1').generateKey();
try {
return await this.credentialsRepository.create({
user_id: userId,
secret_data: encodeHex(twoFactorSecret),
type: 'totp'
});
} catch (e) {
console.error(e);
return null;
}
}
async deleteOneByUserId(userId: string) {
return this.credentialsRepository.deleteByUserId(userId);
}
}

View file

@ -68,4 +68,17 @@ export class UsersService {
async findOneById(id: string) {
return this.usersRepository.findOneById(id);
}
async verifyPassword(userId: string, data: { password: string }) {
const user = await this.usersRepository.findOneById(userId);
if (!user) {
throw new Error('User not found');
}
const credential = await this.credentialsRepository.findOneByUserIdAndType(userId, CredentialsType.PASSWORD);
if (!credential) {
throw new Error('Password credentials not found');
}
const { password } = data;
return this.tokenService.verifyHashedToken(credential.secret_data, password);
}
}

View file

@ -1,20 +1,20 @@
import { redirect } from 'sveltekit-flash-message/server';
import { eq } from 'drizzle-orm';
import db from '../../../../db';
import { wishlists } from '$db/schema';
import { userNotAuthenticated } from '$lib/server/auth-utils';
import { db } from '$lib/server/api/infrastructure/database';
import { wishlists } from '$lib/server/api/infrastructure/database/tables';
import { notSignedInMessage } from '$lib/flashMessages';
export async function load(event) {
const { locals } = event;
const { user, session } = locals;
if (userNotAuthenticated(user, session)) {
redirect(302, '/login', notSignedInMessage, event);
const authedUser = await locals.getAuthedUser();
if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event);
}
try {
const dbWishlists = await db.query.wishlists.findMany({
where: eq(wishlists.user_id, user!.id!),
where: eq(wishlists.user_id, authedUser.id),
});
return {

View file

@ -10,129 +10,129 @@ import { db } from '$lib/server/api/infrastructure/database';
import type { PageServerLoad } from './$types';
import { usersTable, credentialsTable } from '$lib/server/api/infrastructure/database/tables';
import { userNotAuthenticated } from '$lib/server/auth-utils';
import {updateProfileDto} from "$lib/dtos/update-profile.dto";
import {updateEmailDto} from "$lib/dtos/update-email.dto";
import { updateProfileDto } from "$lib/dtos/update-profile.dto";
import { updateEmailDto } from "$lib/dtos/update-email.dto";
export const load: PageServerLoad = async (event) => {
const { locals } = event;
const { locals } = event;
const authedUser = await locals.getAuthedUser();
if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event);
}
const authedUser = await locals.getAuthedUser();
if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event);
}
console.log('authedUser', authedUser);
// if (userNotAuthenticated(user, session)) {
// redirect(302, '/login', notSignedInMessage, event);
// }
// const dbUser = await db.query.usersTable.findFirst({
// where: eq(usersTable.id, user!.id!),
// });
console.log('authedUser', authedUser);
// if (userNotAuthenticated(user, session)) {
// redirect(302, '/login', notSignedInMessage, event);
// }
// const dbUser = await db.query.usersTable.findFirst({
// where: eq(usersTable.id, user!.id!),
// });
const profileForm = await superValidate(zod(profileSchema), {
defaults: {
firstName: authedUser?.firstName ?? '',
lastName: authedUser?.lastName ?? '',
username: authedUser?.username ?? '',
},
});
const emailForm = await superValidate(zod(changeEmailSchema), {
defaults: {
email: authedUser?.email ?? '',
},
});
const profileForm = await superValidate(zod(profileSchema), {
defaults: {
firstName: authedUser?.firstName ?? '',
lastName: authedUser?.lastName ?? '',
username: authedUser?.username ?? '',
},
});
const emailForm = await superValidate(zod(changeEmailSchema), {
defaults: {
email: authedUser?.email ?? '',
},
});
// const twoFactorDetails = await db.query.twoFactor.findFirst({
// where: eq(twoFactor.userId, authedUser!.id!),
// });
// const twoFactorDetails = await db.query.twoFactor.findFirst({
// where: eq(twoFactor.userId, authedUser!.id!),
// });
return {
profileForm,
emailForm,
hasSetupTwoFactor: false //!!twoFactorDetails?.enabled,
};
return {
profileForm,
emailForm,
hasSetupTwoFactor: false //!!twoFactorDetails?.enabled,
};
};
const changeEmailIfNotEmpty = z.object({
email: z
.string()
.trim()
.max(64, { message: 'Email must be less than 64 characters' })
.email({ message: 'Please enter a valid email' }),
email: z
.string()
.trim()
.max(64, { message: 'Email must be less than 64 characters' })
.email({ message: 'Please enter a valid email' }),
});
export const actions: Actions = {
profileUpdate: async (event) => {
const { locals } = event;
profileUpdate: async (event) => {
const { locals } = event;
const authedUser = await locals.getAuthedUser();
const authedUser = await locals.getAuthedUser();
if (!authedUser) {
redirect(302, '/login', notSignedInMessage, event);
}
if (!authedUser) {
redirect(302, '/login', notSignedInMessage, event);
}
const form = await superValidate(event, zod(updateProfileDto));
const form = await superValidate(event, zod(updateProfileDto));
const { error } = await locals.api.me.update.profile.$put({ json: form.data }).then(locals.parwseApiResponse);
console.log('data from profile update', error);
if (error) {
return setError(form, 'username', error);
}
const { error } = await locals.api.me.update.profile.$put({ json: form.data }).then(locals.parseApiResponse);
console.log('data from profile update', error);
if (error) {
return setError(form, 'username', error);
}
if (!form.valid) {
return fail(400, {
form,
});
}
if (!form.valid) {
return fail(400, {
form,
});
}
console.log('profile updated successfully');
return message(form, { type: 'success', message: 'Profile updated successfully!' });
},
changeEmail: async (event) => {
const form = await superValidate(event, zod(updateEmailDto));
console.log('profile updated successfully');
return message(form, { type: 'success', message: 'Profile updated successfully!' });
},
changeEmail: async (event) => {
const form = await superValidate(event, zod(updateEmailDto));
const newEmail = form.data?.email;
if (
!form.valid ||
!newEmail ||
(newEmail !== '' && !changeEmailIfNotEmpty.safeParse(form.data).success)
) {
return fail(400, {
form,
});
}
const newEmail = form.data?.email;
if (
!form.valid ||
!newEmail ||
(newEmail !== '' && !changeEmailIfNotEmpty.safeParse(form.data).success)
) {
return fail(400, {
form,
});
}
if (!event.locals.user) {
redirect(302, '/login', notSignedInMessage, event);
}
if (!event.locals.user) {
redirect(302, '/login', notSignedInMessage, event);
}
const user = event.locals.user;
const existingUser = await db.query.usersTable.findFirst({
where: eq(usersTable.email, newEmail),
});
const user = event.locals.user;
const existingUser = await db.query.usersTable.findFirst({
where: eq(usersTable.email, newEmail),
});
if (existingUser && existingUser.id !== user.id) {
return setError(form, 'email', 'That email is already taken');
}
if (existingUser && existingUser.id !== user.id) {
return setError(form, 'email', 'That email is already taken');
}
await db.update(usersTable).set({ email: form.data.email }).where(eq(usersTable.id, user.id));
await db.update(usersTable).set({ email: form.data.email }).where(eq(usersTable.id, user.id));
// if (user.email !== form.data.email) {
// Send email to confirm new email?
// auth.update
// await locals.prisma.key.update({
// where: {
// id: 'emailpassword:' + user.email
// },
// data: {
// id: 'emailpassword:' + form.data.email
// }
// });
// auth.updateUserAttributes(user.user_id, {
// receiveEmail: false
// });
// }
// if (user.email !== form.data.email) {
// Send email to confirm new email?
// auth.update
// await locals.prisma.key.update({
// where: {
// id: 'emailpassword:' + user.email
// },
// data: {
// id: 'emailpassword:' + form.data.email
// }
// });
// auth.updateUserAttributes(user.user_id, {
// receiveEmail: false
// });
// }
return message(form, { type: 'success', message: 'Email updated successfully!' });
},
return message(form, { type: 'success', message: 'Email updated successfully!' });
},
};

View file

@ -96,16 +96,16 @@
</form>
<div class="mt-6">
{#if !hasSetupTwoFactor}
<p>Two Factor Authentication is: <strong>Disabled</strong></p>
<Button variant="link" class="text-secondary-foreground" href="/profile/security/two-factor">
<p>Multi Factor Authentication is: <strong>Disabled</strong></p>
<Button variant="link" class="text-secondary-foreground" href="/profile/security/mfa">
<KeyRound class="mr-2 h-4 w-4" />
Setup 2FA
Setup Multi-factor Authentication
</Button>
{:else}
<p>Two Factor Authentication is: <strong>Enabled</strong></p>
<Button variant="link" class="text-secondary-foreground" href="/profile/security/two-factor">
<p>Multi Factor Authentication is: <strong>Enabled</strong></p>
<Button variant="link" class="text-secondary-foreground" href="/profile/security/mfa">
<KeyRound class="mr-2 h-4 w-4" />
Disable 2FA
Disable Multi-factor Authentication
</Button>
{/if}
</div>

View file

@ -12,30 +12,32 @@ import { redirect, setFlash } from 'sveltekit-flash-message/server';
import type { PageServerLoad } from '../../$types';
import { addTwoFactorSchema, removeTwoFactorSchema } from '$lib/validations/account';
import { notSignedInMessage } from '$lib/flashMessages';
import db from '$lib/server/api/infrastructure/database';
import { recoveryCodes, twoFactor, usersTable } from '$lib/server/api/infrastructure/database/tables';
import { db } from '$lib/server/api/infrastructure/database';
import { recoveryCodesTable, credentialsTable, usersTable, type Credentials } from '$lib/server/api/infrastructure/database/tables';
import { userNotAuthenticated } from '$lib/server/auth-utils';
import env from '../../../../../../env';
import env from '$src/env';
export const load: PageServerLoad = async (event) => {
const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema));
const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema));
const { locals } = event;
const { user, session } = locals;
if (userNotAuthenticated(user, session)) {
redirect(302, '/login', notSignedInMessage, event);
const authedUser = await locals.getAuthedUser();
if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event);
}
const dbUser = await db.query.usersTable.findFirst({
where: eq(usersTable.id, user!.id!),
});
const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema));
const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema));
// const addAuthNFactorForm = await superValidate(event, zod(addAuthNFactorSchema));
const twoFactorDetails = await db.query.twoFactor.findFirst({
where: eq(twoFactor.userId, dbUser!.id!),
});
const { data, error } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse);
if (error || !data) {
return fail(500, {
addTwoFactorForm,
});
}
const { totpCredential } = data
if (twoFactorDetails?.enabled) {
if (totpCredential) {
return {
addTwoFactorForm,
removeTwoFactorForm,
@ -43,35 +45,27 @@ export const load: PageServerLoad = async (event) => {
recoveryCodes: [],
totpUri: '',
qrCode: '',
};
}
const twoFactorSecret = await new HMAC('SHA-1').generateKey();
try {
await db
.insert(twoFactor)
.values({
secret: encodeHex(twoFactorSecret),
enabled: false,
userId: dbUser!.id!,
})
.onConflictDoUpdate({
target: twoFactor.userId,
set: {
secret: encodeHex(twoFactorSecret),
enabled: false,
},
});
} catch (e) {
console.error(e);
error(500);
}
}
const issuer = kebabCase(env.PUBLIC_SITE_NAME);
const accountName = dbUser!.email! || dbUser!.username!;
const accountName = authedUser.email || authedUser.username;
const { data: createdTotpData, error: createdTotpError } = await locals.api.mfa.totp.$post().then(locals.parseApiResponse);
if (createdTotpError || !createdTotpData) {
return fail(500, {
addTwoFactorForm,
})
}
const { totpCredential: createdTotpCredentials } = createdTotpData;
// pass the website's name and the user identifier (e.g. email, username)
const totpUri = createTOTPKeyURI(issuer, accountName, twoFactorSecret);
if (!createdTotpCredentials?.secret_data) {
return fail(500, {
addTwoFactorForm,
})
}
const totpUri = createTOTPKeyURI(issuer, accountName, createdTotpCredentials.secret_data);
addTwoFactorForm.data = {
current_password: '',
@ -88,111 +82,85 @@ export const load: PageServerLoad = async (event) => {
};
export const actions: Actions = {
enableTwoFactor: async (event) => {
const { locals } = event;
const { user, session } = locals;
if (userNotAuthenticated(user, session)) {
return fail(401);
enableTotp: async (event) => {
const { locals } = event
const authedUser = await locals.getAuthedUser()
if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event)
}
const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema));
const addTwoFactorForm = await superValidate(event, zod(addTwoFactorSchema))
if (!addTwoFactorForm.valid) {
return fail(400, {
addTwoFactorForm,
});
})
}
if (!event.locals.user) {
redirect(302, '/login', notSignedInMessage, event);
}
if (!event.locals.session) {
return fail(401);
}
const dbUser = await db.query.usersTable.findFirst({
where: eq(usersTable.id, user!.id!),
});
// if (!dbUser?.hashed_password) {
// addTwoFactorForm.data.current_password = '';
// addTwoFactorForm.data.two_factor_code = '';
// return setError(
// addTwoFactorForm,
// 'Error occurred. Please try again or contact support if you need further help.',
// );
// }
const twoFactorDetails = await db.query.twoFactor.findFirst({
where: eq(twoFactor.userId, dbUser?.id),
});
if (!twoFactorDetails) {
addTwoFactorForm.data.current_password = '';
addTwoFactorForm.data.two_factor_code = '';
return setError(
const { data, error } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse)
if (error || !data) {
return fail(500, {
addTwoFactorForm,
'Error occurred. Please try again or contact support if you need further help.',
);
})
}
const { totpCredential } = data
if (!totpCredential) {
addTwoFactorForm.data.current_password = ''
addTwoFactorForm.data.two_factor_code = ''
return setError(addTwoFactorForm, 'Error occurred. Please try again or contact support if you need further help.')
}
if (twoFactorDetails.secret === '' || twoFactorDetails.secret === null) {
addTwoFactorForm.data.current_password = '';
addTwoFactorForm.data.two_factor_code = '';
return setError(
addTwoFactorForm,
'Error occurred. Please try again or contact support if you need further help.',
);
if (totpCredential.secret_data === '' || totpCredential.secret_data === null) {
addTwoFactorForm.data.current_password = ''
addTwoFactorForm.data.two_factor_code = ''
return setError(addTwoFactorForm, 'Error occurred. Please try again or contact support if you need further help.')
}
const currentPasswordVerified = await new Argon2id().verify(
// dbUser.hashed_password,
addTwoFactorForm.data.current_password,
);
const currentPasswordVerified = await locals.api.me.verify.password.$post({
json: { password: addTwoFactorForm.data.current_password },
});
if (!currentPasswordVerified) {
return setError(addTwoFactorForm, 'current_password', 'Your password is incorrect');
return setError(addTwoFactorForm, 'current_password', 'Your password is incorrect')
}
if (addTwoFactorForm.data.two_factor_code === '') {
return setError(addTwoFactorForm, 'two_factor_code', 'Please enter a code');
return setError(addTwoFactorForm, 'two_factor_code', 'Please enter a code')
}
const twoFactorCode = addTwoFactorForm.data.two_factor_code;
const validOTP = await new TOTPController().verify(
twoFactorCode,
decodeHex(twoFactorDetails.secret),
);
const twoFactorCode = addTwoFactorForm.data.two_factor_code
const validOTP = await new TOTPController().verify(twoFactorCode, decodeHex(twoFactorDetails.secret))
if (!validOTP) {
return setError(addTwoFactorForm, 'two_factor_code', 'Invalid code');
return setError(addTwoFactorForm, 'two_factor_code', 'Invalid code')
}
await db.update(twoFactor).set({ enabled: true }).where(eq(twoFactor.userId, user!.id!));
await db.update(twoFactor).set({ enabled: true }).where(eq(twoFactor.userId, user!.id!))
redirect(302, '/profile/security/two-factor/recovery-codes');
redirect(302, '/profile/security/two-factor/recovery-codes')
},
disableTwoFactor: async (event) => {
const { locals } = event;
const { user, session } = locals;
const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema));
disableTotp: async (event) => {
const { locals } = event
const { user, session } = locals
const removeTwoFactorForm = await superValidate(event, zod(removeTwoFactorSchema))
if (!removeTwoFactorForm.valid) {
return fail(400, {
removeTwoFactorForm,
});
})
}
if (!user || !session) {
return fail(401, {
removeTwoFactorForm,
});
})
}
const dbUser = await db.query.usersTable.findFirst({
where: eq(usersTable.id, user.id),
});
})
// if (!dbUser?.hashed_password) {
// removeTwoFactorForm.data.current_password = '';
@ -205,24 +173,24 @@ export const actions: Actions = {
const currentPasswordVerified = await new Argon2id().verify(
// dbUser.hashed_password,
removeTwoFactorForm.data.current_password,
);
)
if (!currentPasswordVerified) {
return setError(removeTwoFactorForm, 'current_password', 'Your password is incorrect');
return setError(removeTwoFactorForm, 'current_password', 'Your password is incorrect')
}
const twoFactorDetails = await db.query.twoFactor.findFirst({
where: eq(twoFactor.userId, dbUser.id),
});
})
if (!twoFactorDetails) {
return fail(500, {
removeTwoFactorForm,
});
})
}
await db.update(twoFactor).set({ enabled: false }).where(eq(twoFactor.userId, user.id));
await db.delete(recoveryCodes).where(eq(recoveryCodes.userId, user.id));
await db.update(twoFactor).set({ enabled: false }).where(eq(twoFactor.userId, user.id))
await db.delete(recoveryCodes).where(eq(recoveryCodes.userId, user.id))
// setFlash({ type: 'success', message: 'Two-Factor Authentication has been disabled.' }, cookies);
redirect(
@ -233,6 +201,6 @@ export const actions: Actions = {
message: 'Two-Factor Authentication has been disabled.',
},
event,
);
)
},
};
}

View file

@ -37,7 +37,7 @@
{#if twoFactorEnabled}
<h2>Currently you have two factor authentication <span class="text-green-500">enabled</span></h2>
<p>To disable two factor authentication, please enter your current password.</p>
<form method="POST" action="?/disableTwoFactor" use:removeTwoFactorEnhance data-sveltekit-replacestate>
<form method="POST" action="?/disableTotp" use:removeTwoFactorEnhance data-sveltekit-replacestate>
<Form.Field form={removeTwoFactorForm} name="current_password">
<Form.Control let:attrs>
<Form.Label for="password">Current Password</Form.Label>
@ -51,7 +51,7 @@
{:else}
<h2>Please scan the following QR Code</h2>
<img src={qrCode} alt="QR Code" />
<form method="POST" action="?/enableTwoFactor" use:addTwoFactorEnhance data-sveltekit-replacestate>
<form method="POST" action="?/enableTotp" use:addTwoFactorEnhance data-sveltekit-replacestate>
<Form.Field form={addTwoFactorForm} name="two_factor_code">
<Form.Control let:attrs>
<Form.Label for="code">Enter Code</Form.Label>

View file

@ -1,31 +1,36 @@
import db from '../../../../../../../db';
import { eq } from 'drizzle-orm';
import { Argon2id } from 'oslo/password';
import { alphabet, generateRandomString } from 'oslo/crypto';
import { redirect } from 'sveltekit-flash-message/server';
import { db } from '$lib/server/api/infrastructure/database';
import { notSignedInMessage } from '$lib/flashMessages';
import type { PageServerLoad } from '../../../$types';
import {recoveryCodes, twoFactor, usersTable} from '$db/schema';
import { recoveryCodesTable, twoFactorTable, usersTable} from '$lib/server/api/infrastructure/database/tables';
import { userNotAuthenticated } from '$lib/server/auth-utils';
export const load: PageServerLoad = async (event) => {
const { locals } = event;
const { user, session } = locals;
if (userNotAuthenticated(user, session)) {
redirect(302, '/login', notSignedInMessage, event);
const authedUser = await locals.getAuthedUser();
if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event);
}
const dbUser = await db.query.usersTable.findFirst({
where: eq(usersTable.id, user!.id),
where: eq(usersTable.id, authedUser.id),
});
if (!dbUser) {
throw redirect(302, '/login', notSignedInMessage, event);
}
const twoFactorDetails = await db.query.twoFactor.findFirst({
where: eq(twoFactor.userId, dbUser!.id),
where: eq(twoFactor.userId, dbUser.id),
});
if (twoFactorDetails?.enabled) {
const dbRecoveryCodes = await db.query.recoveryCodes.findMany({
where: eq(recoveryCodes.userId, user!.id),
where: eq(recoveryCodes.userId, authedUser.id),
});
if (dbRecoveryCodes.length === 0) {
@ -37,7 +42,7 @@ export const load: PageServerLoad = async (event) => {
const hashedCode = await new Argon2id().hash(code);
console.log('Inserting recovery code', code, hashedCode);
await db.insert(recoveryCodes).values({
userId: user!.id,
userId: authedUser.id,
code: hashedCode,
});
}

View file

@ -5,22 +5,22 @@ import { setError, superValidate } from 'sveltekit-superforms/server';
import { redirect } from 'sveltekit-flash-message/server';
import { Argon2id } from 'oslo/password';
import type { PageServerLoad } from '../../../$types';
import db from '../../../../../../../db';
import { db } from '$lib/server/api/infrastructure/database';
import { changeUserPasswordSchema } from '$lib/validations/account';
import { lucia } from '$lib/server/auth.js';
import { usersTable } from '$db/schema';
import { usersTable } from '$lib/server/api/infrastructure/database/tables';
import { notSignedInMessage } from '$lib/flashMessages';
import type { Cookie } from 'lucia';
import { userNotAuthenticated } from '$lib/server/auth-utils';
export const load: PageServerLoad = async (event) => {
const form = await superValidate(event, zod(changeUserPasswordSchema));
const { locals } = event;
const { user, session } = locals;
if (userNotAuthenticated(user, session)) {
redirect(302, '/login', notSignedInMessage, event);
const authedUser = await locals.getAuthedUser();
if (!authedUser) {
throw redirect(302, '/login', notSignedInMessage, event);
}
const form = await superValidate(event, zod(changeUserPasswordSchema));
form.data = {
current_password: '',
password: '',
@ -34,9 +34,10 @@ export const load: PageServerLoad = async (event) => {
export const actions: Actions = {
default: 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(changeUserPasswordSchema));
@ -57,7 +58,7 @@ export const actions: Actions = {
}
const dbUser = await db.query.usersTable.findFirst({
where: eq(usersTable.id, user!.id),
where: eq(usersTable.id, authedUser.id),
});
// if (!dbUser?.hashed_password) {
@ -78,14 +79,14 @@ export const actions: Actions = {
if (!currentPasswordVerified) {
return setError(form, 'current_password', 'Your password is incorrect');
}
if (user?.username) {
if (authedUser?.username) {
let sessionCookie: Cookie;
try {
if (form.data.password !== form.data.confirm_password) {
return setError(form, 'Password and confirm password do not match');
}
const hashedPassword = await new Argon2id().hash(form.data.password);
await lucia.invalidateUserSessions(user.id);
await lucia.invalidateUserSessions(authedUser.id);
// await db
// .update(usersTable)
// .set({ hashed_password: hashedPassword })

View file

@ -52,7 +52,7 @@
<AddToList {in_collection} {in_wishlist} game_id={game.id} {wishlist} {collection} />
{:else}
<span>
<Button href="/sign-up">Sign Up</Button> or <Button href="/login">Sign In</Button> to add to a list.
<Button href="/signup">Sign Up</Button> or <Button href="/login">Sign In</Button> to add to a list.
</span>
{/if}
</div>

View file

@ -18,8 +18,8 @@
{#if $page.url.pathname !== '/login'}
<Button href="/login" variant="ghost">Login</Button>
{/if}
{#if $page.url.pathname !== '/sign-up'}
<Button href="/sign-up" variant="ghost">Sign up</Button>
{#if $page.url.pathname !== '/signup'}
<Button href="/signup" variant="ghost">Sign up</Button>
{/if}
</div>
<div class="auth-marketing">

View file

@ -46,7 +46,7 @@
<Card.Title class="text-2xl">Signup for an account</Card.Title>
</Card.Header>
<Card.Content>
<form method="POST" action="/sign-up" use:enhance class="grid gap-2 mt-4">
<form method="POST" action="/signup" use:enhance class="grid gap-2 mt-4">
<Label for="username">Username <small>(required)</small></Label>
<Input type="text" id="username" class={$errors.username && "outline outline-destructive"} name="username"
placeholder="Username" autocomplete="username" data-invalid={$errors.username} bind:value={$form.username} />

View file

@ -22,9 +22,10 @@ const config = {
$assets: './src/assets',
$components: './src/components',
'$components/*': 'src/lib/components/*',
$db: './src/db',
$db: './src/lib/server/api/infrastructure/database',
$server: './src/server',
$lib: './src/lib',
$src: './src',
$state: './src/state',
$styles: './src/styles',
$themes: './src/themes',