Creating APIs for changing user password and calling it in change password. Cleaned up CSS layout styles and got settings nav correct.

This commit is contained in:
Bradley Shellnut 2024-09-09 19:25:16 -07:00
parent 2eee00a20d
commit 3b33880166
31 changed files with 350 additions and 357 deletions

View file

@ -76,7 +76,7 @@
},
"type": "module",
"dependencies": {
"@fontsource/fira-mono": "^5.0.14",
"@fontsource/fira-mono": "^5.0.15",
"@hono/swagger-ui": "^0.4.1",
"@hono/zod-openapi": "^0.15.3",
"@hono/zod-validator": "^0.2.2",

View file

@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@fontsource/fira-mono':
specifier: ^5.0.14
version: 5.0.14
specifier: ^5.0.15
version: 5.0.15
'@hono/swagger-ui':
specifier: ^0.4.1
version: 0.4.1(hono@4.5.11)
@ -1245,8 +1245,8 @@ packages:
'@floating-ui/utils@0.2.7':
resolution: {integrity: sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==}
'@fontsource/fira-mono@5.0.14':
resolution: {integrity: sha512-4IKa+cuHipk/vr2frgZh4pyR2XcoQk/j3zmMlo8uuAGUB3IPLpQlgN6Qm5d3RfRZ7dXGlTn/PWiAJeU8bkmD4w==}
'@fontsource/fira-mono@5.0.15':
resolution: {integrity: sha512-wc3TpF2GBbtFDKNbb444BrC3mEKuoPLITSYCKweNIrqBvAalIfJGloY/MVrmSGaMNgaAKUpdgy4eAWPLkUVzaA==}
'@gcornut/valibot-json-schema@0.31.0':
resolution: {integrity: sha512-3xGptCurm23e7nuPQkdrE5rEs1FeTPHhAUsBuwwqG4/YeZLwJOoYZv+fmsppUEfo5y9lzUwNQrNqLS/q7HMc7g==}
@ -5489,7 +5489,7 @@ snapshots:
'@floating-ui/utils@0.2.7': {}
'@fontsource/fira-mono@5.0.14': {}
'@fontsource/fira-mono@5.0.15': {}
'@gcornut/valibot-json-schema@0.31.0':
dependencies:

View file

@ -4,15 +4,10 @@ import { invalidateAll } from '$app/navigation'
import Logo from '$components/logo.svelte'
import * as Avatar from '$components/ui/avatar'
import * as DropdownMenu from '$components/ui/dropdown-menu'
import type { Users } from '$db/schema'
import { ListChecks, ListTodo, LogOut, User } from 'lucide-svelte'
import { ListChecks, ListTodo, LogOut, Settings } from 'lucide-svelte'
import toast from 'svelte-french-toast'
type HeaderProps = {
user: Users | null
}
let { user = null }: HeaderProps = $props()
let { user = null } = $props()
let avatar: string = $derived(user?.username?.slice(0, 1).toUpperCase() || ':)')
</script>
@ -28,39 +23,48 @@ let avatar: string = $derived(user?.username?.slice(0, 1).toUpperCase() || ':)')
</div>
<nav>
{#if user}
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Avatar.Root asChild>
<Avatar.Fallback class="text-3xl font-medium text-magnum-700 h-16 w-16 bg-neutral-100">
{avatar}
</Avatar.Fallback>
</Avatar.Root>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Group>
<DropdownMenu.Label>Account</DropdownMenu.Label>
<DropdownMenu.Separator />
<a href="/settings">
<DropdownMenu.Item>
<User class="mr-2 h-4 w-4" />
<span>Settings</span>
</DropdownMenu.Item>
</a>
<a href="/collections">
<DropdownMenu.Item>
<ListChecks class="mr-2 h-4 w-4" />
<span>Collections</span>
</DropdownMenu.Item>
</a>
<a href="/wishlists">
<DropdownMenu.Item>
<ListTodo class="mr-2 h-4 w-4" />
<span>Wishlists</span>
</DropdownMenu.Item>
</a>
<DropdownMenu.Item>
<form
use:enhance={() => {
{@render userDropdown()}
{:else}
<a href="/login"> <span class="flex-auto">Login</span></a>
<a href="/signup"> <span class="flex-auto">Sign Up</span></a>
{/if}
</nav>
</header>
{#snippet userDropdown()}
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Avatar.Root asChild>
<Avatar.Fallback class="text-3xl font-medium text-magnum-700 h-16 w-16 bg-neutral-100">
{avatar}
</Avatar.Fallback>
</Avatar.Root>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Group>
<DropdownMenu.Label>Account</DropdownMenu.Label>
<DropdownMenu.Separator />
<a href="/settings">
<DropdownMenu.Item>
<Settings class="mr-2 h-4 w-4" />
<span>Settings</span>
</DropdownMenu.Item>
</a>
<a href="/collections">
<DropdownMenu.Item>
<ListChecks class="mr-2 h-4 w-4" />
<span>Collections</span>
</DropdownMenu.Item>
</a>
<a href="/wishlists">
<DropdownMenu.Item>
<ListTodo class="mr-2 h-4 w-4" />
<span>Wishlists</span>
</DropdownMenu.Item>
</a>
<DropdownMenu.Item>
<form
use:enhance={() => {
return async ({ result }) => {
console.log(result);
if (result.type === 'success' || result.type === 'redirect') {
@ -76,26 +80,21 @@ let avatar: string = $derived(user?.username?.slice(0, 1).toUpperCase() || ':)')
await applyAction(result);
};
}}
action="/logout"
method="POST"
>
<button type="submit">
<div class="flex items-center gap-1">
<LogOut class="mr-2 h-4 w-4" />
<span>Sign out</span>
</div>
</button>
</form>
</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
{:else}
<a href="/login"> <span class="flex-auto">Login</span></a>
<a href="/signup"> <span class="flex-auto">Sign Up</span></a>
{/if}
</nav>
</header>
action="/logout"
method="POST"
>
<button type="submit">
<div class="flex items-center gap-1">
<LogOut class="mr-2 h-4 w-4" />
<span>Sign out</span>
</div>
</button>
</form>
</DropdownMenu.Item>
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
{/snippet}
<style lang="postcss">
header {

View file

@ -1,77 +0,0 @@
<script lang="ts">
import { page } from '$app/stores'
import type { Route } from '$lib/types'
let { children, routes }: { children: unknown; routes: Route[] } = $props()
</script>
<div class="mx-auto grid w-full max-w-6xl gap-2">
<h1 class="text-3xl font-semibold">Settings</h1>
</div>
<div class="security-nav">
<nav>
<ul>
{#each routes as { href, label }}
<li>
<a href={href} class:active={$page.url.pathname.includes(href)}>
{label}
</a>
</li>
{/each}
</ul>
</nav>
<div class="security-nav-content">
{@render children()}
</div>
</div>
<style lang="postcss">
.security-nav {
display: flex;
nav {
width: 16rem;
position: sticky;
top: 0;
left: 0;
background-color: #fff;
padding: 1rem;
border-right: 1px solid #ddd;
height: 100vh;
ul {
list-style-type: none;
padding: 0;
margin: 0;
}
li {
margin-bottom: 0.5rem;
}
a {
text-decoration: none;
color: #337ab7;
display: block;
padding: 0.5rem;
border-radius: 0.25rem;
&:hover {
background-color: #f8f9fa;
}
&.active {
color: var(--color-link-hover);
font-weight: 600;
background-color: #e9ecef;
}
}
}
}
.security-nav-content {
flex: 1;
padding: 1rem;
}
</style>

View file

@ -1,7 +1,7 @@
<script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements";
import type { InputEvents } from "./index.js";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils/ui.js";
type $$Props = HTMLInputAttributes;
type $$Events = InputEvents;

View file

@ -2,12 +2,12 @@ import { type VariantProps, tv } from "tailwind-variants";
import Root from "./toggle.svelte";
export const toggleVariants = tv({
base: "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
base: "ring-offset-background hover:bg-muted hover:text-muted-foreground focus-visible:ring-ring data-[state=on]:bg-accent data-[state=on]:text-accent-foreground inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
"border-input hover:bg-accent hover:text-accent-foreground border bg-transparent",
},
size: {
default: "h-10 px-3",

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { Toggle as TogglePrimitive } from "bits-ui";
import { type Size, type Variant, toggleVariants } from "./index.js";
import { cn } from "$lib/utils.js";
import { cn } from "$lib/utils/ui.js";
type $$Props = TogglePrimitive.Props & {
variant?: Variant;

View file

@ -1,36 +1,37 @@
<script lang="ts">
import { navigating } from '$app/stores';
import { onNavigate } from '$app/navigation';
let visible = false;
let progress = 0;
let load_durations: number[] = [];
$: average_load = load_durations.reduce((a, b) => a + b, 0) / load_durations.length;
const increment = 1;
onNavigate((navigation) => {
const typical_load_time = average_load || 200; //ms
const frequency = typical_load_time / 100;
let start = performance.now();
// Start the progress bar
visible = true;
progress = 0;
const interval = setInterval(() => {
// Increment the progress bar
progress += increment;
}, frequency);
// Resolve the promise when the page is done loading
$navigating?.complete.then(() => {
progress = 100; // Fill out the progress bar
clearInterval(interval);
// after 100 ms hide the progress bar
setTimeout(() => {
visible = false;
}, 500);
// Log how long that one took
const end = performance.now();
const duration = end - start;
load_durations = [...load_durations, duration];
});
});
import { onNavigate } from '$app/navigation'
import { navigating } from '$app/stores'
let visible = false
let progress = 0
let load_durations: number[] = []
$: average_load = load_durations.reduce((a, b) => a + b, 0) / load_durations.length
const increment = 1
onNavigate((navigation) => {
const typical_load_time = average_load || 200 //ms
const frequency = typical_load_time / 100
let start = performance.now()
// Start the progress bar
visible = true
progress = 0
const interval = setInterval(() => {
// Increment the progress bar
progress += increment
}, frequency)
// Resolve the promise when the page is done loading
$navigating?.complete.then(() => {
progress = 100 // Fill out the progress bar
clearInterval(interval)
// after 100 ms hide the progress bar
setTimeout(() => {
visible = false
}, 500)
// Log how long that one took
const end = performance.now()
const duration = end - start
load_durations = [...load_durations, duration]
})
})
</script>
<div class="progress" class:visible style:--progress={progress}>

View file

@ -34,5 +34,3 @@ export const config: Config = {
max: env.DB_MIGRATING || env.DB_SEEDING ? 1 : undefined,
},
}
console.log('config', config)

View file

@ -1,26 +1,26 @@
import { StatusCodes } from '$lib/constants/status-codes';
import { HTTPException } from 'hono/http-exception';
import { StatusCodes } from '$lib/constants/status-codes'
import { HTTPException } from 'hono/http-exception'
export function TooManyRequests(message: string = 'Too many requests') {
return new HTTPException(StatusCodes.TOO_MANY_REQUESTS, { message });
export function TooManyRequests(message = 'Too many requests') {
return new HTTPException(StatusCodes.TOO_MANY_REQUESTS, { message })
}
export function Forbidden(message: string = 'Forbidden') {
return new HTTPException(StatusCodes.FORBIDDEN, { message });
export function Forbidden(message = 'Forbidden') {
return new HTTPException(StatusCodes.FORBIDDEN, { message })
}
export function Unauthorized(message: string = 'Unauthorized') {
return new HTTPException(StatusCodes.UNAUTHORIZED, { message });
export function Unauthorized(message = 'Unauthorized') {
return new HTTPException(StatusCodes.UNAUTHORIZED, { message })
}
export function NotFound(message: string = 'Not Found') {
return new HTTPException(StatusCodes.NOT_FOUND, { message });
export function NotFound(message = 'Not Found') {
return new HTTPException(StatusCodes.NOT_FOUND, { message })
}
export function BadRequest(message: string = 'Bad Request') {
return new HTTPException(StatusCodes.BAD_REQUEST, { message });
export function BadRequest(message = 'Bad Request') {
return new HTTPException(StatusCodes.BAD_REQUEST, { message })
}
export function InternalError(message: string = 'Internal Error') {
return new HTTPException(StatusCodes.INTERNAL_SERVER_ERROR, { message });
export function InternalError(message = 'Internal Error') {
return new HTTPException(StatusCodes.INTERNAL_SERVER_ERROR, { message })
}

View file

@ -2,7 +2,7 @@ import 'reflect-metadata'
import { Controller } from '$lib/server/api/common/types/controller'
import { CollectionsService } from '$lib/server/api/services/collections.service'
import { inject, injectable } from 'tsyringe'
import { requireAuth } from '../middleware/auth.middleware'
import { requireAuth } from '../middleware/require-auth.middleware'
@injectable()
export class CollectionController extends Controller {

View file

@ -1,20 +1,23 @@
import { StatusCodes } from '$lib/constants/status-codes'
import { Controller } from '$lib/server/api/common/types/controller'
import { changePasswordDto } from '$lib/server/api/dtos/change-password.dto'
import { updateEmailDto } from '$lib/server/api/dtos/update-email.dto'
import { updateProfileDto } from '$lib/server/api/dtos/update-profile.dto'
import { verifyPasswordDto } from '$lib/server/api/dtos/verify-password.dto'
import { limiter } from '$lib/server/api/middleware/rate-limiter.middleware'
import { IamService } from '$lib/server/api/services/iam.service'
import { LoginRequestsService } from '$lib/server/api/services/loginrequest.service'
import { LuciaService } from '$lib/server/api/services/lucia.service'
import { zValidator } from '@hono/zod-validator'
import { setCookie } from 'hono/cookie'
import { inject, injectable } from 'tsyringe'
import { requireAuth } from '../middleware/auth.middleware'
import { requireAuth } from '../middleware/require-auth.middleware'
@injectable()
export class IamController extends Controller {
constructor(
@inject(IamService) private readonly iamService: IamService,
@inject(LoginRequestsService) private readonly loginRequestService: LoginRequestsService,
@inject(LuciaService) private luciaService: LuciaService,
) {
super()
@ -45,6 +48,32 @@ export class IamController extends Controller {
}
return c.json({}, StatusCodes.OK)
})
.put('/update/password', requireAuth, zValidator('json', changePasswordDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
const user = c.var.user
const { password, confirm_password } = c.req.valid('json')
if (password !== confirm_password) {
return c.json('Passwords do not match', StatusCodes.BAD_REQUEST)
}
try {
await this.iamService.updatePassword(user.id, { password, confirm_password })
await this.luciaService.lucia.invalidateUserSessions(user.id)
await this.loginRequestService.createUserSession(user.id, c.req, undefined)
const sessionCookie = this.luciaService.lucia.createBlankSessionCookie()
setCookie(c, sessionCookie.name, sessionCookie.value, {
path: sessionCookie.attributes.path,
maxAge: sessionCookie.attributes.maxAge,
domain: sessionCookie.attributes.domain,
sameSite: sessionCookie.attributes.sameSite as any,
secure: sessionCookie.attributes.secure,
httpOnly: sessionCookie.attributes.httpOnly,
expires: sessionCookie.attributes.expires,
})
return c.json({ status: 'success' })
} catch (error) {
console.error('Error updating password', error)
return c.json('Error updating password', StatusCodes.BAD_REQUEST)
}
})
.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

@ -8,7 +8,7 @@ import { UsersService } from '$lib/server/api/services/users.service'
import { zValidator } from '@hono/zod-validator'
import { inject, injectable } from 'tsyringe'
import { CredentialsType } from '../databases/tables'
import { requireAuth } from '../middleware/auth.middleware'
import { requireAuth } from '../middleware/require-auth.middleware'
@injectable()
export class MfaController extends Controller {

View file

@ -2,7 +2,7 @@ import 'reflect-metadata'
import { Controller } from '$lib/server/api/common/types/controller'
import { UsersService } from '$lib/server/api/services/users.service'
import { inject, injectable } from 'tsyringe'
import { requireAuth } from '../middleware/auth.middleware'
import { requireAuth } from '../middleware/require-auth.middleware'
@injectable()
export class UserController extends Controller {

View file

@ -2,7 +2,7 @@ import 'reflect-metadata'
import { Controller } from '$lib/server/api/common/types/controller'
import { WishlistsService } from '$lib/server/api/services/wishlists.service'
import { inject, injectable } from 'tsyringe'
import { requireAuth } from '../middleware/auth.middleware'
import { requireAuth } from '../middleware/require-auth.middleware'
@injectable()
export class WishlistController extends Controller {

View file

@ -0,0 +1,17 @@
import { refinePasswords } from '$lib/validations/account'
import { z } from 'zod'
export const changePasswordDto = z
.object({
password: z.string({ required_error: 'Password is required' }).trim(),
confirm_password: z
.string({ required_error: 'Confirm Password is required' })
.trim()
.min(8, { message: 'Must be at least 8 characters' })
.max(128, { message: 'Must be less than 128 characters' }),
})
.superRefine(({ confirm_password, password }, ctx) => {
return refinePasswords(confirm_password, password, ctx)
})
export type ChangePasswordDto = z.infer<typeof changePasswordDto>

View file

@ -1,23 +1,14 @@
import { z } from "zod";
import { z } from 'zod'
export const updateProfileDto = z.object({
firstName: z
.string()
.trim()
.min(3, {message: 'Must be at least 3 characters'})
.max(50, {message: 'Must be less than 50 characters'})
.min(3, { message: 'Must be at least 3 characters' })
.max(50, { message: 'Must be less than 50 characters' })
.optional(),
lastName: z
.string()
.trim()
.min(3, {message: 'Must be at least 3 characters'})
.max(50, {message: 'Must be less than 50 characters'})
.optional(),
username: z
.string()
.trim()
.min(3, {message: 'Must be at least 3 characters'})
.max(50, {message: 'Must be less than 50 characters'})
});
lastName: z.string().trim().min(3, { message: 'Must be at least 3 characters' }).max(50, { message: 'Must be less than 50 characters' }).optional(),
username: z.string().trim().min(3, { message: 'Must be at least 3 characters' }).max(50, { message: 'Must be less than 50 characters' }),
})
export type UpdateProfileDto = z.infer<typeof updateProfileDto>;
export type UpdateProfileDto = z.infer<typeof updateProfileDto>

View file

@ -10,11 +10,11 @@ export class AuthCleanupJobs {
this.queue = this.jobsService.createQueue('test')
/* ---------------------------- Register Workers ---------------------------- */
this.worker().then((r) => console.log('auth-cleanup job worker started'))
this.worker().then(() => console.log('auth-cleanup job worker started'))
}
async deleteStaleEmailVerificationRequests() {
await this.queue.add('delete_stale_email_verifiactions', null, {
await this.queue.add('delete_stale_email_verifications', null, {
repeat: {
pattern: '0 0 * * 0', // Runs once a week at midnight on Sunday
},
@ -31,7 +31,7 @@ export class AuthCleanupJobs {
private async worker() {
return this.jobsService.createWorker(this.queue.name, async (job) => {
if (job.name === 'delete_stale_email_verifiactions') {
if (job.name === 'delete_stale_email_verifications') {
// delete stale email verifications
}
if (job.name === 'delete_stale_login_requests') {

View file

@ -1,10 +1,8 @@
import { LuciaService } from '$lib/server/api/services/lucia.service'
import type { MiddlewareHandler } from 'hono'
import { createMiddleware } from 'hono/factory'
import type { Session, User } from 'lucia'
import { verifyRequestOrigin } from 'oslo/request'
import { container } from 'tsyringe'
import { Unauthorized } from '../common/exceptions'
import type { HonoTypes } from '../common/types/hono'
// resolve dependencies from the container
@ -41,14 +39,3 @@ export const validateAuthSession: MiddlewareHandler<HonoTypes> = createMiddlewar
c.set('user', user)
return next()
})
export const requireAuth: MiddlewareHandler<{
Variables: {
session: Session
user: User
}
}> = createMiddleware(async (c, next) => {
const user = c.var.user
if (!user) throw Unauthorized('You must be logged in to access this resource')
return next()
})

View file

@ -1,3 +1,5 @@
import { CredentialsType } from '$lib/server/api/databases/tables'
import type { ChangePasswordDto } from '$lib/server/api/dtos/change-password.dto'
import type { UpdateEmailDto } from '$lib/server/api/dtos/update-email.dto'
import type { UpdateProfileDto } from '$lib/server/api/dtos/update-profile.dto'
import type { VerifyPasswordDto } from '$lib/server/api/dtos/verify-password.dto'
@ -68,6 +70,11 @@ export class IamService {
})
}
async updatePassword(userId: string, data: ChangePasswordDto) {
const { password } = data
await this.usersService.updatePassword(userId, password)
}
async verifyPassword(userId: string, data: VerifyPasswordDto) {
const user = await this.usersService.findOneById(userId)
if (!user) {

View file

@ -69,6 +69,22 @@ export class UsersService {
return this.usersRepository.findOneById(id)
}
async updatePassword(userId: string, password: string) {
const hashedPassword = await this.tokenService.createHashedToken(password)
const currentCredentials = await this.credentialsRepository.findPasswordCredentialsByUserId(userId)
if (!currentCredentials) {
await this.credentialsRepository.create({
user_id: userId,
type: CredentialsType.PASSWORD,
secret_data: hashedPassword,
})
} else {
await this.credentialsRepository.update(currentCredentials.id, {
secret_data: hashedPassword,
})
}
}
async verifyPassword(userId: string, data: { password: string }) {
const user = await this.usersRepository.findOneById(userId)
if (!user) {

View file

@ -1,5 +1,5 @@
<script lang="ts">
import LeftNav from '$components/LeftNav.svelte'
import { page } from '$app/stores'
import type { Route } from '$lib/types'
const routes: Route[] = [
@ -10,6 +10,74 @@ const routes: Route[] = [
let { children } = $props()
</script>
<LeftNav {routes}>
{@render children()}
</LeftNav>
<div class="security-nav">
<nav>
<div class="mx-auto w-full max-w-6xl mb-2">
<h1 class="text-3xl font-semibold">Settings</h1>
</div>
<ul>
{#each routes as { href, label }}
<li>
<a href={href} class:active={$page.url.pathname.includes(href)}>
{label}
</a>
</li>
{/each}
</ul>
</nav>
<div class="security-nav-content">
{@render children()}
</div>
</div>
<style lang="postcss">
.security-nav {
display: flex;
nav {
width: 16rem;
position: sticky;
top: 0;
left: 0;
background-color: #fff;
padding: 1rem;
border-right: 1px solid #ddd;
height: 100vh;
ul {
list-style-type: none;
padding: 0;
margin: 0;
}
li {
margin-bottom: 0.5rem;
}
a {
text-decoration: none;
color: #337ab7;
display: block;
padding: 0.5rem;
border-radius: 0.25rem;
&:hover {
background-color: #f8f9fa;
}
&.active {
color: var(--color-link-hover);
font-weight: 600;
background-color: #e9ecef;
}
}
}
.security-nav-content {
flex: 1;
padding: 1rem;
margin: 0 auto;
max-width: 80vw;
}
}
</style>

View file

@ -48,33 +48,11 @@ export const actions: Actions = {
})
}
console.log('updating profile')
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, authedUser.id),
})
// if (!dbUser?.hashed_password) {
// form.data.password = '';
// form.data.confirm_password = '';
// form.data.current_password = '';
// return setError(
// form,
// 'Error occurred. Please try again or contact support if you need further help.',
// );
// }
const currentPasswordVerified = await new Argon2id().verify(
// dbUser.hashed_password,
form.data.current_password,
)
const currentPasswordVerified = await locals.api.me.verify.password
.$post({
json: { password: form.data.current_password },
})
.then(locals.parseApiResponse)
if (!currentPasswordVerified) {
return setError(form, 'current_password', 'Your password is incorrect')
@ -85,16 +63,9 @@ export const actions: Actions = {
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(authedUser.id)
// await db
// .update(usersTable)
// .set({ hashed_password: hashedPassword })
// .where(eq(usersTable.id, user.id));
await lucia.createSession(user.id, {
country: event.locals.session?.ipCountry ?? 'unknown',
})
sessionCookie = lucia.createBlankSessionCookie()
await locals.api.me.change.password.$put({
json: { password: form.data.password, confirm_password: form.data.confirm_password },
}).then(locals.parseApiResponse)
} catch (e) {
console.error(e)
form.data.password = ''

View file

@ -2,7 +2,8 @@
import * as Alert from '$components/ui/alert'
import * as Form from '$components/ui/form'
import { Input } from '$components/ui/input'
import { AlertTriangle } from 'lucide-svelte'
import { Toggle } from '$components/ui/toggle'
import { AlertTriangle, EyeIcon, EyeOff } from 'lucide-svelte'
import { zodClient } from 'sveltekit-superforms/adapters'
import { superForm } from 'sveltekit-superforms/client'
import { changeUserPasswordSchema } from './schemas'
@ -16,13 +17,17 @@ const form = superForm(data.form, {
multipleSubmits: 'prevent',
})
let hiddenCurrentPassword = $state(true)
let hiddenPassword = $state(true)
let hiddenConfirmPassword = $state(true)
const { form: formData, enhance } = form
</script>
<form method="POST" use:enhance>
<h3>Change Password</h3>
<hr class="!border-t-2 mt-2 mb-6" />
<Alert.Root variant="destructive">
<Alert.Root variant="destructive" class="mb-4">
<AlertTriangle class="h-4 w-4" />
<Alert.Title>Heads up!</Alert.Title>
<Alert.Description>
@ -32,21 +37,30 @@ const { form: formData, enhance } = form
<Form.Field {form} name="current_password">
<Form.Control let:attrs>
<Form.Label for="current_password">Current Password</Form.Label>
<Input {...attrs} bind:value={$formData.current_password} />
<span class="flex gap-1">
<Input {...attrs} autocomplete="password" type={hiddenCurrentPassword ? 'password' : 'text'} bind:value={$formData.current_password} />
<Toggle aria-label={`${hiddenCurrentPassword ? 'Show' : 'Hide' } Current Password}`} onPressedChange={() => hiddenCurrentPassword = !hiddenCurrentPassword}>{#if hiddenCurrentPassword}<EyeIcon />{:else}<EyeOff />{/if}</Toggle>
</span>
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="password">
<Form.Control let:attrs>
<Form.Label for="password">New Password</Form.Label>
<Input {...attrs} bind:value={$formData.password} />
<span class="flex gap-1">
<Input {...attrs} autocomplete="new-password" type={hiddenPassword ? 'password' : 'text'} bind:value={$formData.password} />
<Toggle aria-label={`${hiddenPassword ? 'Show' : 'Hide' } Password}`} onPressedChange={() => hiddenPassword = !hiddenPassword}>{#if hiddenPassword}<EyeIcon />{:else}<EyeOff />{/if}</Toggle>
</span>
</Form.Control>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="confirm_password">
<Form.Control let:attrs>
<Form.Label for="confirm_password">Confirm New Password</Form.Label>
<Input {...attrs} bind:value={$formData.confirm_password} />
<span class="flex gap-1">
<Input {...attrs} autocomplete="new-password" type={hiddenConfirmPassword ? 'password' : 'text'} bind:value={$formData.confirm_password} />
<Toggle aria-label={`${hiddenConfirmPassword ? 'Show' : 'Hide' } Confirm Password}`} onPressedChange={() => hiddenConfirmPassword = !hiddenConfirmPassword}>{#if hiddenConfirmPassword}<EyeIcon />{:else}<EyeOff />{/if}</Toggle>
</span>
</Form.Control>
<Form.FieldErrors />
</Form.Field>

View file

@ -13,8 +13,8 @@ export const changeUserPasswordSchema = z
export type ChangeUserPasswordSchema = typeof changeUserPasswordSchema
const refinePasswords = async (confirm_password: string, password: string, ctx: z.RefinementCtx) => {
comparePasswords(confirm_password, password, ctx)
checkPasswordStrength(password, ctx)
await comparePasswords(confirm_password, password, ctx)
await checkPasswordStrength(password, ctx)
}
const comparePasswords = async (confirm_password: string, password: string, ctx: z.RefinementCtx) => {

View file

@ -1,14 +1,6 @@
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 type { Actions } 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 (event) => {
@ -19,7 +11,14 @@ export const load: PageServerLoad = async (event) => {
throw redirect(302, '/login', notSignedInMessage, event)
}
return {}
const { data: totpData, error: totpDataError } = await locals.api.mfa.totp.$get().then(locals.parseApiResponse)
const totpEnabled = !!totpData
return {
totpEnabled,
hardwareTokenEnabled: false,
}
}
export const actions: Actions = {}

View file

@ -5,8 +5,8 @@ import * as Card from '$lib/components/ui/card'
const { data } = $props()
const totpEnabled = true
const hardwareTokenEnabled = true
const totpEnabled = data.totpEnabled
const hardwareTokenEnabled = data.hardwareTokenEnabled
</script>
<h1>Two-factor authentication</h1>
@ -19,7 +19,7 @@ const hardwareTokenEnabled = true
<section>
<div class="two-factor-method">
<div class="two-factor-method-content">
<h2>Authenticator app {#if hardwareTokenEnabled}<Badge variant="outline" className="text-green-500 border-green-500">Configured</Badge>{/if}</h2>
<h2>Authenticator app {#if totpEnabled}<Badge variant="outline" className="text-green-500 border-green-500">Configured</Badge>{/if}</h2>
<p>Use an authenticator app or browser extension to get two-factor authentication codes when prompted.</p>
</div>
<Button href="/settings/security/mfa/totp">Edit</Button>

View file

@ -1,24 +1,12 @@
import { loadFlash } from 'sveltekit-flash-message/server';
import type { LayoutServerLoad } from '../$types';
// import { userFullyAuthenticated, userNotFullyAuthenticated } from '$lib/server/auth-utils';
// import { lucia } from '$lib/server/auth';
import { loadFlash } from 'sveltekit-flash-message/server'
import type { LayoutServerLoad } from '../$types'
export const load: LayoutServerLoad = loadFlash(async (event) => {
const { url, locals, cookies } = event;
const authedUser = await locals.getAuthedUser();
// if (userNotFullyAuthenticated(user, session)) {
// await lucia.invalidateSession(locals.session!.id!);
// const sessionCookie = lucia.createBlankSessionCookie();
// cookies.set(sessionCookie.name, sessionCookie.value, {
// path: '.',
// ...sessionCookie.attributes,
// });
// }
const { url, locals } = event
const authedUser = await locals.getAuthedUser()
return {
url: url.pathname,
// user: userFullyAuthenticated(user, session) ? locals.user : null,
user: authedUser,
};
});
authedUser,
}
})

View file

@ -1,37 +1,39 @@
<script lang="ts">
import 'iconify-icon';
import Header from '$components/Header.svelte';
import Footer from '$components/Footer.svelte';
import 'iconify-icon'
import Footer from '$components/Footer.svelte'
import Header from '$components/Header.svelte'
const { data, children } = $props();
console.log('layout data user', data.user);
const { data, children } = $props()
</script>
<Header user={data.user} />
<div class="flex min-h-screen w-full flex-col">
<Header user={data.authedUser} />
<main>
{@render children()}
</main>
<main
class="flex min-h-[calc(100vh_-_theme(spacing.16))] flex-1 flex-col gap-4 p-4 md:gap-8 md:p-10"
>
{@render children()}
</main>
<Footer />
<Footer />
</div>
<style lang="postcss">
main {
flex: 1;
display: flex;
flex-direction: column;
max-width: 850px;
margin: 0 auto;
padding: 2rem 0rem;
max-width: 80vw;
/*main {*/
/* flex: 1;*/
/* display: flex;*/
/* flex-direction: column;*/
/* max-width: 850px;*/
/* margin: 0 auto;*/
/* padding: 2rem 0rem;*/
/* max-width: 80vw;*/
@media (min-width: 1600px) {
max-width: 70vw;
}
/* @media (min-width: 1600px) {*/
/* max-width: 70vw;*/
/* }*/
box-sizing: border-box;
}
/* box-sizing: border-box;*/
/*}*/
:global(.dialog-overlay) {
position: fixed;

View file

@ -1,10 +1,6 @@
import { db } from '$lib/server/api/packages/drizzle'
import { fail } from '@sveltejs/kit'
import { eq } from 'drizzle-orm'
import type { MetaTagsProps } from 'svelte-meta-tags'
import { collections, usersTable, wishlistsTable } from '../../lib/server/api/databases/tables'
import type { PageServerLoad } from './$types'
// import { userFullyAuthenticated } from '$lib/server/auth-utils';
export const load: PageServerLoad = async (event) => {
const { locals, url } = event
@ -61,5 +57,7 @@ export const load: PageServerLoad = async (event) => {
}
}
console.log('Not Authed')
return { metaTagsChild: metaTags, user: null, wishlists: [], collections: [] }
}

View file

@ -73,21 +73,6 @@ onNavigate(async (navigation) => {
{/if}
<MetaTags {...metaTags} />
<PageLoadingIndicator />
<div class="layout">
{@render children()}
</div>
<Toaster />
<!-- <Loading /> -->
<style lang="postcss">
.layout {
display: flex;
position: relative;
flex-direction: column;
min-height: 100vh;
}
</style>
{@render children()}
<Toaster />