mirror of
https://github.com/BradNut/TofuStack
synced 2025-09-08 17:40:26 +00:00
refactored auth services
This commit is contained in:
parent
80ba1c9861
commit
b33590edad
13 changed files with 215 additions and 110 deletions
2
src/app.d.ts
vendored
2
src/app.d.ts
vendored
|
|
@ -12,7 +12,7 @@ declare global {
|
|||
api: ApiClient['api'];
|
||||
parseApiResponse: typeof parseApiResponse;
|
||||
getAuthedUser: () => Promise<Returned<User> | null>;
|
||||
getAuthedUserOrThrow: () => Promise<Returned<User>>;
|
||||
getAuthedUserOrThrow: (redirectTo: string) => Promise<Returned<User>>;
|
||||
}
|
||||
|
||||
// interface PageData {}
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@ const apiClient: Handle = async ({ event, resolve }) => {
|
|||
|
||||
/* ----------------------------- Auth functions ----------------------------- */
|
||||
async function getAuthedUser() {
|
||||
const { data } = await api.iam.user.$get().then(parseApiResponse)
|
||||
const { data } = await api.users.me.$get().then(parseApiResponse)
|
||||
return data && data.user;
|
||||
}
|
||||
|
||||
async function getAuthedUserOrThrow() {
|
||||
const { data } = await api.iam.user.$get().then(parseApiResponse);
|
||||
if (!data || !data.user) throw redirect(StatusCodes.TEMPORARY_REDIRECT, '/');
|
||||
async function getAuthedUserOrThrow(redirectTo = '/') {
|
||||
const { data } = await api.users.me.$get().then(parseApiResponse);
|
||||
if (!data || !data.user) throw redirect(StatusCodes.TEMPORARY_REDIRECT, redirectTo);
|
||||
return data?.user;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
import { setCookie } from 'hono/cookie';
|
||||
import { inject, injectable } from 'tsyringe';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { IamService } from '../services/iam.service';
|
||||
import { limiter } from '../middlewares/rate-limiter.middlware';
|
||||
import { requireAuth } from '../middlewares/auth.middleware';
|
||||
import { requireAuth } from '../middlewares/require-auth.middleware';
|
||||
import { Controler } from '../common/types/controller';
|
||||
import { registerEmailDto } from '$lib/server/api/dtos/register-email.dto';
|
||||
import { signInEmailDto } from '$lib/server/api/dtos/signin-email.dto';
|
||||
import { updateEmailDto } from '$lib/server/api/dtos/update-email.dto';
|
||||
import { verifyEmailDto } from '$lib/server/api/dtos/verify-email.dto';
|
||||
import { LuciaService } from '../services/lucia.service';
|
||||
import { AuthenticationService } from '../services/authentication.service';
|
||||
import { EmailVerificationService } from '../services/email-verification.service';
|
||||
import { loginDto } from '../dtos/login.dto';
|
||||
import { verifyLoginDto } from '../dtos/verify-login.dto';
|
||||
|
||||
@injectable()
|
||||
export class IamController extends Controler {
|
||||
constructor(
|
||||
@inject(IamService) private iamService: IamService,
|
||||
@inject(AuthenticationService) private authenticationService: AuthenticationService,
|
||||
@inject(EmailVerificationService) private emailVerificationService: EmailVerificationService,
|
||||
@inject(LuciaService) private luciaService: LuciaService,
|
||||
) {
|
||||
super();
|
||||
|
|
@ -22,18 +24,18 @@ export class IamController extends Controler {
|
|||
|
||||
routes() {
|
||||
return this.controller
|
||||
.get('/user', async (c) => {
|
||||
.get('/me', async (c) => {
|
||||
const user = c.var.user;
|
||||
return c.json({ user: user });
|
||||
})
|
||||
.post('/login/request', zValidator('json', registerEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
.post('/login', zValidator('json', loginDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
const { email } = c.req.valid('json');
|
||||
await this.iamService.createLoginRequest({ email });
|
||||
await this.authenticationService.createLoginRequest({ email });
|
||||
return c.json({ message: 'Verification email sent' });
|
||||
})
|
||||
.post('/login/verify', zValidator('json', signInEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
.post('/login/verify', zValidator('json', verifyLoginDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
const { email, token } = c.req.valid('json');
|
||||
const session = await this.iamService.verifyLoginRequest({ email, token });
|
||||
const session = await this.authenticationService.verifyLoginRequest({ email, token });
|
||||
const sessionCookie = this.luciaService.lucia.createSessionCookie(session.id);
|
||||
setCookie(c, sessionCookie.name, sessionCookie.value, {
|
||||
path: sessionCookie.attributes.path,
|
||||
|
|
@ -46,9 +48,21 @@ export class IamController extends Controler {
|
|||
});
|
||||
return c.json({ message: 'ok' });
|
||||
})
|
||||
.patch('/email', requireAuth, zValidator('json', updateEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
const json = c.req.valid('json');
|
||||
await this.emailVerificationService.create(c.var.user.id, json.email);
|
||||
return c.json({ message: 'Verification email sent' });
|
||||
})
|
||||
// this could also be named to use custom methods, aka /email#verify
|
||||
// https://cloud.google.com/apis/design/custom_methods
|
||||
.post('/email/verify', requireAuth, zValidator('json', verifyEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
const json = c.req.valid('json');
|
||||
await this.emailVerificationService.verify(c.var.user.id, json.token);
|
||||
return c.json({ message: 'Verified and updated' });
|
||||
})
|
||||
.post('/logout', requireAuth, async (c) => {
|
||||
const sessionId = c.var.session.id;
|
||||
await this.iamService.logout(sessionId);
|
||||
await this.authenticationService.logout(sessionId);
|
||||
const sessionCookie = this.luciaService.lucia.createBlankSessionCookie();
|
||||
setCookie(c, sessionCookie.name, sessionCookie.value, {
|
||||
path: sessionCookie.attributes.path,
|
||||
|
|
@ -61,17 +75,5 @@ export class IamController extends Controler {
|
|||
});
|
||||
return c.json({ status: 'success' });
|
||||
})
|
||||
.patch('/email', requireAuth, zValidator('json', updateEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
const json = c.req.valid('json');
|
||||
await this.iamService.dispatchEmailVerificationRequest(c.var.user.id, json.email);
|
||||
return c.json({ message: 'Verification email sent' });
|
||||
})
|
||||
// this could also be named to use custom methods, aka /email#verify
|
||||
// https://cloud.google.com/apis/design/custom_methods
|
||||
.post('/email/verification', requireAuth, zValidator('json', verifyEmailDto), limiter({ limit: 10, minutes: 60 }), async (c) => {
|
||||
const json = c.req.valid('json');
|
||||
await this.iamService.processEmailVerificationRequest(c.var.user.id, json.token);
|
||||
return c.json({ message: 'Verified and updated' });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
src/lib/server/api/dtos/login.dto.ts
Normal file
7
src/lib/server/api/dtos/login.dto.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const loginDto = z.object({
|
||||
email: z.string().email()
|
||||
});
|
||||
|
||||
export type LoginDto = z.infer<typeof loginDto>;
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const registerEmailDto = z.object({
|
||||
email: z.string().email()
|
||||
});
|
||||
|
||||
export type RegisterEmailDto = z.infer<typeof registerEmailDto>;
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const signInEmailDto = z.object({
|
||||
email: z.string().email(),
|
||||
token: z.string()
|
||||
});
|
||||
|
||||
export type SignInEmailDto = z.infer<typeof signInEmailDto>;
|
||||
8
src/lib/server/api/dtos/verify-login.dto.ts
Normal file
8
src/lib/server/api/dtos/verify-login.dto.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const verifyLoginDto = z.object({
|
||||
email: z.string().email(),
|
||||
token: z.string()
|
||||
});
|
||||
|
||||
export type VerifyLoginDto = z.infer<typeof verifyLoginDto>;
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
import type { MiddlewareHandler } from 'hono';
|
||||
import { createMiddleware } from 'hono/factory';
|
||||
import { verifyRequestOrigin } from 'lucia';
|
||||
import type { Session, User } from 'lucia';
|
||||
import { Unauthorized } from '../common/exceptions';
|
||||
import type { HonoTypes } from '../common/types/hono';
|
||||
import { container } from 'tsyringe';
|
||||
import { LuciaService } from '../services/lucia.service';
|
||||
|
|
@ -41,15 +39,4 @@ export const validateAuthSession: MiddlewareHandler<HonoTypes> = createMiddlewar
|
|||
c.set("session", session);
|
||||
c.set("user", user);
|
||||
return next();
|
||||
})
|
||||
|
||||
export const requireAuth: MiddlewareHandler<{
|
||||
Variables: {
|
||||
session: Session;
|
||||
user: User;
|
||||
};
|
||||
}> = createMiddleware(async (c, next) => {
|
||||
const user = c.var.user;
|
||||
if (!user) throw Unauthorized('You must be logged in to access this resource');
|
||||
return next();
|
||||
});
|
||||
})
|
||||
15
src/lib/server/api/middlewares/require-auth.middleware.ts
Normal file
15
src/lib/server/api/middlewares/require-auth.middleware.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import type { MiddlewareHandler } from "hono";
|
||||
import { createMiddleware } from "hono/factory";
|
||||
import type { Session, User } from "lucia";
|
||||
import { Unauthorized } from "../common/exceptions";
|
||||
|
||||
export const requireAuth: MiddlewareHandler<{
|
||||
Variables: {
|
||||
session: Session;
|
||||
user: User;
|
||||
};
|
||||
}> = createMiddleware(async (c, next) => {
|
||||
const user = c.var.user;
|
||||
if (!user) throw Unauthorized('You must be logged in to access this resource');
|
||||
return next();
|
||||
});
|
||||
80
src/lib/server/api/services/authentication.service.ts
Normal file
80
src/lib/server/api/services/authentication.service.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { inject, injectable } from 'tsyringe';
|
||||
import { MailerService } from './mailer.service';
|
||||
import { TokensService } from './tokens.service';
|
||||
import { UsersRepository } from '../repositories/users.repository';
|
||||
import type { VerifyLoginDto } from '../dtos/verify-login.dto';
|
||||
import type { LoginDto } from '../dtos/login.dto';
|
||||
import { LoginRequestsRepository } from '../repositories/login-requests.repository';
|
||||
import { LoginVerificationEmail } from '../emails/login-verification.email';
|
||||
import { BadRequest } from '../common/exceptions';
|
||||
import { WelcomeEmail } from '../emails/welcome.email';
|
||||
import { DrizzleService } from './drizzle.service';
|
||||
import { LuciaService } from './lucia.service';
|
||||
|
||||
@injectable()
|
||||
export class AuthenticationService {
|
||||
constructor(
|
||||
@inject(LuciaService) private readonly luciaService: LuciaService,
|
||||
@inject(DrizzleService) private readonly drizzleService: DrizzleService,
|
||||
@inject(TokensService) private readonly tokensService: TokensService,
|
||||
@inject(MailerService) private readonly mailerService: MailerService,
|
||||
@inject(UsersRepository) private readonly usersRepository: UsersRepository,
|
||||
@inject(LoginRequestsRepository) private readonly loginRequestsRepository: LoginRequestsRepository,
|
||||
) { }
|
||||
|
||||
async createLoginRequest(data: LoginDto) {
|
||||
// generate a token, expiry date, and hash
|
||||
const { token, expiry, hashedToken } = await this.tokensService.generateTokenWithExpiryAndHash(15, 'm');
|
||||
// save the login request to the database - ensuring we save the hashedToken
|
||||
await this.loginRequestsRepository.create({ email: data.email, hashedToken, expiresAt: expiry });
|
||||
// send the login request email
|
||||
await this.mailerService.send({ email: new LoginVerificationEmail(token), to: data.email });
|
||||
}
|
||||
|
||||
async verifyLoginRequest(data: VerifyLoginDto) {
|
||||
const validLoginRequest = await this.getValidLoginRequest(data.email, data.token);
|
||||
if (!validLoginRequest) throw BadRequest('Invalid token');
|
||||
|
||||
let existingUser = await this.usersRepository.findOneByEmail(data.email);
|
||||
|
||||
if (!existingUser) {
|
||||
const newUser = await this.handleNewUserRegistration(data.email);
|
||||
return this.luciaService.lucia.createSession(newUser.id, {});
|
||||
}
|
||||
|
||||
return this.luciaService.lucia.createSession(existingUser.id, {});
|
||||
}
|
||||
|
||||
async logout(sessionId: string) {
|
||||
return this.luciaService.lucia.invalidateSession(sessionId);
|
||||
}
|
||||
|
||||
// Create a new user and send a welcome email - or other onboarding process
|
||||
private async handleNewUserRegistration(email: string) {
|
||||
const newUser = await this.usersRepository.create({ email, verified: true })
|
||||
await this.mailerService.send({ email: new WelcomeEmail(), to: newUser.email });
|
||||
// TODO: add whatever onboarding process or extra data you need here
|
||||
return newUser
|
||||
}
|
||||
|
||||
// Fetch a valid request from the database, verify the token and burn the request if it is valid
|
||||
private async getValidLoginRequest(email: string, token: string) {
|
||||
return await this.drizzleService.db.transaction(async (trx) => {
|
||||
// fetch the login request
|
||||
const loginRequest = await this.loginRequestsRepository.findOneByEmail(email, trx)
|
||||
if (!loginRequest) return null;
|
||||
|
||||
// check if the token is valid
|
||||
const isValidRequest = await this.tokensService.verifyHashedToken(loginRequest.hashedToken, token);
|
||||
if (!isValidRequest) return null
|
||||
|
||||
// if the token is valid, burn the request
|
||||
await this.loginRequestsRepository.deleteById(loginRequest.id, trx);
|
||||
return loginRequest
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
69
src/lib/server/api/services/email-verification.service.ts
Normal file
69
src/lib/server/api/services/email-verification.service.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { inject, injectable } from 'tsyringe';
|
||||
import { MailerService } from './mailer.service';
|
||||
import { TokensService } from './tokens.service';
|
||||
import { UsersRepository } from '../repositories/users.repository';
|
||||
import { LoginVerificationEmail } from '../emails/login-verification.email';
|
||||
import { BadRequest } from '../common/exceptions';
|
||||
import { EmailVerificationsRepository } from '../repositories/email-verifications.repository';
|
||||
import { EmailChangeNoticeEmail } from '../emails/email-change-notice.email';
|
||||
import { DrizzleService } from './drizzle.service';
|
||||
|
||||
@injectable()
|
||||
export class EmailVerificationService {
|
||||
constructor(
|
||||
@inject(DrizzleService) private readonly drizzleService: DrizzleService,
|
||||
@inject(TokensService) private readonly tokensService: TokensService,
|
||||
@inject(MailerService) private readonly mailerService: MailerService,
|
||||
@inject(UsersRepository) private readonly usersRepository: UsersRepository,
|
||||
@inject(EmailVerificationsRepository) private readonly emailVerificationsRepository: EmailVerificationsRepository,
|
||||
) { }
|
||||
|
||||
// These steps follow the process outlined in OWASP's "Changing A User's Email Address" guide.
|
||||
// https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#changing-a-users-registered-email-address
|
||||
async create(userId: string, requestedEmail: string) {
|
||||
// generate a token and expiry
|
||||
const { token, expiry, hashedToken } = await this.tokensService.generateTokenWithExpiryAndHash(15, 'm')
|
||||
const user = await this.usersRepository.findOneByIdOrThrow(userId)
|
||||
|
||||
// create a new email verification record
|
||||
await this.emailVerificationsRepository.create({ requestedEmail, userId, hashedToken, expiresAt: expiry })
|
||||
|
||||
// A confirmation-required email message to the proposed new address, instructing the user to
|
||||
// confirm the change and providing a link for unexpected situations
|
||||
this.mailerService.send({
|
||||
to: requestedEmail,
|
||||
email: new LoginVerificationEmail(token)
|
||||
})
|
||||
|
||||
// A notification-only email message to the current address, alerting the user to the impending change and
|
||||
// providing a link for an unexpected situation.
|
||||
this.mailerService.send({
|
||||
to: user.email,
|
||||
email: new EmailChangeNoticeEmail()
|
||||
})
|
||||
}
|
||||
|
||||
async verify(userId: string, token: string) {
|
||||
const validRecord = await this.burnVerificationToken(userId, token)
|
||||
if (!validRecord) throw BadRequest('Invalid token');
|
||||
await this.usersRepository.update(userId, { email: validRecord.requestedEmail, verified: true });
|
||||
}
|
||||
|
||||
private async burnVerificationToken(userId: string, token: string) {
|
||||
return this.drizzleService.db.transaction(async (trx) => {
|
||||
// find a valid record
|
||||
const emailVerificationRecord = await this.emailVerificationsRepository.findValidRecord(userId, trx);
|
||||
if (!emailVerificationRecord) return null;
|
||||
|
||||
// check if the token is valid
|
||||
const isValidRecord = await this.tokensService.verifyHashedToken(emailVerificationRecord.hashedToken, token);
|
||||
if (!isValidRecord) return null
|
||||
|
||||
// burn the token if it is valid
|
||||
await this.emailVerificationsRepository.deleteById(emailVerificationRecord.id, trx)
|
||||
return emailVerificationRecord
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -2,14 +2,12 @@ import { inject, injectable } from 'tsyringe';
|
|||
import { MailerService } from './mailer.service';
|
||||
import { TokensService } from './tokens.service';
|
||||
import { UsersRepository } from '../repositories/users.repository';
|
||||
import type { SignInEmailDto } from '../dtos/signin-email.dto';
|
||||
import type { RegisterEmailDto } from '../dtos/register-email.dto';
|
||||
import type { VerifyLoginDto } from '../dtos/verify-login.dto';
|
||||
import type { LoginDto } from '../dtos/login.dto';
|
||||
import { LoginRequestsRepository } from '../repositories/login-requests.repository';
|
||||
import { LoginVerificationEmail } from '../emails/login-verification.email';
|
||||
import { BadRequest } from '../common/exceptions';
|
||||
import { WelcomeEmail } from '../emails/welcome.email';
|
||||
import { EmailVerificationsRepository } from '../repositories/email-verifications.repository';
|
||||
import { EmailChangeNoticeEmail } from '../emails/email-change-notice.email';
|
||||
import { DrizzleService } from './drizzle.service';
|
||||
import { LuciaService } from './lucia.service';
|
||||
|
||||
|
|
@ -22,10 +20,9 @@ export class IamService {
|
|||
@inject(MailerService) private readonly mailerService: MailerService,
|
||||
@inject(UsersRepository) private readonly usersRepository: UsersRepository,
|
||||
@inject(LoginRequestsRepository) private readonly loginRequestsRepository: LoginRequestsRepository,
|
||||
@inject(EmailVerificationsRepository) private readonly emailVerificationsRepository: EmailVerificationsRepository,
|
||||
) { }
|
||||
|
||||
async createLoginRequest(data: RegisterEmailDto) {
|
||||
async createLoginRequest(data: LoginDto) {
|
||||
// generate a token, expiry date, and hash
|
||||
const { token, expiry, hashedToken } = await this.tokensService.generateTokenWithExpiryAndHash(15, 'm');
|
||||
// save the login request to the database - ensuring we save the hashedToken
|
||||
|
|
@ -34,7 +31,7 @@ export class IamService {
|
|||
await this.mailerService.send({ email: new LoginVerificationEmail(token), to: data.email });
|
||||
}
|
||||
|
||||
async verifyLoginRequest(data: SignInEmailDto) {
|
||||
async verifyLoginRequest(data: VerifyLoginDto) {
|
||||
const validLoginRequest = await this.getValidLoginRequest(data.email, data.token);
|
||||
if (!validLoginRequest) throw BadRequest('Invalid token');
|
||||
|
||||
|
|
@ -48,37 +45,6 @@ export class IamService {
|
|||
return this.luciaService.lucia.createSession(existingUser.id, {});
|
||||
}
|
||||
|
||||
// These steps follow the process outlined in OWASP's "Changing A User's Email Address" guide.
|
||||
// https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#changing-a-users-registered-email-address
|
||||
async dispatchEmailVerificationRequest(userId: string, requestedEmail: string) {
|
||||
// generate a token and expiry
|
||||
const { token, expiry, hashedToken } = await this.tokensService.generateTokenWithExpiryAndHash(15, 'm')
|
||||
const user = await this.usersRepository.findOneByIdOrThrow(userId)
|
||||
|
||||
// create a new email verification record
|
||||
await this.emailVerificationsRepository.create({ requestedEmail, userId, hashedToken, expiresAt: expiry })
|
||||
|
||||
// A confirmation-required email message to the proposed new address, instructing the user to
|
||||
// confirm the change and providing a link for unexpected situations
|
||||
this.mailerService.send({
|
||||
to: requestedEmail,
|
||||
email: new LoginVerificationEmail(token)
|
||||
})
|
||||
|
||||
// A notification-only email message to the current address, alerting the user to the impending change and
|
||||
// providing a link for an unexpected situation.
|
||||
this.mailerService.send({
|
||||
to: user.email,
|
||||
email: new EmailChangeNoticeEmail()
|
||||
})
|
||||
}
|
||||
|
||||
async processEmailVerificationRequest(userId: string, token: string) {
|
||||
const validRecord = await this.findAndBurnEmailVerificationToken(userId, token)
|
||||
if (!validRecord) throw BadRequest('Invalid token');
|
||||
await this.usersRepository.update(userId, { email: validRecord.requestedEmail, verified: true });
|
||||
}
|
||||
|
||||
async logout(sessionId: string) {
|
||||
return this.luciaService.lucia.invalidateSession(sessionId);
|
||||
}
|
||||
|
|
@ -108,21 +74,7 @@ export class IamService {
|
|||
})
|
||||
}
|
||||
|
||||
private async findAndBurnEmailVerificationToken(userId: string, token: string) {
|
||||
return this.drizzleService.db.transaction(async (trx) => {
|
||||
// find a valid record
|
||||
const emailVerificationRecord = await this.emailVerificationsRepository.findValidRecord(userId, trx);
|
||||
if (!emailVerificationRecord) return null;
|
||||
|
||||
// check if the token is valid
|
||||
const isValidRecord = await this.tokensService.verifyHashedToken(emailVerificationRecord.hashedToken, token);
|
||||
if (!isValidRecord) return null
|
||||
|
||||
// burn the token if it is valid
|
||||
await this.emailVerificationsRepository.deleteById(emailVerificationRecord.id, trx)
|
||||
return emailVerificationRecord
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export const actions = {
|
|||
register: async ({ locals, request }) => {
|
||||
const emailRegisterForm = await superValidate(request, zod(registerFormSchema));
|
||||
if (!emailRegisterForm.valid) return fail(StatusCodes.BAD_REQUEST, { emailRegisterForm });
|
||||
const { error } = await locals.api.iam.login.request.$post({ json: emailRegisterForm.data }).then(locals.parseApiResponse);
|
||||
const { error } = await locals.api.iam.login.$post({ json: emailRegisterForm.data }).then(locals.parseApiResponse);
|
||||
if (error) return setError(emailRegisterForm, 'email', error);
|
||||
return { emailRegisterForm };
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue