From cc12d1925064645c698bcdc4bd0d391b0e7a4932 Mon Sep 17 00:00:00 2001 From: Bradley Shellnut Date: Mon, 30 Dec 2024 09:12:26 -0800 Subject: [PATCH] Bring back mailpit, add roles, user roles, and set up login form for differnent login options. --- docker-compose.yml | 30 +++---- src/hooks.server.ts | 6 +- src/lib/server/api/common/utils/drizzle.ts | 4 + .../api/databases/postgres/drizzle.service.ts | 1 + src/lib/server/api/iam/iam.controller.ts | 3 +- .../login-requests/login-requests.service.ts | 31 +++++--- src/lib/server/api/roles/roles.repository.ts | 53 +++++++++++++ src/lib/server/api/roles/roles.service.ts | 11 +++ .../server/api/users/user_roles.repository.ts | 39 +++++++++ .../server/api/users/user_roles.service.ts | 51 ++++++++++++ src/lib/server/api/users/users.repository.ts | 4 + src/lib/server/api/users/users.service.ts | 79 ++++++++++++++++++- src/lib/tanstack-query/{ => domains}/iam.ts | 26 +++--- src/lib/tanstack-query/{ => domains}/users.ts | 29 ++++--- src/lib/tanstack-query/index.ts | 10 +-- src/lib/tanstack-query/query-module.ts | 13 --- src/lib/tanstack-query/request-options.ts | 12 +++ src/lib/utils/api.ts | 6 +- src/lib/utils/types.ts | 4 +- src/routes/(app)/+layout.svelte | 29 +++---- .../login/(components)/login-form.svelte | 35 ++++---- src/routes/+layout.svelte | 4 +- 22 files changed, 365 insertions(+), 115 deletions(-) create mode 100644 src/lib/server/api/roles/roles.repository.ts create mode 100644 src/lib/server/api/roles/roles.service.ts create mode 100644 src/lib/server/api/users/user_roles.repository.ts create mode 100644 src/lib/server/api/users/user_roles.service.ts rename src/lib/tanstack-query/{ => domains}/iam.ts (62%) rename src/lib/tanstack-query/{ => domains}/users.ts (59%) delete mode 100644 src/lib/tanstack-query/query-module.ts create mode 100644 src/lib/tanstack-query/request-options.ts diff --git a/docker-compose.yml b/docker-compose.yml index 84dd553..a6e92f1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,25 +36,25 @@ services: - MINIO_ROOT_PASSWORD=password - MINIO_DEFAULT_BUCKETS=dev - # mailpit: - # image: axllent/mailpit - # volumes: - # - mailpit_data:/data - # ports: - # - 8025:8025 - # - 1025:1025 - # environment: - # MP_MAX_MESSAGES: 5000 - # MP_DATABASE: /data/mailpit.db - # MP_SMTP_AUTH_ACCEPT_ANY: 1 - # MP_SMTP_AUTH_ALLOW_INSECURE: 1 - # networks: - # - app-network + mailpit: + image: axllent/mailpit + volumes: + - mailpit_data:/data + ports: + - 8025:8025 + - 1025:1025 + environment: + MP_MAX_MESSAGES: 5000 + MP_DATABASE: /data/mailpit.db + MP_SMTP_AUTH_ACCEPT_ANY: 1 + MP_SMTP_AUTH_ALLOW_INSECURE: 1 + networks: + - app-network volumes: postgres_data: redis_data: - # mailpit_data: + mailpit_data: minio_data: driver: local diff --git a/src/hooks.server.ts b/src/hooks.server.ts index a5e522c..5a5feda 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,10 +1,12 @@ -import type { Handle } from '@sveltejs/kit'; +import type { Handle, ServerInit } from '@sveltejs/kit'; import { i18n } from '$lib/i18n'; import { sequence } from '@sveltejs/kit/hooks'; import { startServer } from '$lib/server/api'; const handleParaglide: Handle = i18n.handle(); -startServer(); +export const init: ServerInit = async () => { + await startServer(); +}; export const handle: Handle = sequence(handleParaglide); diff --git a/src/lib/server/api/common/utils/drizzle.ts b/src/lib/server/api/common/utils/drizzle.ts index 8b21b69..341fa59 100644 --- a/src/lib/server/api/common/utils/drizzle.ts +++ b/src/lib/server/api/common/utils/drizzle.ts @@ -1,5 +1,7 @@ import { customType, timestamp } from 'drizzle-orm/pg-core'; import { NotFound } from './exceptions'; +import type { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import * as drizzleSchema from '../../databases/postgres/drizzle-schema'; /* -------------------------------------------------------------------------- */ /* Repository */ @@ -51,3 +53,5 @@ export const timestamps = { .defaultNow() .$onUpdateFn(() => new Date()) }; + +export type Transaction = Parameters["transaction"]>[0]>[0]; diff --git a/src/lib/server/api/databases/postgres/drizzle.service.ts b/src/lib/server/api/databases/postgres/drizzle.service.ts index 3e63809..5032f14 100644 --- a/src/lib/server/api/databases/postgres/drizzle.service.ts +++ b/src/lib/server/api/databases/postgres/drizzle.service.ts @@ -8,6 +8,7 @@ import { ConfigService } from '../../common/configs/config.service'; export class DrizzleService { public db: NodePgDatabase; public schema: typeof drizzleSchema = drizzleSchema; + constructor(private configService = inject(ConfigService)) { this.db = drizzle( new Pool({ diff --git a/src/lib/server/api/iam/iam.controller.ts b/src/lib/server/api/iam/iam.controller.ts index 5698a01..4377eb2 100644 --- a/src/lib/server/api/iam/iam.controller.ts +++ b/src/lib/server/api/iam/iam.controller.ts @@ -9,6 +9,7 @@ import { authState } from '../common/middleware/auth.middleware'; import { Controller } from '../common/factories/controllers.factory'; import { loginRequestDto } from './login-requests/dtos/login-request.dto'; import { signInEmail } from './login-requests/routes/login.routes'; +import { rateLimit } from '../common/middleware/rate-limit.middleware'; @injectable() export class IamController extends Controller { @@ -21,7 +22,7 @@ export class IamController extends Controller { routes() { return this.controller - .post('/login', openApi(signInEmail), authState('none'), zValidator('json', loginRequestDto), async (c) => { + .post('/login', openApi(signInEmail), authState('none'), zValidator('json', loginRequestDto), rateLimit({ limit: 3, minutes: 1 }), async (c) => { const session = await this.loginRequestsService.login(c.req.valid('json')); await this.sessionsService.setSessionCookie(session); return c.json({ message: 'welcome' }); diff --git a/src/lib/server/api/iam/login-requests/login-requests.service.ts b/src/lib/server/api/iam/login-requests/login-requests.service.ts index d41675e..b8d4391 100644 --- a/src/lib/server/api/iam/login-requests/login-requests.service.ts +++ b/src/lib/server/api/iam/login-requests/login-requests.service.ts @@ -1,18 +1,19 @@ import { inject, injectable } from '@needle-di/core'; -import { LoginRequestsRepository } from './login-requests.repository'; +import { TokensService } from '../../common/services/tokens.service'; +import { VerificationCodesService } from '../../common/services/verification-codes.service'; +import { BadRequest, NotFound } from '../../common/utils/exceptions'; import { MailerService } from '../../mail/mailer.service'; import { LoginVerificationEmail } from '../../mail/templates/login-verification.template'; -import { BadRequest, NotFound } from '../../common/utils/exceptions'; import { WelcomeEmail } from '../../mail/templates/welcome.template'; -import { SessionsService } from '../sessions/sessions.service'; -import type { VerifyLoginRequestDto } from './dtos/verify-login-request.dto'; -import type { CreateLoginRequestDto } from './dtos/create-login-request.dto'; -import { UsersService } from '../../users/users.service'; -import { UsersRepository } from '../../users/users.repository'; -import { VerificationCodesService } from '../../common/services/verification-codes.service'; -import type { LoginRequestDto } from './dtos/login-request.dto'; import { CredentialsRepository } from '../../users/credentials.repository'; -import { TokensService } from '../../common/services/tokens.service'; +import { UsersRepository } from '../../users/users.repository'; +import { UsersService } from '../../users/users.service'; +import { SessionsService } from '../sessions/sessions.service'; +import type { CreateLoginRequestDto } from './dtos/create-login-request.dto'; +import type { LoginRequestDto } from './dtos/login-request.dto'; +import type { VerifyLoginRequestDto } from './dtos/verify-login-request.dto'; +import { LoginRequestsRepository } from './login-requests.repository'; +import { logger } from 'hono-pino'; @injectable() export class LoginRequestsService { @@ -31,7 +32,7 @@ export class LoginRequestsService { const existingUser = await this.usersRepository.findOneByEmail(email); if (!existingUser) { - throw NotFound('User not found'); + throw BadRequest('Invalid credentials'); } const credential = await this.credentialsRepository.findPasswordCredentialsByUserId(existingUser.id); @@ -40,7 +41,11 @@ export class LoginRequestsService { throw BadRequest('Invalid credentials'); } - if (!(await this.tokensService.verifyHashedToken(credential.secret_data, password))) { + try { + if (!(await this.tokensService.verifyHashedToken(credential.secret_data, password))) { + throw BadRequest('Invalid credentials'); + } + } catch (error) { throw BadRequest('Invalid credentials'); } @@ -97,7 +102,7 @@ export class LoginRequestsService { private async authNewUser({ email }: { email: string }) { // create a new user - const user = await this.usersService.create(email); + const user = await this.usersService.createEmail(email); // send the welcome email await this.mailer.send({ diff --git a/src/lib/server/api/roles/roles.repository.ts b/src/lib/server/api/roles/roles.repository.ts new file mode 100644 index 0000000..f49d14f --- /dev/null +++ b/src/lib/server/api/roles/roles.repository.ts @@ -0,0 +1,53 @@ +import { DrizzleService } from '$lib/server/api/databases/postgres/drizzle.service'; +import { inject, injectable } from '@needle-di/core'; +import { type InferInsertModel, eq } from 'drizzle-orm'; +import { takeFirstOrThrow } from '../common/utils/drizzle'; +import { roles_table } from './tables/roles.table'; + +export type CreateRole = InferInsertModel; +export type UpdateRole = Partial; + +@injectable() +export class RolesRepository { + constructor(private drizzle = inject(DrizzleService)) {} + + async findOneById(id: string, db = this.drizzle.db) { + return db.query.roles_table.findFirst({ + where: eq(roles_table.id, id), + }); + } + + async findOneByIdOrThrow(id: string, db = this.drizzle.db) { + const role = await this.findOneById(id, db); + if (!role) throw Error('Role not found'); + return role; + } + + async findAll(db = this.drizzle.db) { + return db.query.roles_table.findMany(); + } + + async findOneByName(name: string, db = this.drizzle.db) { + return db.query.roles_table.findFirst({ + where: eq(roles_table.name, name), + }); + } + + async findOneByNameOrThrow(name: string, db = this.drizzle.db) { + const role = await this.findOneByName(name, db); + if (!role) throw Error('Role not found'); + return role; + } + + async create(data: CreateRole, db = this.drizzle.db) { + return db.insert(roles_table).values(data).returning().then(takeFirstOrThrow); + } + + async update(id: string, data: UpdateRole, db = this.drizzle.db) { + return db.update(roles_table).set(data).where(eq(roles_table.id, id)).returning().then(takeFirstOrThrow); + } + + async delete(id: string, db = this.drizzle.db) { + return db.delete(roles_table).where(eq(roles_table.id, id)).returning().then(takeFirstOrThrow); + } +} diff --git a/src/lib/server/api/roles/roles.service.ts b/src/lib/server/api/roles/roles.service.ts new file mode 100644 index 0000000..ad89a77 --- /dev/null +++ b/src/lib/server/api/roles/roles.service.ts @@ -0,0 +1,11 @@ +import { RolesRepository } from './roles.repository'; +import { inject, injectable } from '@needle-di/core'; + +@injectable() +export class RolesService { + constructor(private rolesRepository = inject(RolesRepository)) {} + + async findOneByNameOrThrow(name: string) { + return this.rolesRepository.findOneByNameOrThrow(name); + } +} diff --git a/src/lib/server/api/users/user_roles.repository.ts b/src/lib/server/api/users/user_roles.repository.ts new file mode 100644 index 0000000..051582e --- /dev/null +++ b/src/lib/server/api/users/user_roles.repository.ts @@ -0,0 +1,39 @@ +import { DrizzleService } from '$lib/server/api/databases/postgres/drizzle.service'; +import { inject, injectable } from '@needle-di/core'; +import { type InferInsertModel, eq } from 'drizzle-orm'; +import { takeFirstOrThrow } from '../common/utils/drizzle'; +import { user_roles_table } from './tables/user-roles.table'; + +export type CreateUserRole = InferInsertModel; +export type UpdateUserRole = Partial; + +@injectable() +export class UserRolesRepository { + constructor(private drizzle = inject(DrizzleService)) {} + + async findOneById(id: string, db = this.drizzle.db) { + return db.query.user_roles_table.findFirst({ + where: eq(user_roles_table.id, id), + }); + } + + async findOneByIdOrThrow(id: string, db = this.drizzle.db) { + const userRole = await this.findOneById(id, db); + if (!userRole) throw Error('User not found'); + return userRole; + } + + async findAllByUserId(userId: string, db = this.drizzle.db) { + return db.query.user_roles_table.findMany({ + where: eq(user_roles_table.user_id, userId), + }); + } + + async create(data: CreateUserRole, db = this.drizzle.db) { + return db.insert(user_roles_table).values(data).returning().then(takeFirstOrThrow); + } + + async delete(id: string, db = this.drizzle.db) { + return db.delete(user_roles_table).where(eq(user_roles_table.id, id)).returning().then(takeFirstOrThrow); + } +} diff --git a/src/lib/server/api/users/user_roles.service.ts b/src/lib/server/api/users/user_roles.service.ts new file mode 100644 index 0000000..6cadc24 --- /dev/null +++ b/src/lib/server/api/users/user_roles.service.ts @@ -0,0 +1,51 @@ +import type { Transaction } from '../common/utils/drizzle'; +import { RolesService } from '../roles/roles.service'; +import { type CreateUserRole, UserRolesRepository } from './user_roles.repository'; +import { inject, injectable } from '@needle-di/core'; + +@injectable() +export class UserRolesService { + constructor( + private userRolesRepository = inject(UserRolesRepository), + private rolesService = inject(RolesService), + ) {} + + async findOneById(id: string) { + return this.userRolesRepository.findOneById(id); + } + + async findAllByUserId(userId: string) { + return this.userRolesRepository.findAllByUserId(userId); + } + + async create(data: CreateUserRole) { + return this.userRolesRepository.create(data); + } + + async addRoleToUser(userId: string, roleName: string, primary = false, trx: Transaction | null = null) { + // Find the role by its name + const role = await this.rolesService.findOneByNameOrThrow(roleName); + + if (!role || !role.id) { + throw new Error(`Role with name ${roleName} not found`); + } + + if (!trx) { + return this.userRolesRepository.create({ + user_id: userId, + role_id: role.id, + primary, + }); + } + + // Create a UserRole entry linking the user and the role + return this.userRolesRepository.create( + { + user_id: userId, + role_id: role.id, + primary, + }, + trx, + ); + } +} diff --git a/src/lib/server/api/users/users.repository.ts b/src/lib/server/api/users/users.repository.ts index 1ba4b4a..6833146 100644 --- a/src/lib/server/api/users/users.repository.ts +++ b/src/lib/server/api/users/users.repository.ts @@ -37,4 +37,8 @@ export class UsersRepository extends DrizzleRepository { async create(data: Create, db = this.drizzle.db) { return db.insert(users_table).values(data).returning().then(takeFirstOrThrow); } + + async delete(id: string, db = this.drizzle.db) { + return db.delete(users_table).where(eq(users_table.id, id)).returning().then(takeFirstOrThrow); + } } diff --git a/src/lib/server/api/users/users.service.ts b/src/lib/server/api/users/users.service.ts index eb3398a..a1e70cf 100644 --- a/src/lib/server/api/users/users.service.ts +++ b/src/lib/server/api/users/users.service.ts @@ -1,13 +1,23 @@ import { inject, injectable } from '@needle-di/core'; -import { UsersRepository } from './users.repository'; -import type { UpdateUserDto } from './dtos/update-user.dto'; +import { TokensService } from '../common/services/tokens.service'; +import { DrizzleService } from '../databases/postgres/drizzle.service'; import { StorageService } from '../storage/storage.service'; +import { CredentialsRepository } from './credentials.repository'; +import type { UpdateUserDto } from './dtos/update-user.dto'; +import { CredentialsType } from './tables/credentials.table'; +import { UsersRepository } from './users.repository'; +import { UserRolesService } from './user_roles.service'; +import { RoleName } from '../roles/tables/roles.table'; @injectable() export class UsersService { constructor( + private drizzleService = inject(DrizzleService), + private credentialsRepository = inject(CredentialsRepository), private usersRepository = inject(UsersRepository), - private storageService = inject(StorageService) + private userRoleService = inject(UserRolesService), + private storageService = inject(StorageService), + private tokenService = inject(TokensService) ) {} async update(userId: string, updateUserDto: UpdateUserDto) { @@ -17,7 +27,68 @@ export class UsersService { } } - async create(email: string) { + async createEmail(email: string) { return this.usersRepository.create({ avatar: null, email }); } + + async createEmailPassword(email: string, password: string) { + const hashedPassword = await this.tokenService.createHashedToken(password); + return await this.drizzleService.db.transaction(async (trx) => { + const createdUser = await this.usersRepository.create( + { email, avatar: null }, + trx, + ); + + if (!createdUser) { + return null; + } + + const credentials = await this.credentialsRepository.create( + { + user_id: createdUser.id, + type: CredentialsType.PASSWORD, + secret_data: hashedPassword, + }, + trx, + ); + + if (!credentials) { + await trx.rollback(); + return null; + } + + await this.userRoleService.addRoleToUser(createdUser.id, RoleName.USER, true, trx); + + return createdUser; + }); + } + + 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) { + 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); + } } diff --git a/src/lib/tanstack-query/iam.ts b/src/lib/tanstack-query/domains/iam.ts similarity index 62% rename from src/lib/tanstack-query/iam.ts rename to src/lib/tanstack-query/domains/iam.ts index 3a4082b..16d33da 100644 --- a/src/lib/tanstack-query/iam.ts +++ b/src/lib/tanstack-query/domains/iam.ts @@ -1,41 +1,43 @@ -import type { InferRequestType } from 'hono'; -import { parseApiResponse } from '$lib/utils/api'; +import { parseClientResponse } from '$lib/utils/api'; import type { Api, ApiMutation } from '$lib/utils/types'; -import { TanstackQueryModule } from './query-module'; +import type { InferRequestType } from 'hono'; +import { TanstackRequestOptions } from '../request-options'; /* -------------------------------------------------------------------------- */ /* Types */ /* -------------------------------------------------------------------------- */ -type RequestUsernamePasswordLogin = Api['iam']['login']['$post']; -type RequestLogin = Api['iam']['login']['request']['$post']; -type VerifyLogin = Api['iam']['login']['verify']['$post']; -type Logout = Api['iam']['logout']['$post']; +export type RequestUsernamePasswordLogin = Api['iam']['login']['$post']; +export type RequestLogin = Api['iam']['login']['request']['$post']; +export type VerifyLogin = Api['iam']['login']['verify']['$post']; +export type Logout = Api['iam']['logout']['$post']; /* -------------------------------------------------------------------------- */ /* Api */ /* -------------------------------------------------------------------------- */ -export class IamModule extends TanstackQueryModule<'iam'> { +export class IamModule extends TanstackRequestOptions { + namespace = 'iam'; + logout(): ApiMutation { return { - mutationFn: async () => await this.api.iam.logout.$post().then(parseApiResponse) + mutationFn: async () => await this.api.iam.logout.$post().then(parseClientResponse) }; } requestUsernamePasswordLogin(): ApiMutation { return { mutationFn: async (data: InferRequestType) => - await this.api.iam.login.$post(data).then(parseApiResponse) + await this.api.iam.login.$post(data).then(parseClientResponse) } } requestLogin(): ApiMutation { return { mutationFn: async (data: InferRequestType) => - await this.api.iam.login.request.$post(data).then(parseApiResponse) + await this.api.iam.login.request.$post(data).then(parseClientResponse) }; } verifyLogin(): ApiMutation { return { mutationFn: async (data: InferRequestType) => - await this.api.iam.login.verify.$post(data).then(parseApiResponse) + await this.api.iam.login.verify.$post(data).then(parseClientResponse) }; } } diff --git a/src/lib/tanstack-query/users.ts b/src/lib/tanstack-query/domains/users.ts similarity index 59% rename from src/lib/tanstack-query/users.ts rename to src/lib/tanstack-query/domains/users.ts index dff5916..7a5f2a5 100644 --- a/src/lib/tanstack-query/users.ts +++ b/src/lib/tanstack-query/domains/users.ts @@ -1,37 +1,48 @@ -import { parseApiResponse } from '$lib/utils/api'; +import { parseClientResponse } from '$lib/utils/api'; import type { Api, ApiMutation, ApiQuery } from '$lib/utils/types'; import type { InferRequestType } from 'hono'; -import { TanstackQueryModule } from './query-module'; +import { TanstackRequestOptions } from '../request-options'; /* -------------------------------------------------------------------------- */ /* Types */ /* -------------------------------------------------------------------------- */ -type Me = Api['users']['me']['$get']; -type UpdateEmailRequest = Api['users']['me']['email']['request']['$post']; -type VerifyEmailRequest = Api['users']['me']['email']['verify']['$post']; +export type Me = Api['users']['me']['$get']; +export type UpdateEmailRequest = Api['users']['me']['email']['request']['$post']; +export type VerifyEmailRequest = Api['users']['me']['email']['verify']['$post']; +export type UpdateUser = Api['users']['me']['$patch']; + /* -------------------------------------------------------------------------- */ /* Api */ /* -------------------------------------------------------------------------- */ -export class UsersModule extends TanstackQueryModule<'users'> { +export class UsersModule extends TanstackRequestOptions { + namespace = 'users'; + me(): ApiQuery { return { queryKey: [this.namespace, 'me'], - queryFn: async () => await this.api.users.me.$get().then(parseApiResponse) + queryFn: async () => await this.api.users.me.$get().then(parseClientResponse) + }; + } + + update(): ApiMutation { + return { + mutationFn: async (args: InferRequestType) => + await this.api.users.me.$patch(args).then(parseClientResponse) }; } updateEmailRequest(): ApiMutation { return { mutationFn: async (args: InferRequestType) => - await this.api.users.me.email.request.$post(args).then(parseApiResponse) + await this.api.users.me.email.request.$post(args).then(parseClientResponse) }; } verifyEmailRequest(): ApiMutation { return { mutationFn: async (args: InferRequestType) => - await this.api.users.me.email.verify.$post(args).then(parseApiResponse) + await this.api.users.me.email.verify.$post(args).then(parseClientResponse) }; } } diff --git a/src/lib/tanstack-query/index.ts b/src/lib/tanstack-query/index.ts index 2e5fc1a..da27fc3 100644 --- a/src/lib/tanstack-query/index.ts +++ b/src/lib/tanstack-query/index.ts @@ -1,11 +1,11 @@ -import { IamModule } from './iam'; +import { IamModule } from './domains/iam'; import type { ClientRequestOptions } from 'hono'; -import { UsersModule } from './users'; -import { TanstackQueryModule } from './query-module'; +import { UsersModule } from './domains/users'; +import { TanstackRequestOptions } from './request-options'; -class TanstackQueryHandler extends TanstackQueryModule { +class TanstackQueryModule extends TanstackRequestOptions { iam = new IamModule(this.opts); users = new UsersModule(this.opts); } -export const queryHandler = (opts?: ClientRequestOptions) => new TanstackQueryHandler(opts); +export const api = (opts?: ClientRequestOptions) => new TanstackQueryModule(opts); \ No newline at end of file diff --git a/src/lib/tanstack-query/query-module.ts b/src/lib/tanstack-query/query-module.ts deleted file mode 100644 index 6eede2f..0000000 --- a/src/lib/tanstack-query/query-module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { ClientRequestOptions } from 'hono'; -import { api } from '$lib/utils/api'; - -export abstract class TanstackQueryModule { - protected readonly opts: ClientRequestOptions | undefined; - protected readonly api: ReturnType; - public namespace: T | null = null; - - constructor(opts?: ClientRequestOptions) { - this.opts = opts; - this.api = api(opts); - } -} diff --git a/src/lib/tanstack-query/request-options.ts b/src/lib/tanstack-query/request-options.ts new file mode 100644 index 0000000..bef5abe --- /dev/null +++ b/src/lib/tanstack-query/request-options.ts @@ -0,0 +1,12 @@ +import type { ClientRequestOptions } from 'hono'; +import { honoClient } from '$lib/utils/api'; + +export abstract class TanstackRequestOptions { + protected readonly opts: ClientRequestOptions | undefined; + protected readonly api: ReturnType; + + constructor(opts?: ClientRequestOptions) { + this.opts = opts; + this.api = honoClient(opts); + } +} diff --git a/src/lib/utils/api.ts b/src/lib/utils/api.ts index 065d560..b964ac1 100644 --- a/src/lib/utils/api.ts +++ b/src/lib/utils/api.ts @@ -1,10 +1,10 @@ import type { ApiRoutes } from '$lib/server/api'; import type { ClientRequestOptions } from 'hono'; -import { hc, type ClientResponse } from 'hono/client'; +import { type ClientResponse, hc } from 'hono/client'; -export const api = (options?: ClientRequestOptions) => hc('/', options).api; +export const honoClient = (options?: ClientRequestOptions) => hc('/', options).api; -export async function parseApiResponse(response: ClientResponse) { +export async function parseClientResponse(response: ClientResponse) { if (response.ok) { return response.json() as T; } diff --git a/src/lib/utils/types.ts b/src/lib/utils/types.ts index ed647f7..aa9730e 100644 --- a/src/lib/utils/types.ts +++ b/src/lib/utils/types.ts @@ -1,4 +1,4 @@ -import type { api } from '$lib/utils/api'; +import type { honoClient } from '$lib/utils/api'; import type { CreateMutationOptions, CreateQueryOptions, @@ -13,4 +13,4 @@ export type ApiMutation = CreateMutationOptions< unknown >; export type ApiQuery = CreateQueryOptions>; -export type Api = ReturnType; +export type Api = ReturnType; diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index 723734f..c59fb2b 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -6,28 +6,24 @@ import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js'; import { Input } from '$lib/components/ui/input/index.js'; import * as Sheet from '$lib/components/ui/sheet/index.js'; - import { createMutation } from '@tanstack/svelte-query'; - import { authContext } from '$lib/hooks/session.svelte.js'; - import { queryHandler } from '$lib/tanstack-query/index.js'; - import { goto, invalidateAll } from '$app/navigation'; + import { createMutation, createQuery, useQueryClient } from '@tanstack/svelte-query'; + import { api } from '$lib/tanstack-query/index.js'; import UserAvatar from '$lib/components/user-avatar.svelte'; import ThemeDropdown from '$lib/components/theme-dropdown.svelte'; + import { invalidateAll } from '$app/navigation'; const { children, data } = $props(); - $effect.pre(() => { - authContext.setAuthedUser(data.authedUser); - }); + const queryClient = useQueryClient(); + const authedUserQuery = createQuery(api().users.me()); - const logoutMutation = createMutation({ - ...queryHandler().iam.logout(), + const logoutMutation = createMutation({ + ...api().iam.logout(), onSuccess: async () => { - await data.queryClient.invalidateQueries(); - invalidateAll(); - goto('/login'); + await queryClient.invalidateQueries(); + await invalidateAll(); } }); - queryHandler;
@@ -86,11 +82,11 @@
- {#if !!data.authedUser} + {#if !!$authedUserQuery.data} @@ -100,8 +96,7 @@ Settings - Logout - onclick={$logoutMutation.mutate} + Logout diff --git a/src/routes/(app)/login/(components)/login-form.svelte b/src/routes/(app)/login/(components)/login-form.svelte index fe5ad67..1d935be 100644 --- a/src/routes/(app)/login/(components)/login-form.svelte +++ b/src/routes/(app)/login/(components)/login-form.svelte @@ -1,19 +1,19 @@