Skip to content

Commit

Permalink
feat: can send email & validate token
Browse files Browse the repository at this point in the history
  • Loading branch information
max-lt committed May 31, 2024
1 parent 140f69c commit 07cf995
Show file tree
Hide file tree
Showing 43 changed files with 674 additions and 40 deletions.
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
**/node_modules

.turbo
.vite
1 change: 0 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,5 @@
},
"search.exclude": {
"**/tsconfig.*.tsbuildinfo": true
},
"cSpell.words": ["bytea", "zoomable"]
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Once updated, to reflect it in your database, run the following commands :
1. `cd packages/database`
2. `pnpm run drizzle:generate`

This will add a new migration script, then you hav to run it :
This will add a new migration script, then you have to run it :

`pnpm run db:migrate`

Expand Down
53 changes: 53 additions & 0 deletions apps/api/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { EnvConfig } from '@sovereign-university/types';

const getenv = <T extends string | number | boolean | null = string>(
name: string,
fallback?: T,
): T => {
const value = process.env[name] ?? '';

// If the value is empty and no fallback is provided, throw an error
if (!value && fallback === undefined) {
throw new Error(`Missing mandatory value for "${name}"`);
}

// If the value is empty and a fallback is provided, log a warning
if (!value && fallback !== null) {
console.warn(
`No value found for "${name}"${fallback !== null ? `, defaulting to '${JSON.stringify(fallback)}'` : '!'}`,
);
}

// If the value is not empty, parse it to the correct type (inferred from fallback type)
if (fallback !== null) {
switch (typeof fallback) {
case 'boolean':
return (value ? value === 'true' : fallback) as T;
case 'number':
return (parseInt(value) || fallback) as T;
}
}

return value as T;
};

/**
* Real application domain (without trailing slash)
*/
export const domain = getenv<string>('DOMAIN', 'http://localhost:8181');

export const sendgrid: EnvConfig['sendgrid'] = {
key: getenv<string | null>('SENDGRID_KEY', null),
enable: getenv<boolean>('SENDGRID_ENABLE', false),
email: getenv<string | null>('SENDGRID_EMAIL', null),
templates: {
emailChange: getenv<string | null>(
'SENDGRID_EMAIL_CHANGE_TEMPLATE_ID',
null,
),
recoverPassword: getenv<string | null>(
'SENDGRID_RECOVER_PASSWORD_TEMPLATE_ID',
null,
),
},
};
9 changes: 6 additions & 3 deletions apps/api/src/dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { EventEmitter } from 'eventemitter3';
import { createPostgresClient } from '@sovereign-university/database';
import type { PostgresClient } from '@sovereign-university/database';
import { RedisClient } from '@sovereign-university/redis';
import type { ApiEvents } from '@sovereign-university/types';
import type { ApiEvents, EnvConfig } from '@sovereign-university/types';
import * as config from './config.js';

export interface Dependencies {
redis: RedisClient;
postgres: PostgresClient;
events: EventEmitter<ApiEvents>;
config: EnvConfig;
}

