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_ROOT_PASSWORD=password
|
||||||
- MINIO_DEFAULT_BUCKETS=dev
|
- MINIO_DEFAULT_BUCKETS=dev
|
||||||
|
|
||||||
# mailpit:
|
mailpit:
|
||||||
# image: axllent/mailpit
|
image: axllent/mailpit
|
||||||
# volumes:
|
volumes:
|
||||||
# - mailpit_data:/data
|
- mailpit_data:/data
|
||||||
# ports:
|
ports:
|
||||||
# - 8025:8025
|
- 8025:8025
|
||||||
# - 1025:1025
|
- 1025:1025
|
||||||
# environment:
|
environment:
|
||||||
# MP_MAX_MESSAGES: 5000
|
MP_MAX_MESSAGES: 5000
|
||||||
# MP_DATABASE: /data/mailpit.db
|
MP_DATABASE: /data/mailpit.db
|
||||||
# MP_SMTP_AUTH_ACCEPT_ANY: 1
|
MP_SMTP_AUTH_ACCEPT_ANY: 1
|
||||||
# MP_SMTP_AUTH_ALLOW_INSECURE: 1
|
MP_SMTP_AUTH_ALLOW_INSECURE: 1
|
||||||
# networks:
|
networks:
|
||||||
# - app-network
|
- app-network
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
redis_data:
|
redis_data:
|
||||||
# mailpit_data:
|
mailpit_data:
|
||||||
minio_data:
|
minio_data:
|
||||||
driver: local
|
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 { i18n } from '$lib/i18n';
|
||||||
import { sequence } from '@sveltejs/kit/hooks';
|
import { sequence } from '@sveltejs/kit/hooks';
|
||||||
import { startServer } from '$lib/server/api';
|
import { startServer } from '$lib/server/api';
|
||||||
|
|
||||||
const handleParaglide: Handle = i18n.handle();
|
const handleParaglide: Handle = i18n.handle();
|
||||||
|
|
||||||
startServer();
|
export const init: ServerInit = async () => {
|
||||||
|
await startServer();
|
||||||
|
};
|
||||||
|
|
||||||
export const handle: Handle = sequence(handleParaglide);
|
export const handle: Handle = sequence(handleParaglide);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { customType, timestamp } from 'drizzle-orm/pg-core';
|
import { customType, timestamp } from 'drizzle-orm/pg-core';
|
||||||
import { NotFound } from './exceptions';
|
import { NotFound } from './exceptions';
|
||||||
|
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||||
|
import * as drizzleSchema from '../../databases/postgres/drizzle-schema';
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* Repository */
|
/* Repository */
|
||||||
|
|
@ -51,3 +53,5 @@ export const timestamps = {
|
||||||
.defaultNow()
|
.defaultNow()
|
||||||
.$onUpdateFn(() => new Date())
|
.$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 {
|
export class DrizzleService {
|
||||||
public db: NodePgDatabase<typeof drizzleSchema>;
|
public db: NodePgDatabase<typeof drizzleSchema>;
|
||||||
public schema: typeof drizzleSchema = drizzleSchema;
|
public schema: typeof drizzleSchema = drizzleSchema;
|
||||||
|
|
||||||
constructor(private configService = inject(ConfigService)) {
|
constructor(private configService = inject(ConfigService)) {
|
||||||
this.db = drizzle(
|
this.db = drizzle(
|
||||||
new Pool({
|
new Pool({
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { authState } from '../common/middleware/auth.middleware';
|
||||||
import { Controller } from '../common/factories/controllers.factory';
|
import { Controller } from '../common/factories/controllers.factory';
|
||||||
import { loginRequestDto } from './login-requests/dtos/login-request.dto';
|
import { loginRequestDto } from './login-requests/dtos/login-request.dto';
|
||||||
import { signInEmail } from './login-requests/routes/login.routes';
|
import { signInEmail } from './login-requests/routes/login.routes';
|
||||||
|
import { rateLimit } from '../common/middleware/rate-limit.middleware';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class IamController extends Controller {
|
export class IamController extends Controller {
|
||||||
|
|
@ -21,7 +22,7 @@ export class IamController extends Controller {
|
||||||
|
|
||||||
routes() {
|
routes() {
|
||||||
return this.controller
|
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'));
|
const session = await this.loginRequestsService.login(c.req.valid('json'));
|
||||||
await this.sessionsService.setSessionCookie(session);
|
await this.sessionsService.setSessionCookie(session);
|
||||||
return c.json({ message: 'welcome' });
|
return c.json({ message: 'welcome' });
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
import { inject, injectable } from '@needle-di/core';
|
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 { MailerService } from '../../mail/mailer.service';
|
||||||
import { LoginVerificationEmail } from '../../mail/templates/login-verification.template';
|
import { LoginVerificationEmail } from '../../mail/templates/login-verification.template';
|
||||||
import { BadRequest, NotFound } from '../../common/utils/exceptions';
|
|
||||||
import { WelcomeEmail } from '../../mail/templates/welcome.template';
|
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 { 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()
|
@injectable()
|
||||||
export class LoginRequestsService {
|
export class LoginRequestsService {
|
||||||
|
|
@ -31,7 +32,7 @@ export class LoginRequestsService {
|
||||||
const existingUser = await this.usersRepository.findOneByEmail(email);
|
const existingUser = await this.usersRepository.findOneByEmail(email);
|
||||||
|
|
||||||
if (!existingUser) {
|
if (!existingUser) {
|
||||||
throw NotFound('User not found');
|
throw BadRequest('Invalid credentials');
|
||||||
}
|
}
|
||||||
|
|
||||||
const credential = await this.credentialsRepository.findPasswordCredentialsByUserId(existingUser.id);
|
const credential = await this.credentialsRepository.findPasswordCredentialsByUserId(existingUser.id);
|
||||||
|
|
@ -40,7 +41,11 @@ export class LoginRequestsService {
|
||||||
throw BadRequest('Invalid credentials');
|
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');
|
throw BadRequest('Invalid credentials');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,7 +102,7 @@ export class LoginRequestsService {
|
||||||
|
|
||||||
private async authNewUser({ email }: { email: string }) {
|
private async authNewUser({ email }: { email: string }) {
|
||||||
// create a new user
|
// create a new user
|
||||||
const user = await this.usersService.create(email);
|
const user = await this.usersService.createEmail(email);
|
||||||
|
|
||||||
// send the welcome email
|
// send the welcome email
|
||||||
await this.mailer.send({
|
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) {
|
async create(data: Create, db = this.drizzle.db) {
|
||||||
return db.insert(users_table).values(data).returning().then(takeFirstOrThrow);
|
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 { inject, injectable } from '@needle-di/core';
|
||||||
import { UsersRepository } from './users.repository';
|
import { TokensService } from '../common/services/tokens.service';
|
||||||
import type { UpdateUserDto } from './dtos/update-user.dto';
|
import { DrizzleService } from '../databases/postgres/drizzle.service';
|
||||||
import { StorageService } from '../storage/storage.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()
|
@injectable()
|
||||||
export class UsersService {
|
export class UsersService {
|
||||||
constructor(
|
constructor(
|
||||||
|
private drizzleService = inject(DrizzleService),
|
||||||
|
private credentialsRepository = inject(CredentialsRepository),
|
||||||
private usersRepository = inject(UsersRepository),
|
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) {
|
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 });
|
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 { parseClientResponse } from '$lib/utils/api';
|
||||||
import { parseApiResponse } from '$lib/utils/api';
|
|
||||||
import type { Api, ApiMutation } from '$lib/utils/types';
|
import type { Api, ApiMutation } from '$lib/utils/types';
|
||||||
import { TanstackQueryModule } from './query-module';
|
import type { InferRequestType } from 'hono';
|
||||||
|
import { TanstackRequestOptions } from '../request-options';
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* Types */
|
/* Types */
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
type RequestUsernamePasswordLogin = Api['iam']['login']['$post'];
|
export type RequestUsernamePasswordLogin = Api['iam']['login']['$post'];
|
||||||
type RequestLogin = Api['iam']['login']['request']['$post'];
|
export type RequestLogin = Api['iam']['login']['request']['$post'];
|
||||||
type VerifyLogin = Api['iam']['login']['verify']['$post'];
|
export type VerifyLogin = Api['iam']['login']['verify']['$post'];
|
||||||
type Logout = Api['iam']['logout']['$post'];
|
export type Logout = Api['iam']['logout']['$post'];
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* Api */
|
/* Api */
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
export class IamModule extends TanstackQueryModule<'iam'> {
|
export class IamModule extends TanstackRequestOptions {
|
||||||
|
namespace = 'iam';
|
||||||
|
|
||||||
logout(): ApiMutation<Logout> {
|
logout(): ApiMutation<Logout> {
|
||||||
return {
|
return {
|
||||||
mutationFn: async () => await this.api.iam.logout.$post().then(parseApiResponse)
|
mutationFn: async () => await this.api.iam.logout.$post().then(parseClientResponse)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
requestUsernamePasswordLogin(): ApiMutation<RequestUsernamePasswordLogin> {
|
requestUsernamePasswordLogin(): ApiMutation<RequestUsernamePasswordLogin> {
|
||||||
return {
|
return {
|
||||||
mutationFn: async (data: InferRequestType<RequestUsernamePasswordLogin>) =>
|
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> {
|
requestLogin(): ApiMutation<RequestLogin> {
|
||||||
return {
|
return {
|
||||||
mutationFn: async (data: InferRequestType<RequestLogin>) =>
|
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> {
|
verifyLogin(): ApiMutation<VerifyLogin> {
|
||||||
return {
|
return {
|
||||||
mutationFn: async (data: InferRequestType<VerifyLogin>) =>
|
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 { Api, ApiMutation, ApiQuery } from '$lib/utils/types';
|
||||||
import type { InferRequestType } from 'hono';
|
import type { InferRequestType } from 'hono';
|
||||||
import { TanstackQueryModule } from './query-module';
|
import { TanstackRequestOptions } from '../request-options';
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* Types */
|
/* Types */
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
type Me = Api['users']['me']['$get'];
|
export type Me = Api['users']['me']['$get'];
|
||||||
type UpdateEmailRequest = Api['users']['me']['email']['request']['$post'];
|
export type UpdateEmailRequest = Api['users']['me']['email']['request']['$post'];
|
||||||
type VerifyEmailRequest = Api['users']['me']['email']['verify']['$post'];
|
export type VerifyEmailRequest = Api['users']['me']['email']['verify']['$post'];
|
||||||
|
export type UpdateUser = Api['users']['me']['$patch'];
|
||||||
|
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* Api */
|
/* Api */
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
export class UsersModule extends TanstackQueryModule<'users'> {
|
export class UsersModule extends TanstackRequestOptions {
|
||||||
|
namespace = 'users';
|
||||||
|
|
||||||
me(): ApiQuery<Me> {
|
me(): ApiQuery<Me> {
|
||||||
return {
|
return {
|
||||||
queryKey: [this.namespace, 'me'],
|
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> {
|
updateEmailRequest(): ApiMutation<UpdateEmailRequest> {
|
||||||
return {
|
return {
|
||||||
mutationFn: async (args: InferRequestType<UpdateEmailRequest>) =>
|
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> {
|
verifyEmailRequest(): ApiMutation<VerifyEmailRequest> {
|
||||||
return {
|
return {
|
||||||
mutationFn: async (args: InferRequestType<VerifyEmailRequest>) =>
|
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 type { ClientRequestOptions } from 'hono';
|
||||||
import { UsersModule } from './users';
|
import { UsersModule } from './domains/users';
|
||||||
import { TanstackQueryModule } from './query-module';
|
import { TanstackRequestOptions } from './request-options';
|
||||||
|
|
||||||
class TanstackQueryHandler extends TanstackQueryModule {
|
class TanstackQueryModule extends TanstackRequestOptions {
|
||||||
iam = new IamModule(this.opts);
|
iam = new IamModule(this.opts);
|
||||||
users = new UsersModule(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 { ApiRoutes } from '$lib/server/api';
|
||||||
import type { ClientRequestOptions } from 'hono';
|
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) {
|
if (response.ok) {
|
||||||
return response.json() as T;
|
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 {
|
import type {
|
||||||
CreateMutationOptions,
|
CreateMutationOptions,
|
||||||
CreateQueryOptions,
|
CreateQueryOptions,
|
||||||
|
|
@ -13,4 +13,4 @@ export type ApiMutation<T> = CreateMutationOptions<
|
||||||
unknown
|
unknown
|
||||||
>;
|
>;
|
||||||
export type ApiQuery<T> = CreateQueryOptions<InferResponseType<T>>;
|
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 * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||||
import { Input } from '$lib/components/ui/input/index.js';
|
import { Input } from '$lib/components/ui/input/index.js';
|
||||||
import * as Sheet from '$lib/components/ui/sheet/index.js';
|
import * as Sheet from '$lib/components/ui/sheet/index.js';
|
||||||
import { createMutation } from '@tanstack/svelte-query';
|
import { createMutation, createQuery, useQueryClient } from '@tanstack/svelte-query';
|
||||||
import { authContext } from '$lib/hooks/session.svelte.js';
|
import { api } from '$lib/tanstack-query/index.js';
|
||||||
import { queryHandler } from '$lib/tanstack-query/index.js';
|
|
||||||
import { goto, invalidateAll } from '$app/navigation';
|
|
||||||
import UserAvatar from '$lib/components/user-avatar.svelte';
|
import UserAvatar from '$lib/components/user-avatar.svelte';
|
||||||
import ThemeDropdown from '$lib/components/theme-dropdown.svelte';
|
import ThemeDropdown from '$lib/components/theme-dropdown.svelte';
|
||||||
|
import { invalidateAll } from '$app/navigation';
|
||||||
|
|
||||||
const { children, data } = $props();
|
const { children, data } = $props();
|
||||||
|
|
||||||
$effect.pre(() => {
|
const queryClient = useQueryClient();
|
||||||
authContext.setAuthedUser(data.authedUser);
|
const authedUserQuery = createQuery(api().users.me());
|
||||||
});
|
|
||||||
|
|
||||||
const logoutMutation = createMutation({
|
const logoutMutation = createMutation({
|
||||||
...queryHandler().iam.logout(),
|
...api().iam.logout(),
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await data.queryClient.invalidateQueries();
|
await queryClient.invalidateQueries();
|
||||||
invalidateAll();
|
await invalidateAll();
|
||||||
goto('/login');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
queryHandler;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex min-h-screen w-full flex-col">
|
<div class="flex min-h-screen w-full flex-col">
|
||||||
|
|
@ -86,11 +82,11 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<ThemeDropdown />
|
<ThemeDropdown />
|
||||||
{#if !!data.authedUser}
|
{#if !!$authedUserQuery.data}
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger>
|
<DropdownMenu.Trigger>
|
||||||
<Button variant="secondary" size="icon" class="rounded-lg">
|
<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>
|
<span class="sr-only">Toggle user menu</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Trigger>
|
||||||
|
|
@ -100,8 +96,7 @@
|
||||||
<a class="w-full" href="/settings">Settings</a>
|
<a class="w-full" href="/settings">Settings</a>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Separator />
|
<DropdownMenu.Separator />
|
||||||
<DropdownMenu.Item >Logout</DropdownMenu.Item>
|
<DropdownMenu.Item onclick={$logoutMutation.mutate}>Logout</DropdownMenu.Item>
|
||||||
onclick={$logoutMutation.mutate}
|
|
||||||
</DropdownMenu.Group>
|
</DropdownMenu.Group>
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
<script module lang="ts">
|
<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(),
|
email: z.string().email(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const loginPasswordSchema = z.object({
|
export const loginPasswordSchema = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
password: z.string({ required_error: 'Password is required' }),
|
password: z.string({ required_error: 'Password is required' }),
|
||||||
})
|
});
|
||||||
|
|
||||||
export const verifySchema = z.object({
|
export const verifySchema = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
code: z.string().length(6)
|
code: z.string().length(6),
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
import { createMutation, useQueryClient } from '@tanstack/svelte-query';
|
import { createMutation, useQueryClient } from '@tanstack/svelte-query';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import ChevronLeftIcon from 'lucide-svelte/icons/chevron-left';
|
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 * as InputOTP from '$lib/components/ui/input-otp/index.js';
|
||||||
import Separator from '@/components/ui/separator/separator.svelte';
|
import Separator from '@/components/ui/separator/separator.svelte';
|
||||||
|
|
||||||
|
|
@ -41,19 +41,20 @@
|
||||||
|
|
||||||
/* ----------------------------------- Api ---------------------------------- */
|
/* ----------------------------------- Api ---------------------------------- */
|
||||||
const requestUsernamePasswordLoginMutation = createMutation({
|
const requestUsernamePasswordLoginMutation = createMutation({
|
||||||
...queryHandler().iam.requestUsernamePasswordLogin(),
|
...api().iam.requestUsernamePasswordLogin(),
|
||||||
onSuccess(_data, variables, _context) {
|
onSuccess(_data, variables, _context) {
|
||||||
step = 'totp';
|
step = 'totp';
|
||||||
$loginPasswordForm.email = variables.json.email;
|
$loginPasswordForm.email = variables.json.email;
|
||||||
$loginPasswordForm.password = variables.json.password;
|
$loginPasswordForm.password = variables.json.password;
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
loginPasswordErrors.set({ email: [error.message] });
|
const { message } = JSON.parse(error.message);
|
||||||
|
loginPasswordErrors.set({ email: [message] });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const requestMutation = createMutation({
|
const requestMutation = createMutation({
|
||||||
...queryHandler().iam.requestLogin(),
|
...api().iam.requestLogin(),
|
||||||
onSuccess(_data, variables, _context) {
|
onSuccess(_data, variables, _context) {
|
||||||
step = 'verify';
|
step = 'verify';
|
||||||
$verifyForm.email = variables.json.email;
|
$verifyForm.email = variables.json.email;
|
||||||
|
|
@ -65,7 +66,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
const verifyMutation = createMutation({
|
const verifyMutation = createMutation({
|
||||||
...queryHandler().iam.verifyLogin(),
|
...api().iam.verifyLogin(),
|
||||||
async onSuccess() {
|
async onSuccess() {
|
||||||
await queryClient.invalidateQueries();
|
await queryClient.invalidateQueries();
|
||||||
goto('/');
|
goto('/');
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@
|
||||||
<QueryClientProvider client={data.queryClient}>
|
<QueryClientProvider client={data.queryClient}>
|
||||||
<ParaglideJS {i18n}>
|
<ParaglideJS {i18n}>
|
||||||
<ModeWatcher />
|
<ModeWatcher />
|
||||||
<main>
|
<main class="antialiased">
|
||||||
{@render children()}
|
{@render children?.()}
|
||||||
</main>
|
</main>
|
||||||
<SvelteQueryDevtools />
|
<SvelteQueryDevtools />
|
||||||
</ParaglideJS>
|
</ParaglideJS>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue