Bring back mailpit, add roles, user roles, and set up login form for differnent login options.

This commit is contained in:
Bradley Shellnut 2024-12-30 09:12:26 -08:00
parent 5497fb75a7
commit cc12d19250
22 changed files with 365 additions and 115 deletions

View file

@ -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

View file

@ -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);

View file

@ -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];

View file

@ -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({

View file

@ -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' });

View file

@ -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({

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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,
);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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)
};
}
}

View file

@ -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)
};
}
}

View file

@ -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);

View file

@ -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);
}
}

View 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);
}
}

View file

@ -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;
}

View file

@ -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>;

View file

@ -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;
</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>

View file

@ -1,19 +1,19 @@
<script module lang="ts">
import { z } from 'zod';
import { z } from 'zod';
export const loginSchema = z.object({
email: z.string().email(),
});
export const loginSchema = z.object({
email: z.string().email(),
});
export const loginPasswordSchema = z.object({
email: z.string().email(),
password: z.string({ required_error: 'Password is required' }),
})
export const loginPasswordSchema = z.object({
email: z.string().email(),
password: z.string({ required_error: 'Password is required' }),
});
export const verifySchema = z.object({
email: z.string().email(),
code: z.string().length(6)
});
export const verifySchema = z.object({
email: z.string().email(),
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('/');

View file

@ -12,8 +12,8 @@
<QueryClientProvider client={data.queryClient}>
<ParaglideJS {i18n}>
<ModeWatcher />
<main>
{@render children()}
<main class="antialiased">
{@render children?.()}
</main>
<SvelteQueryDevtools />
</ParaglideJS>