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';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const verifyEmailDto = z.object({
|
export const verifyEmailDto = z.object({
|
||||||
token: z.string()
|
code: z.string().length(6),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type VerifyEmailDto = z.infer<typeof verifyEmailDto>;
|
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: 'session'): AuthedReturnType;
|
||||||
export function authState(state: 'none'): UnauthedReturnType;
|
export function authState(state: 'none'): UnauthedReturnType;
|
||||||
export function authState(state: AuthStates): AuthedReturnType | UnauthedReturnType {
|
export function authState(state: AuthStates): AuthedReturnType | UnauthedReturnType {
|
||||||
if (state === 'session') return authed;
|
if (state === 'session') {
|
||||||
|
return authed;
|
||||||
|
}
|
||||||
return unauthed;
|
return unauthed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export class IamController extends Controller {
|
||||||
routes() {
|
routes() {
|
||||||
return this.controller
|
return this.controller
|
||||||
.post('/login', openApi(signInEmail), authState('none'), zValidator('json', signinDto), rateLimit({ limit: 3, minutes: 1 }), async (c) => {
|
.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'));
|
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,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 { BadRequest } from '../../common/utils/exceptions';
|
||||||
import { UsersRepository } from '../users.repository';
|
import { UsersRepository } from '../users.repository';
|
||||||
import { VerificationCodesService } from '../../common/services/verification-codes.service';
|
import { VerificationCodesService } from '../../common/services/verification-codes.service';
|
||||||
|
import { LoggerService } from '../../common/services/logger.service';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class EmailChangeRequestsService {
|
export class EmailChangeRequestsService {
|
||||||
constructor(
|
constructor(
|
||||||
private emailChangeRequetsRepository = inject(EmailChangeRequestsRepository),
|
private emailChangeRequetsRepository = inject(EmailChangeRequestsRepository),
|
||||||
|
private loggerService = inject(LoggerService),
|
||||||
private verificationCodesService = inject(VerificationCodesService),
|
private verificationCodesService = inject(VerificationCodesService),
|
||||||
private mailerService = inject(MailerService),
|
private mailerService = inject(MailerService),
|
||||||
private usersRepository = inject(UsersRepository)
|
private usersRepository = inject(UsersRepository)
|
||||||
|
|
@ -44,19 +46,24 @@ export class EmailChangeRequestsService {
|
||||||
to: requestedEmail,
|
to: requestedEmail,
|
||||||
template: new EmailChangeRequestEmail(verificationCode)
|
template: new EmailChangeRequestEmail(verificationCode)
|
||||||
});
|
});
|
||||||
|
this.loggerService.log.info(`Email change request sent to ${requestedEmail}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyEmailChange(userId: string, verificationCode: string) {
|
async verifyEmailChange(userId: string, verificationCode: string) {
|
||||||
// Get the email change request
|
// Get the email change request
|
||||||
const emailChangeRequest = await this.emailChangeRequetsRepository.get(userId);
|
const emailChangeRequest = await this.emailChangeRequetsRepository.get(userId);
|
||||||
if (!emailChangeRequest) throw BadRequest('Bad Request');
|
if (!emailChangeRequest) {
|
||||||
|
throw BadRequest('Bad Request');
|
||||||
|
}
|
||||||
|
|
||||||
// Verify the verification code
|
// Verify the verification code
|
||||||
const isValid = await this.verificationCodesService.verify({
|
const isValid = await this.verificationCodesService.verify({
|
||||||
verificationCode,
|
verificationCode,
|
||||||
hashedVerificationCode: emailChangeRequest.hashedCode
|
hashedVerificationCode: emailChangeRequest.hashedCode
|
||||||
});
|
});
|
||||||
if (!isValid) throw BadRequest('Bad Request');
|
if (!isValid) {
|
||||||
|
throw BadRequest('Bad Request');
|
||||||
|
}
|
||||||
|
|
||||||
// Update the account's email
|
// Update the account's email
|
||||||
await this.usersRepository.update(userId, {
|
await this.usersRepository.update(userId, {
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ import { authState } from '../common/middleware/auth.middleware';
|
||||||
import { zValidator } from '@hono/zod-validator';
|
import { zValidator } from '@hono/zod-validator';
|
||||||
import { updateUserDto } from './dtos/update-user.dto';
|
import { updateUserDto } from './dtos/update-user.dto';
|
||||||
import { EmailChangeRequestsService } from './email-change-requests/email-change-requests.service';
|
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 { 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';
|
import { rateLimit } from '../common/middleware/rate-limit.middleware';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
|
|
@ -39,6 +39,7 @@ export class UsersController extends Controller {
|
||||||
zValidator('json', updateEmailDto),
|
zValidator('json', updateEmailDto),
|
||||||
rateLimit({ limit: 5, minutes: 15 }),
|
rateLimit({ limit: 5, minutes: 15 }),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
|
c.var.logger.info(`Request email change: ${c.req.valid('json').email}`);
|
||||||
await this.emailChangeRequestsService.requestEmailChange(
|
await this.emailChangeRequestsService.requestEmailChange(
|
||||||
c.var.session.userId,
|
c.var.session.userId,
|
||||||
c.req.valid('json').email
|
c.req.valid('json').email
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,47 @@
|
||||||
import { zod } from "sveltekit-superforms/adapters";
|
import { zod } from "sveltekit-superforms/adapters";
|
||||||
import { fail, setError, superValidate } from "sveltekit-superforms";
|
import { fail, setError, superValidate } from "sveltekit-superforms";
|
||||||
import { StatusCodes } from "@/constants/status-codes.js";
|
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) => {
|
export const load = async (event) => {
|
||||||
const authedUser = await event.locals.getAuthedUserOrThrow()
|
const { parent } = event;
|
||||||
|
const { authedUser } = await parent();
|
||||||
|
|
||||||
|
if (!authedUser) {
|
||||||
|
throw redirect('/login', notSignedInMessage, event);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authedUser,
|
updateEmailForm: await superValidate(zod(updateEmailDto), {
|
||||||
updateEmailForm: await superValidate(authedUser, zod(updateEmailFormSchema)),
|
defaults: {
|
||||||
verifyEmailForm: await superValidate(zod(verifyEmailFormSchema))
|
email: authedUser.email
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
verifyEmailForm: await superValidate(zod(verifyEmailDto))
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
updateEmail: async ({ request, locals }) => {
|
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 })
|
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);
|
if (error) return setError(updateEmailForm, 'email', error);
|
||||||
return { updateEmailForm }
|
return { updateEmailForm }
|
||||||
},
|
},
|
||||||
verifyEmail: async ({ request, locals }) => {
|
verifyEmail: async ({ request, locals }) => {
|
||||||
const verifyEmailForm = await superValidate(request, zod(verifyEmailFormSchema));
|
const verifyEmailForm = await superValidate(request, zod(verifyEmailDto));
|
||||||
console.log(verifyEmailForm)
|
console.log(verifyEmailForm)
|
||||||
if (!verifyEmailForm.valid) return fail(StatusCodes.BAD_REQUEST, { verifyEmailForm })
|
if (!verifyEmailForm.valid) {
|
||||||
const { error } = await locals.api.iam.email.verify.$post({ json: verifyEmailForm.data }).then(locals.parseApiResponse);
|
return fail(StatusCodes.BAD_REQUEST, { verifyEmailForm });
|
||||||
if (error) return setError(verifyEmailForm, 'token', error);
|
}
|
||||||
|
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 }
|
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">
|
<script lang="ts">
|
||||||
import { Button } from '$lib/components/ui/button/index.js';
|
import { Button } from '$lib/components/ui/button/index.js';
|
||||||
import * as Card from '$lib/components/ui/card/index.js';
|
import * as Card from '$lib/components/ui/card/index.js';
|
||||||
|
|
@ -15,28 +6,32 @@ interface UpdateEmailCardProps {
|
||||||
import { superForm } from 'sveltekit-superforms';
|
import { superForm } from 'sveltekit-superforms';
|
||||||
import * as Dialog from '@/components/ui/dialog';
|
import * as Dialog from '@/components/ui/dialog';
|
||||||
import * as InputOTP from '$lib/components/ui/input-otp/index.js';
|
import * as InputOTP from '$lib/components/ui/input-otp/index.js';
|
||||||
import type { updateEmailDto } from '$lib/dtos/update-email.dto';
|
import type { UpdateEmailDto } from '$lib/dtos/settings/update-email.dto';
|
||||||
import type { verifyEmailDto } from '$lib/dtos/verify-email.dto';
|
import type { VerifyEmailDto } from '$lib/dtos/settings/verify-email.dto';
|
||||||
|
|
||||||
/* ---------------------------------- props --------------------------------- */
|
/* ---------------------------------- props --------------------------------- */
|
||||||
let { updateEmailForm, verifyEmailForm }: UpdateEmailCardProps = $props();
|
let { updateEmailForm, verifyEmailForm }: { updateEmailForm: UpdateEmailDto; verifyEmailForm: VerifyEmailDto } = $props();
|
||||||
|
|
||||||
/* ---------------------------------- state --------------------------------- */
|
/* ---------------------------------- state --------------------------------- */
|
||||||
let verifyTokenDialogOpen = $state(false);
|
let verifyDialogOpen = $state(false);
|
||||||
|
|
||||||
/* ---------------------------------- forms --------------------------------- */
|
/* ---------------------------------- forms --------------------------------- */
|
||||||
const sf_updateEmailForm = superForm(updateEmailForm, {
|
const sf_updateEmailForm = superForm(updateEmailForm, {
|
||||||
resetForm: false,
|
resetForm: false,
|
||||||
onUpdated: ({ form }) => {
|
onUpdated: ({ form }) => {
|
||||||
if (!form.valid) return;
|
if (!form.valid) {
|
||||||
verifyTokenDialogOpen = true;
|
return;
|
||||||
|
}
|
||||||
|
verifyDialogOpen = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const sf_verifyEmailForm = superForm(verifyEmailForm, {
|
const sf_verifyEmailForm = superForm(verifyEmailForm, {
|
||||||
onUpdated: ({ form }) => {
|
onUpdated: ({ form }) => {
|
||||||
if (!form.valid) return;
|
if (!form.valid) {
|
||||||
verifyTokenDialogOpen = false;
|
return;
|
||||||
|
}
|
||||||
|
verifyDialogOpen = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -80,7 +75,7 @@ interface UpdateEmailCardProps {
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|
||||||
<!-- Dialogs -->
|
<!-- Dialogs -->
|
||||||
<Dialog.Root bind:open={verifyTokenDialogOpen}>
|
<Dialog.Root bind:open={verifyDialogOpen}>
|
||||||
<Dialog.Content>
|
<Dialog.Content>
|
||||||
<Dialog.Header>
|
<Dialog.Header>
|
||||||
<Dialog.Title>Verify Email</Dialog.Title>
|
<Dialog.Title>Verify Email</Dialog.Title>
|
||||||
|
|
@ -90,10 +85,10 @@ interface UpdateEmailCardProps {
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
</Dialog.Header>
|
</Dialog.Header>
|
||||||
<form method="POST" action="?/verifyEmail" use:verifyEmailFormEnhance>
|
<form method="POST" action="?/verifyEmail" use:verifyEmailFormEnhance>
|
||||||
<Form.Field form={sf_verifyEmailForm} name="token">
|
<Form.Field form={sf_verifyEmailForm} name="code">
|
||||||
<Form.Control>
|
<Form.Control>
|
||||||
{#snippet children({ props })}
|
{#snippet children({ props })}
|
||||||
<InputOTP.Root maxlength={6} {...props} bind:value={$verifyEmailFormData.token}>
|
<InputOTP.Root maxlength={6} {...props} bind:value={$verifyEmailFormData.code}>
|
||||||
{#snippet children({ cells })}
|
{#snippet children({ cells })}
|
||||||
<InputOTP.Group>
|
<InputOTP.Group>
|
||||||
{#each cells as cell}
|
{#each cells as cell}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue