mirror of
https://github.com/BradNut/musicle-svelte
synced 2025-09-08 17:40:21 +00:00
Bring back mailpit, add roles, user roles, and set up login form for differnent login options.
This commit is contained in:
parent
5497fb75a7
commit
cc12d19250
22 changed files with 365 additions and 115 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<Parameters<NodePgDatabase<typeof drizzleSchema>["transaction"]>[0]>[0];
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { ConfigService } from '../../common/configs/config.service';
|
|||
export class DrizzleService {
|
||||
public db: NodePgDatabase<typeof drizzleSchema>;
|
||||
public schema: typeof drizzleSchema = drizzleSchema;
|
||||
|
||||
constructor(private configService = inject(ConfigService)) {
|
||||
this.db = drizzle(
|
||||
new Pool({
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
|
|
|||
|
|
@ -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,9 +41,13 @@ export class LoginRequestsService {
|
|||
throw BadRequest('Invalid credentials');
|
||||
}
|
||||
|
||||
try {
|
||||
if (!(await this.tokensService.verifyHashedToken(credential.secret_data, password))) {
|
||||
throw BadRequest('Invalid credentials');
|
||||
}
|
||||
} catch (error) {
|
||||
throw BadRequest('Invalid credentials');
|
||||
}
|
||||
|
||||
const totpCredentials = await this.credentialsRepository.findTOTPCredentialsByUserId(existingUser.id);
|
||||
|
||||
|
|
@ -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({
|
||||
|
|
|
|||
53
src/lib/server/api/roles/roles.repository.ts
Normal file
53
src/lib/server/api/roles/roles.repository.ts
Normal file
|
|
@ -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<typeof roles_table>;
|
||||
export type UpdateRole = Partial<CreateRole>;
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
11
src/lib/server/api/roles/roles.service.ts
Normal file
11
src/lib/server/api/roles/roles.service.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
39
src/lib/server/api/users/user_roles.repository.ts
Normal file
39
src/lib/server/api/users/user_roles.repository.ts
Normal file
|
|
@ -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<typeof user_roles_table>;
|
||||
export type UpdateUserRole = Partial<CreateUserRole>;
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
51
src/lib/server/api/users/user_roles.service.ts
Normal file
51
src/lib/server/api/users/user_roles.service.ts
Normal file
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Logout> {
|
||||
return {
|
||||
mutationFn: async () => await this.api.iam.logout.$post().then(parseApiResponse)
|
||||
mutationFn: async () => await this.api.iam.logout.$post().then(parseClientResponse)
|
||||
};
|
||||
}
|
||||
requestUsernamePasswordLogin(): ApiMutation<RequestUsernamePasswordLogin> {
|
||||
return {
|
||||
mutationFn: async (data: InferRequestType<RequestUsernamePasswordLogin>) =>
|
||||
await this.api.iam.login.$post(data).then(parseApiResponse)
|
||||
await this.api.iam.login.$post(data).then(parseClientResponse)
|
||||
}
|
||||
}
|
||||
requestLogin(): ApiMutation<RequestLogin> {
|
||||
return {
|
||||
mutationFn: async (data: InferRequestType<RequestLogin>) =>
|
||||
await this.api.iam.login.request.$post(data).then(parseApiResponse)
|
||||
await this.api.iam.login.request.$post(data).then(parseClientResponse)
|
||||
};
|
||||
}
|
||||
verifyLogin(): ApiMutation<VerifyLogin> {
|
||||
return {
|
||||
mutationFn: async (data: InferRequestType<VerifyLogin>) =>
|
||||
await this.api.iam.login.verify.$post(data).then(parseApiResponse)
|
||||
await this.api.iam.login.verify.$post(data).then(parseClientResponse)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Me> {
|
||||
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<UpdateUser> {
|
||||
return {
|
||||
mutationFn: async (args: InferRequestType<UpdateUser>) =>
|
||||
await this.api.users.me.$patch(args).then(parseClientResponse)
|
||||
};
|
||||
}
|
||||
|
||||
updateEmailRequest(): ApiMutation<UpdateEmailRequest> {
|
||||
return {
|
||||
mutationFn: async (args: InferRequestType<UpdateEmailRequest>) =>
|
||||
await this.api.users.me.email.request.$post(args).then(parseApiResponse)
|
||||
await this.api.users.me.email.request.$post(args).then(parseClientResponse)
|
||||
};
|
||||
}
|
||||
|
||||
verifyEmailRequest(): ApiMutation<VerifyEmailRequest> {
|
||||
return {
|
||||
mutationFn: async (args: InferRequestType<VerifyEmailRequest>) =>
|
||||
await this.api.users.me.email.verify.$post(args).then(parseApiResponse)
|
||||
await this.api.users.me.email.verify.$post(args).then(parseClientResponse)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import type { ClientRequestOptions } from 'hono';
|
||||
import { api } from '$lib/utils/api';
|
||||
|
||||
export abstract class TanstackQueryModule<T extends string | null = null> {
|
||||
protected readonly opts: ClientRequestOptions | undefined;
|
||||
protected readonly api: ReturnType<typeof api>;
|
||||
public namespace: T | null = null;
|
||||
|
||||
constructor(opts?: ClientRequestOptions) {
|
||||
this.opts = opts;
|
||||
this.api = api(opts);
|
||||
}
|
||||
}
|
||||
12
src/lib/tanstack-query/request-options.ts
Normal file
12
src/lib/tanstack-query/request-options.ts
Normal file
|
|
@ -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<typeof honoClient>;
|
||||
|
||||
constructor(opts?: ClientRequestOptions) {
|
||||
this.opts = opts;
|
||||
this.api = honoClient(opts);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ApiRoutes>('/', options).api;
|
||||
export const honoClient = (options?: ClientRequestOptions) => hc<ApiRoutes>('/', options).api;
|
||||
|
||||
export async function parseApiResponse<T>(response: ClientResponse<T>) {
|
||||
export async function parseClientResponse<T>(response: ClientResponse<T>) {
|
||||
if (response.ok) {
|
||||
return response.json() as T;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<T> = CreateMutationOptions<
|
|||
unknown
|
||||
>;
|
||||
export type ApiQuery<T> = CreateQueryOptions<InferResponseType<T>>;
|
||||
export type Api = ReturnType<typeof api>;
|
||||
export type Api = ReturnType<typeof honoClient>;
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
...api().iam.logout(),
|
||||
onSuccess: async () => {
|
||||
await data.queryClient.invalidateQueries();
|
||||
invalidateAll();
|
||||
goto('/login');
|
||||
await queryClient.invalidateQueries();
|
||||
await invalidateAll();
|
||||
}
|
||||
});
|
||||
queryHandler;
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-screen w-full flex-col">
|
||||
|
|
@ -86,11 +82,11 @@
|
|||
</div>
|
||||
</form>
|
||||
<ThemeDropdown />
|
||||
{#if !!data.authedUser}
|
||||
{#if !!$authedUserQuery.data}
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Button variant="secondary" size="icon" class="rounded-lg">
|
||||
<UserAvatar class="h-8 w-8 rounded-lg" user={data.authedUser} />
|
||||
<UserAvatar class="h-8 w-8 rounded-lg" user={$authedUserQuery.data} />
|
||||
<span class="sr-only">Toggle user menu</span>
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
|
@ -100,8 +96,7 @@
|
|||
<a class="w-full" href="/settings">Settings</a>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item >Logout</DropdownMenu.Item>
|
||||
onclick={$logoutMutation.mutate}
|
||||
<DropdownMenu.Item onclick={$logoutMutation.mutate}>Logout</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
<script module lang="ts">
|
||||
import { z } from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const loginSchema = z.object({
|
||||
export const loginSchema = z.object({
|
||||
email: z.string().email(),
|
||||
});
|
||||
});
|
||||
|
||||
export const loginPasswordSchema = z.object({
|
||||
export const loginPasswordSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string({ required_error: 'Password is required' }),
|
||||
})
|
||||
});
|
||||
|
||||
export const verifySchema = z.object({
|
||||
export const verifySchema = z.object({
|
||||
email: z.string().email(),
|
||||
code: z.string().length(6)
|
||||
});
|
||||
code: z.string().length(6),
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
import { createMutation, useQueryClient } from '@tanstack/svelte-query';
|
||||
import { goto } from '$app/navigation';
|
||||
import ChevronLeftIcon from 'lucide-svelte/icons/chevron-left';
|
||||
import { queryHandler } from '$lib/tanstack-query';
|
||||
import { api } from '$lib/tanstack-query';
|
||||
import * as InputOTP from '$lib/components/ui/input-otp/index.js';
|
||||
import Separator from '@/components/ui/separator/separator.svelte';
|
||||
|
||||
|
|
@ -41,19 +41,20 @@
|
|||
|
||||
/* ----------------------------------- Api ---------------------------------- */
|
||||
const requestUsernamePasswordLoginMutation = createMutation({
|
||||
...queryHandler().iam.requestUsernamePasswordLogin(),
|
||||
...api().iam.requestUsernamePasswordLogin(),
|
||||
onSuccess(_data, variables, _context) {
|
||||
step = 'totp';
|
||||
$loginPasswordForm.email = variables.json.email;
|
||||
$loginPasswordForm.password = variables.json.password;
|
||||
},
|
||||
onError(error) {
|
||||
loginPasswordErrors.set({ email: [error.message] });
|
||||
const { message } = JSON.parse(error.message);
|
||||
loginPasswordErrors.set({ email: [message] });
|
||||
}
|
||||
})
|
||||
|
||||
const requestMutation = createMutation({
|
||||
...queryHandler().iam.requestLogin(),
|
||||
...api().iam.requestLogin(),
|
||||
onSuccess(_data, variables, _context) {
|
||||
step = 'verify';
|
||||
$verifyForm.email = variables.json.email;
|
||||
|
|
@ -65,7 +66,7 @@
|
|||
});
|
||||
|
||||
const verifyMutation = createMutation({
|
||||
...queryHandler().iam.verifyLogin(),
|
||||
...api().iam.verifyLogin(),
|
||||
async onSuccess() {
|
||||
await queryClient.invalidateQueries();
|
||||
goto('/');
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@
|
|||
<QueryClientProvider client={data.queryClient}>
|
||||
<ParaglideJS {i18n}>
|
||||
<ModeWatcher />
|
||||
<main>
|
||||
{@render children()}
|
||||
<main class="antialiased">
|
||||
{@render children?.()}
|
||||
</main>
|
||||
<SvelteQueryDevtools />
|
||||
</ParaglideJS>
|
||||
|
|
|
|||
Loading…
Reference in a new issue