mirror of
https://github.com/BradNut/AdelieStack
synced 2025-09-08 17:40:20 +00:00
Fixing the settings change email flow.
This commit is contained in:
parent
224380d9e2
commit
fed1bfb524
12 changed files with 67 additions and 60 deletions
8
.run/AdelieStack.xml
Normal file
8
.run/AdelieStack.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="AdelieStack" type="NodeJSConfigurationType" path-to-js-file="node_modules/vite/bin/vite.js" typescript-loader="bundled" working-dir="$PROJECT_DIR$">
|
||||
<envs>
|
||||
<env name="NODE_ENV" value="development" />
|
||||
</envs>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const verifyEmailDto = z.object({
|
||||
token: z.string()
|
||||
code: z.string().length(6),
|
||||
});
|
||||
|
||||
export type VerifyEmailDto = z.infer<typeof verifyEmailDto>;
|
||||
|
|
@ -13,7 +13,9 @@ type UnauthedReturnType = typeof unauthed;
|
|||
export function authState(state: 'session'): AuthedReturnType;
|
||||
export function authState(state: 'none'): UnauthedReturnType;
|
||||
export function authState(state: AuthStates): AuthedReturnType | UnauthedReturnType {
|
||||
if (state === 'session') return authed;
|
||||
if (state === 'session') {
|
||||
return authed;
|
||||
}
|
||||
return unauthed;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export class IamController extends Controller {
|
|||
routes() {
|
||||
return this.controller
|
||||
.post('/login', openApi(signInEmail), authState('none'), zValidator('json', signinDto), rateLimit({ limit: 3, minutes: 1 }), async (c) => {
|
||||
this.loggerService.log.info(`Login with identifier: ${c.req.valid('json').identifier}`);
|
||||
const session = await this.loginRequestsService.login(c.req.valid('json'));
|
||||
await this.sessionsService.setSessionCookie(session);
|
||||
return c.json({ message: 'welcome' });
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
import { z } from 'zod';
|
||||
import { userDto } from './user.dto';
|
||||
|
||||
export const updateEmailDto = userDto.pick({ email: true });
|
||||
|
||||
export type UpdateEmailDto = z.infer<typeof updateEmailDto>;
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const verifyEmailDto = z.object({
|
||||
code: z.string().length(6)
|
||||
});
|
||||
|
||||
export type VerifyEmailDto = z.infer<typeof verifyEmailDto>;
|
||||
|
|
@ -6,11 +6,13 @@ import { EmailChangeNoticeEmail } from '../../mail/templates/email-change-notice
|
|||
import { BadRequest } from '../../common/utils/exceptions';
|
||||
import { UsersRepository } from '../users.repository';
|
||||
import { VerificationCodesService } from '../../common/services/verification-codes.service';
|
||||
import { LoggerService } from '../../common/services/logger.service';
|
||||
|
||||
@injectable()
|
||||
export class EmailChangeRequestsService {
|
||||
constructor(
|
||||
private emailChangeRequetsRepository = inject(EmailChangeRequestsRepository),
|
||||
private loggerService = inject(LoggerService),
|
||||
private verificationCodesService = inject(VerificationCodesService),
|
||||
private mailerService = inject(MailerService),
|
||||
private usersRepository = inject(UsersRepository)
|
||||
|
|
@ -44,19 +46,24 @@ export class EmailChangeRequestsService {
|
|||
to: requestedEmail,
|
||||
template: new EmailChangeRequestEmail(verificationCode)
|
||||
});
|
||||
this.loggerService.log.info(`Email change request sent to ${requestedEmail}`);
|
||||
}
|
||||
|
||||
async verifyEmailChange(userId: string, verificationCode: string) {
|
||||
// Get the email change request
|
||||
const emailChangeRequest = await this.emailChangeRequetsRepository.get(userId);
|
||||
if (!emailChangeRequest) throw BadRequest('Bad Request');
|
||||
if (!emailChangeRequest) {
|
||||
throw BadRequest('Bad Request');
|
||||
}
|
||||
|
||||
// Verify the verification code
|
||||
const isValid = await this.verificationCodesService.verify({
|
||||
verificationCode,
|
||||
hashedVerificationCode: emailChangeRequest.hashedCode
|
||||
});
|
||||
if (!isValid) throw BadRequest('Bad Request');
|
||||
if (!isValid) {
|
||||
throw BadRequest('Bad Request');
|
||||
}
|
||||
|
||||
// Update the account's email
|
||||
await this.usersRepository.update(userId, {
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import { authState } from '../common/middleware/auth.middleware';
|
|||
import { zValidator } from '@hono/zod-validator';
|
||||
import { updateUserDto } from './dtos/update-user.dto';
|
||||
import { EmailChangeRequestsService } from './email-change-requests/email-change-requests.service';
|
||||
import { updateEmailDto } from './dtos/update-email.dto';
|
||||
import { updateEmailDto } from '$lib/dtos/settings/update-email.dto';
|
||||
import { UsersRepository } from './users.repository';
|
||||
import { verifyEmailDto } from './dtos/verify-email.dto';
|
||||
import { verifyEmailDto } from '$lib/dtos/settings/verify-email.dto';
|
||||
import { rateLimit } from '../common/middleware/rate-limit.middleware';
|
||||
|
||||
@injectable()
|
||||
|
|
@ -39,6 +39,7 @@ export class UsersController extends Controller {
|
|||
zValidator('json', updateEmailDto),
|
||||
rateLimit({ limit: 5, minutes: 15 }),
|
||||
async (c) => {
|
||||
c.var.logger.info(`Request email change: ${c.req.valid('json').email}`);
|
||||
await this.emailChangeRequestsService.requestEmailChange(
|
||||
c.var.session.userId,
|
||||
c.req.valid('json').email
|
||||
|
|
|
|||
|
|
@ -1,32 +1,47 @@
|
|||
import { zod } from "sveltekit-superforms/adapters";
|
||||
import { fail, setError, superValidate } from "sveltekit-superforms";
|
||||
import { StatusCodes } from "@/constants/status-codes.js";
|
||||
import { updateEmailFormSchema, verifyEmailFormSchema } from "./schemas.js";
|
||||
import { updateEmailDto } from "$lib/dtos/settings/update-email.dto.js";
|
||||
import { verifyEmailDto } from "$lib/dtos/settings/verify-email.dto.js";
|
||||
import { redirect } from "sveltekit-flash-message/server";
|
||||
import { notSignedInMessage } from "$lib/utils/flashMessages.js";
|
||||
|
||||
export let load = async (event) => {
|
||||
const authedUser = await event.locals.getAuthedUserOrThrow()
|
||||
export const load = async (event) => {
|
||||
const { parent } = event;
|
||||
const { authedUser } = await parent();
|
||||
|
||||
if (!authedUser) {
|
||||
throw redirect('/login', notSignedInMessage, event);
|
||||
}
|
||||
|
||||
return {
|
||||
authedUser,
|
||||
updateEmailForm: await superValidate(authedUser, zod(updateEmailFormSchema)),
|
||||
verifyEmailForm: await superValidate(zod(verifyEmailFormSchema))
|
||||
updateEmailForm: await superValidate(zod(updateEmailDto), {
|
||||
defaults: {
|
||||
email: authedUser.email
|
||||
}
|
||||
}),
|
||||
verifyEmailForm: await superValidate(zod(verifyEmailDto))
|
||||
};
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
updateEmail: async ({ request, locals }) => {
|
||||
const updateEmailForm = await superValidate(request, zod(updateEmailFormSchema));
|
||||
const updateEmailForm = await superValidate(request, zod(updateEmailDto));
|
||||
if (!updateEmailForm.valid) return fail(StatusCodes.BAD_REQUEST, { updateEmailForm })
|
||||
const { error } = await locals.api.iam.email.$patch({ json: updateEmailForm.data }).then(locals.parseApiResponse);
|
||||
const { error } = await locals.api.users.me.email.request.$post({ json: updateEmailForm.data }).then(locals.parseApiResponse);
|
||||
if (error) return setError(updateEmailForm, 'email', error);
|
||||
return { updateEmailForm }
|
||||
},
|
||||
verifyEmail: async ({ request, locals }) => {
|
||||
const verifyEmailForm = await superValidate(request, zod(verifyEmailFormSchema));
|
||||
const verifyEmailForm = await superValidate(request, zod(verifyEmailDto));
|
||||
console.log(verifyEmailForm)
|
||||
if (!verifyEmailForm.valid) return fail(StatusCodes.BAD_REQUEST, { verifyEmailForm })
|
||||
const { error } = await locals.api.iam.email.verify.$post({ json: verifyEmailForm.data }).then(locals.parseApiResponse);
|
||||
if (error) return setError(verifyEmailForm, 'token', error);
|
||||
if (!verifyEmailForm.valid) {
|
||||
return fail(StatusCodes.BAD_REQUEST, { verifyEmailForm });
|
||||
}
|
||||
const { error } = await locals.api.users.me.email.verify.$post({ json: verifyEmailForm.data }).then(locals.parseApiResponse);
|
||||
if (error) {
|
||||
return setError(verifyEmailForm, 'code', error);
|
||||
}
|
||||
return { verifyEmailForm }
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const verifyEmailFormSchema = z.object({
|
||||
token: z.string()
|
||||
});
|
||||
|
||||
export const updateEmailFormSchema = z.object({
|
||||
email: z.string().email()
|
||||
});
|
||||
|
|
@ -1,12 +1,3 @@
|
|||
<script module lang="ts">
|
||||
import type { Infer, SuperValidated } from 'sveltekit-superforms';
|
||||
|
||||
interface UpdateEmailCardProps {
|
||||
updateEmailForm: SuperValidated<Infer<typeof updateEmailDto>>;
|
||||
verifyEmailForm: SuperValidated<Infer<typeof verifyEmailDto>>;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
|
|
@ -15,28 +6,32 @@ interface UpdateEmailCardProps {
|
|||
import { superForm } from 'sveltekit-superforms';
|
||||
import * as Dialog from '@/components/ui/dialog';
|
||||
import * as InputOTP from '$lib/components/ui/input-otp/index.js';
|
||||
import type { updateEmailDto } from '$lib/dtos/update-email.dto';
|
||||
import type { verifyEmailDto } from '$lib/dtos/verify-email.dto';
|
||||
import type { UpdateEmailDto } from '$lib/dtos/settings/update-email.dto';
|
||||
import type { VerifyEmailDto } from '$lib/dtos/settings/verify-email.dto';
|
||||
|
||||
/* ---------------------------------- props --------------------------------- */
|
||||
let { updateEmailForm, verifyEmailForm }: UpdateEmailCardProps = $props();
|
||||
let { updateEmailForm, verifyEmailForm }: { updateEmailForm: UpdateEmailDto; verifyEmailForm: VerifyEmailDto } = $props();
|
||||
|
||||
/* ---------------------------------- state --------------------------------- */
|
||||
let verifyTokenDialogOpen = $state(false);
|
||||
let verifyDialogOpen = $state(false);
|
||||
|
||||
/* ---------------------------------- forms --------------------------------- */
|
||||
const sf_updateEmailForm = superForm(updateEmailForm, {
|
||||
resetForm: false,
|
||||
onUpdated: ({ form }) => {
|
||||
if (!form.valid) return;
|
||||
verifyTokenDialogOpen = true;
|
||||
if (!form.valid) {
|
||||
return;
|
||||
}
|
||||
verifyDialogOpen = true;
|
||||
}
|
||||
});
|
||||
|
||||
const sf_verifyEmailForm = superForm(verifyEmailForm, {
|
||||
onUpdated: ({ form }) => {
|
||||
if (!form.valid) return;
|
||||
verifyTokenDialogOpen = false;
|
||||
if (!form.valid) {
|
||||
return;
|
||||
}
|
||||
verifyDialogOpen = false;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -80,7 +75,7 @@ interface UpdateEmailCardProps {
|
|||
</Card.Root>
|
||||
|
||||
<!-- Dialogs -->
|
||||
<Dialog.Root bind:open={verifyTokenDialogOpen}>
|
||||
<Dialog.Root bind:open={verifyDialogOpen}>
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Verify Email</Dialog.Title>
|
||||
|
|
@ -90,10 +85,10 @@ interface UpdateEmailCardProps {
|
|||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<form method="POST" action="?/verifyEmail" use:verifyEmailFormEnhance>
|
||||
<Form.Field form={sf_verifyEmailForm} name="token">
|
||||
<Form.Field form={sf_verifyEmailForm} name="code">
|
||||
<Form.Control>
|
||||
{#snippet children({ props })}
|
||||
<InputOTP.Root maxlength={6} {...props} bind:value={$verifyEmailFormData.token}>
|
||||
<InputOTP.Root maxlength={6} {...props} bind:value={$verifyEmailFormData.code}>
|
||||
{#snippet children({ cells })}
|
||||
<InputOTP.Group>
|
||||
{#each cells as cell}
|
||||
|
|
|
|||
Loading…
Reference in a new issue