diff --git a/package.json b/package.json
index e4997a2..bddfaf3 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c00d4b2..94e8b55 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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:
diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte
index f614316..4b3a06b 100644
--- a/src/lib/components/Header.svelte
+++ b/src/lib/components/Header.svelte
@@ -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() || ':)')
@@ -28,39 +23,48 @@ let avatar: string = $derived(user?.username?.slice(0, 1).toUpperCase() || ':)')
+
+
+{#snippet userDropdown()}
+
+
+
+
+ {avatar}
+
+
+
+
+
+ Account
+
+
+
+
+ Settings
+
+
+
+
+
+ Collections
+
+
+
+
+
+ Wishlists
+
+
+
+
-
-
-
-
- {:else}
- Login
- Sign Up
- {/if}
-
-
+ action="/logout"
+ method="POST"
+ >
+
+
+
+
+
+
+{/snippet}
\ No newline at end of file
diff --git a/src/lib/components/ui/input/input.svelte b/src/lib/components/ui/input/input.svelte
index cab1457..477d325 100644
--- a/src/lib/components/ui/input/input.svelte
+++ b/src/lib/components/ui/input/input.svelte
@@ -1,7 +1,7 @@
diff --git a/src/lib/server/api/common/config.ts b/src/lib/server/api/common/config.ts
index 518c73f..0daf6d4 100644
--- a/src/lib/server/api/common/config.ts
+++ b/src/lib/server/api/common/config.ts
@@ -34,5 +34,3 @@ export const config: Config = {
max: env.DB_MIGRATING || env.DB_SEEDING ? 1 : undefined,
},
}
-
-console.log('config', config)
diff --git a/src/lib/server/api/common/exceptions.ts b/src/lib/server/api/common/exceptions.ts
index fee54c4..cbda928 100644
--- a/src/lib/server/api/common/exceptions.ts
+++ b/src/lib/server/api/common/exceptions.ts
@@ -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 })
}
diff --git a/src/lib/server/api/controllers/collection.controller.ts b/src/lib/server/api/controllers/collection.controller.ts
index 56a53aa..86dfcc5 100644
--- a/src/lib/server/api/controllers/collection.controller.ts
+++ b/src/lib/server/api/controllers/collection.controller.ts
@@ -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 {
diff --git a/src/lib/server/api/controllers/iam.controller.ts b/src/lib/server/api/controllers/iam.controller.ts
index 708b7ef..0d2010c 100644
--- a/src/lib/server/api/controllers/iam.controller.ts
+++ b/src/lib/server/api/controllers/iam.controller.ts
@@ -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')
diff --git a/src/lib/server/api/controllers/mfa.controller.ts b/src/lib/server/api/controllers/mfa.controller.ts
index f70aa04..b08b8c1 100644
--- a/src/lib/server/api/controllers/mfa.controller.ts
+++ b/src/lib/server/api/controllers/mfa.controller.ts
@@ -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 {
diff --git a/src/lib/server/api/controllers/user.controller.ts b/src/lib/server/api/controllers/user.controller.ts
index e7c5deb..c05cd2e 100644
--- a/src/lib/server/api/controllers/user.controller.ts
+++ b/src/lib/server/api/controllers/user.controller.ts
@@ -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 {
diff --git a/src/lib/server/api/controllers/wishlist.controller.ts b/src/lib/server/api/controllers/wishlist.controller.ts
index 44a4a11..1a8da1c 100644
--- a/src/lib/server/api/controllers/wishlist.controller.ts
+++ b/src/lib/server/api/controllers/wishlist.controller.ts
@@ -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 {
diff --git a/src/lib/server/api/dtos/change-password.dto.ts b/src/lib/server/api/dtos/change-password.dto.ts
new file mode 100644
index 0000000..b624722
--- /dev/null
+++ b/src/lib/server/api/dtos/change-password.dto.ts
@@ -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
diff --git a/src/lib/server/api/dtos/update-profile.dto.ts b/src/lib/server/api/dtos/update-profile.dto.ts
index 9ea0c6f..1ce379b 100644
--- a/src/lib/server/api/dtos/update-profile.dto.ts
+++ b/src/lib/server/api/dtos/update-profile.dto.ts
@@ -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;
+export type UpdateProfileDto = z.infer
diff --git a/src/lib/server/api/jobs/auth-cleanup.job.ts b/src/lib/server/api/jobs/auth-cleanup.job.ts
index fb2ec6c..efaf1c1 100644
--- a/src/lib/server/api/jobs/auth-cleanup.job.ts
+++ b/src/lib/server/api/jobs/auth-cleanup.job.ts
@@ -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') {
diff --git a/src/lib/server/api/middleware/auth.middleware.ts b/src/lib/server/api/middleware/auth.middleware.ts
index 3cc3766..b88418b 100644
--- a/src/lib/server/api/middleware/auth.middleware.ts
+++ b/src/lib/server/api/middleware/auth.middleware.ts
@@ -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 = 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()
-})
diff --git a/src/lib/server/api/services/iam.service.ts b/src/lib/server/api/services/iam.service.ts
index 0c3bc6e..a70e8c3 100644
--- a/src/lib/server/api/services/iam.service.ts
+++ b/src/lib/server/api/services/iam.service.ts
@@ -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) {
diff --git a/src/lib/server/api/services/users.service.ts b/src/lib/server/api/services/users.service.ts
index 89e6eea..f067809 100644
--- a/src/lib/server/api/services/users.service.ts
+++ b/src/lib/server/api/services/users.service.ts
@@ -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) {
diff --git a/src/routes/(app)/(protected)/settings/+layout.svelte b/src/routes/(app)/(protected)/settings/+layout.svelte
index c4620d3..81483b4 100644
--- a/src/routes/(app)/(protected)/settings/+layout.svelte
+++ b/src/routes/(app)/(protected)/settings/+layout.svelte
@@ -1,5 +1,5 @@
-
- {@render children()}
-
\ No newline at end of file
+
+
+
+ {@render children()}
+
+
+
+
diff --git a/src/routes/(app)/(protected)/settings/security/change/password/+page.server.ts b/src/routes/(app)/(protected)/settings/security/change/password/+page.server.ts
index e708aa5..1c4e775 100644
--- a/src/routes/(app)/(protected)/settings/security/change/password/+page.server.ts
+++ b/src/routes/(app)/(protected)/settings/security/change/password/+page.server.ts
@@ -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 = ''
diff --git a/src/routes/(app)/(protected)/settings/security/change/password/+page.svelte b/src/routes/(app)/(protected)/settings/security/change/password/+page.svelte
index 302379a..1a46b42 100644
--- a/src/routes/(app)/(protected)/settings/security/change/password/+page.svelte
+++ b/src/routes/(app)/(protected)/settings/security/change/password/+page.svelte
@@ -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