export const startDependencies = async () => {
Expand All @@ -30,11 +32,12 @@ export const startDependencies = async () => {

await postgres.connect();

const dependencies = {
const dependencies: Dependencies = {
redis,
postgres,
events,
} as Dependencies;
config,
};

const stopDependencies = async () => {
await postgres.disconnect();
Expand Down
18 changes: 18 additions & 0 deletions apps/api/src/routers/user/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { z } from 'zod';
import {
createChangePassword,
createGetUserDetails,
createChangeEmailConfirmation,
createEmailValidationToken,
createPasswordRecoveryToken,
} from '@sovereign-university/user';

import { protectedProcedure, publicProcedure } from '../../procedures/index.js';
Expand Down Expand Up @@ -46,4 +49,19 @@ export const userRouter = createTRPCRouter({
courses: userCoursesRouter,
events: userEventsRouter,
webhooks: paymentWebhooksProcedure,
changeEmail: protectedProcedure
.input(z.object({ email: z.string().email() }))
.mutation(({ ctx, input }) =>
createEmailValidationToken(ctx.dependencies)(ctx.user.uid, input.email),
),
validateEmailChange: publicProcedure
.input(z.object({ token: z.string() }))
.mutation(async ({ ctx, input }) =>
createChangeEmailConfirmation(ctx.dependencies)(input.token),
),
requestPasswordRecovery: publicProcedure
.input(z.object({ email: z.string().email() }))
.mutation(async ({ ctx, input }) =>
createPasswordRecoveryToken(ctx.dependencies)(input.email),
),
});
3 changes: 2 additions & 1 deletion apps/web/public/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,8 @@
"github": ""
},
"settings": {
"changePassword": "Change password"
"changePassword": "Change password",
"changeEmail": "Change email"
},
"tutorials": {
"details": {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/public/locales/da.json
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,8 @@
"github": ""
},
"settings": {
"changePassword": "Change password"
"changePassword": "Change password",
"changeEmail": "Change email"
},
"tutorials": {
"details": {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/public/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,8 @@
"github": ""
},
"settings": {
"changePassword": "Passwort ändern"
"changePassword": "Passwort ändern",
"changeEmail": "Change email"
},
"tutorials": {
"details": {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,8 @@
},
"settings": {
"changePassword": "Change password",
"changeProfilePicture": "Change profile picture"
"changeProfilePicture": "Change profile picture",
"changeEmail": "Change email"
},
"tutorials": {
"details": {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/public/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,8 @@
"github": ""
},
"settings": {
"changePassword": "Cambiar contraseña"
"changePassword": "Cambiar contraseña",
"changeEmail": "Change email"
},
"tutorials": {
"details": {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/public/locales/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,8 @@
"github": ""
},
"settings": {
"changePassword": "Change password"
"changePassword": "Change password",
"changeEmail": "Change email"
},
"tutorials": {
"details": {
Expand Down
4 changes: 4 additions & 0 deletions apps/web/public/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,10 @@
"subtitle": "",
"title": ""
},
"settings": {
"changePassword": "Changer le mot de passe",
"changeEmail": "Changer l'email"
},
"pageDescription": "",
"pageSubtitle": "",
"pageTitle": "",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/public/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,8 @@
"github": ""
},
"settings": {
"changePassword": "パスワードを変更する"
"changePassword": "パスワードを変更する",
"changeEmail": "Change email"
},
"tutorials": {
"details": {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/public/locales/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,8 @@
"github": ""
},
"settings": {
"changePassword": "Change password"
"changePassword": "Change password",
"changeEmail": "Change email"
},
"tutorials": {
"details": {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/public/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,8 @@
"github": ""
},
"settings": {
"changePassword": "Alterar palavra-passe"
"changePassword": "Alterar palavra-passe",
"changeEmail": "Alterar e-mail"
},
"tutorials": {
"details": {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/public/locales/sv.json
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,8 @@
"github": ""
},
"settings": {
"changePassword": "Change password"
"changePassword": "Change password",
"changeEmail": "Change email"
},
"tutorials": {
"details": {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/public/locales/sw.json
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,8 @@
"github": ""
},
"settings": {
"changePassword": "Change password"
"changePassword": "Change password",
"changeEmail": "Change email"
},
"tutorials": {
"details": {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/public/locales/th.json
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,8 @@
"github": ""
},
"settings": {
"changePassword": "Change password"
"changePassword": "Change password",
"changeEmail": "Change email"
},
"tutorials": {
"details": {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/public/locales/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,8 @@
"github": "Tất cả dữ liệu này được quản lý thông qua một kho mã nguồn mở trên <0>Github</0>. Bạn có thể thoải mái đóng góp và bổ sung bất kỳ nội dung còn thiếu nào."
},
"settings": {
"changePassword": "Thay đổi mật khẩu"
"changePassword": "Thay đổi mật khẩu",
"changeEmail": "Thay đổi email"
},
"tutorials": {
"details": {
Expand Down
114 changes: 114 additions & 0 deletions apps/web/src/features/dashboard/components/change-email-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type { FormikHelpers } from 'formik';
import { Formik } from 'formik';
import { t } from 'i18next';
import { isEmpty } from 'lodash-es';
import { useCallback } from 'react';
import { ZodError, z } from 'zod';

import { Button } from '@sovereign-university/ui';

import { Modal } from '../../../atoms/Modal/index.tsx';
import { TextInput } from '../../../atoms/TextInput/index.tsx';
import { trpc } from '../../../utils/index.ts';

const changeEmailSchema = z.object({
email: z.string(),
});

interface ChangePasswordModalProps {
isOpen: boolean;
onClose: () => void;
email: string;
onEmailSent: () => void;
}

type ChangeEmailForm = z.infer<typeof changeEmailSchema>;

export const ChangeEmailModal = ({
isOpen,
onClose,
email,
onEmailSent,
}: ChangePasswordModalProps) => {
const changeEmail = trpc.user.changeEmail.useMutation({
onSuccess: () => {
onClose();
onEmailSent();
},
});

const handleChangeEmail = useCallback(
async (form: ChangeEmailForm, actions: FormikHelpers<ChangeEmailForm>) => {
const errors = await actions.validateForm();
if (!isEmpty(errors)) return;

changeEmail.mutate(form);
},
[changeEmail],
);

return (
<Modal
isOpen={isOpen}
onClose={onClose}
headerText={t('settings.changeEmail')}
>
<div className="flex flex-col items-center">
<Formik
initialValues={{ email }}
validate={(values) => {
try {
changeEmailSchema.parse(values);
} catch (error) {
if (error instanceof ZodError) {
return error.flatten().fieldErrors;
}
}
}}
onSubmit={handleChangeEmail}
>
{({
handleSubmit,
handleChange,
handleBlur,
values,
errors,
touched,
}) => (
<form
onSubmit={(event) => {
event.preventDefault();
handleSubmit();
}}
className="flex w-full flex-col items-center pb-6"
>
<div className="flex w-full flex-col">
<TextInput
name="email"
type="email"
autoComplete="email"
labelText="Email"
onChange={handleChange}
onBlur={handleBlur}
value={values.email}
className="w-80"
error={touched.email ? errors.email : null}
/>
</div>

{changeEmail.error && (
<p className="mt-2 text-base font-semibold text-red-300">
{changeEmail.error.message}
</p>
)}

<Button className="mt-6" rounded type="submit">
{t('words.update')}
</Button>
</form>
)}
</Formik>
</div>
</Modal>
);
};
Loading

0 comments on commit 07cf995

Please sign in to comment.