Skip to content

Commit 63009b2

Browse files
committed
Merge branch 'dev' into main
2 parents d23a74f + 49ea6e3 commit 63009b2

6 files changed

Lines changed: 146 additions & 25 deletions

File tree

apps/app-server/src/fastify/server.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,9 @@ export const fastify = once(async () => {
6464
});
6565

6666
await fastify.register(import('@fastify/rate-limit'), {
67-
max: 5 * 60,
68-
timeWindow: 5 * 1000,
67+
// Accept up to 5 requests/second within 5 minutes
68+
max: 5 * 60 * 5,
69+
timeWindow: 5 * 60 * 1000,
6970

7071
redis: getRedis(),
7172
});

apps/app-server/src/trpc/api/sessions/login.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import { getUserDevice } from 'src/utils/devices';
2828
import { generateSessionValues } from 'src/utils/sessions';
2929
import { z } from 'zod';
3030

31+
import { sendRegistrationEmail } from '../users/account/register';
32+
3133
const baseProcedure = publicProcedure.input(
3234
z.object({
3335
email: z
@@ -90,6 +92,8 @@ export async function login({
9092
'users.id',
9193

9294
'users.email_verified',
95+
'users.email_verification_code',
96+
9397
'users.encrypted_rehashed_login_hash',
9498

9599
'users.public_keyring',
@@ -148,6 +152,20 @@ export async function login({
148152
});
149153
}
150154

155+
// Check if email is verified
156+
157+
if (!user.email_verified) {
158+
await sendRegistrationEmail({
159+
email: input.email,
160+
emailVerificationCode: user.email_verification_code,
161+
});
162+
163+
throw new TRPCError({
164+
message: 'Email awaiting verification. New email sent.',
165+
code: 'UNAUTHORIZED',
166+
});
167+
}
168+
151169
// Get user device
152170

153171
const device = await getUserDevice({

apps/app-server/src/trpc/api/users/account/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import { trpc } from 'src/trpc/server';
33
import { deleteProcedure } from './delete';
44
import { emailChangeRouter } from './email-change';
55
import { registerProcedure } from './register';
6+
import { resendVerificationEmailProcedure } from './resend-verification-email';
67
import { stripeRouter } from './stripe';
78
import { twoFactorAuthRouter } from './two-factor-auth';
89
import { verifyEmailProcedure } from './verify-email';
910

1011
export const accountRouter = trpc.router({
1112
register: registerProcedure(),
13+
resendVerificationEmail: resendVerificationEmailProcedure(),
1214
verifyEmail: verifyEmailProcedure(),
1315

1416
emailChange: emailChangeRouter,

apps/app-server/src/trpc/api/users/account/register.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export async function register({
3838
.where('email_verified', true)
3939
.orWhere('email_verification_expiration_date', '>', new Date()),
4040
)
41-
.select('email_verified')
41+
.select('email_verified', 'email_verification_code')
4242
.first();
4343

4444
// Check if user already exists
@@ -52,9 +52,14 @@ export async function register({
5252
code: 'CONFLICT',
5353
});
5454
} else {
55+
await sendRegistrationEmail({
56+
email: input.email,
57+
emailVerificationCode: user.email_verification_code,
58+
});
59+
5560
throw new TRPCError({
56-
message: 'Email awaiting verification.',
57-
code: 'CONFLICT',
61+
message: 'Email awaiting verification. New email sent.',
62+
code: 'UNAUTHORIZED',
5863
});
5964
}
6065
}
@@ -74,18 +79,28 @@ export async function register({
7479

7580
// Send email
7681

77-
await sendMail({
78-
from: {
79-
name: 'DeepNotes',
80-
email: 'account@deepnotes.app',
81-
},
82-
to: [input.email],
83-
subject: 'Complete your registration',
84-
html: `
85-
Visit the following link to verify your email address:<br/>
86-
<a href="https://deepnotes.app/verify-email/${user.email_verification_code}">https://deepnotes.app/verify-email/${user.email_verification_code}</a><br/>
87-
The link above will expire in 1 hour.
88-
`,
82+
await sendRegistrationEmail({
83+
email: input.email,
84+
emailVerificationCode: user.email_verification_code,
8985
});
9086
});
9187
}
88+
89+
export async function sendRegistrationEmail(input: {
90+
email: string;
91+
emailVerificationCode: string | null;
92+
}) {
93+
await sendMail({
94+
from: {
95+
name: 'DeepNotes',
96+
email: 'account@deepnotes.app',
97+
},
98+
to: [input.email],
99+
subject: 'Complete your registration',
100+
html: `
101+
Visit the following link to verify your email address:<br/>
102+
<a href="https://deepnotes.app/verify-email/${input.emailVerificationCode}">https://deepnotes.app/verify-email/${input.emailVerificationCode}</a><br/>
103+
The link above expires in 1 hour.
104+
`,
105+
});
106+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { hashUserEmail } from '@deeplib/data';
2+
import { UserModel } from '@deeplib/db';
3+
import { w3cEmailRegex } from '@stdlib/misc';
4+
import { TRPCError } from '@trpc/server';
5+
import { once } from 'lodash';
6+
import type { InferProcedureOpts } from 'src/trpc/helpers';
7+
import { publicProcedure } from 'src/trpc/helpers';
8+
import { z } from 'zod';
9+
10+
import { sendRegistrationEmail } from './register';
11+
12+
const baseProcedure = publicProcedure.input(
13+
z.object({
14+
email: z
15+
.string()
16+
.regex(w3cEmailRegex)
17+
.transform((email) => email.toLowerCase()),
18+
}),
19+
);
20+
21+
export const resendVerificationEmailProcedure = once(() =>
22+
baseProcedure.mutation(resendVerificationEmail),
23+
);
24+
25+
export async function resendVerificationEmail({
26+
input,
27+
}: InferProcedureOpts<typeof baseProcedure>) {
28+
// Get user
29+
30+
const user = await UserModel.query()
31+
.where('email_hash', Buffer.from(hashUserEmail(input.email)))
32+
.select('email_verified', 'email_verification_code')
33+
.first();
34+
35+
// Check if user already exists
36+
37+
if (user == null) {
38+
throw new TRPCError({
39+
message: 'User not found.',
40+
code: 'NOT_FOUND',
41+
});
42+
}
43+
44+
// User already exists, check if email is verified
45+
46+
if (user.email_verified) {
47+
throw new TRPCError({
48+
message: 'Email already registered.',
49+
code: 'CONFLICT',
50+
});
51+
}
52+
53+
// Send email
54+
55+
await sendRegistrationEmail({
56+
email: input.email,
57+
emailVerificationCode: user.email_verification_code,
58+
});
59+
}

apps/client/src/pages/home/FinishRegistration.vue

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,47 @@
1111
Check your email to proceed.
1212
</div>
1313

14-
<Gap style="height: 32px" />
15-
</template>
14+
<Gap style="height: 24px" />
15+
16+
<DeepBtn
17+
label="Resend verification email"
18+
color="primary"
19+
style="font-size: 16px; padding: 10px 22px"
20+
@click="resendVerificationEmail()"
21+
/>
1622

17-
<DeepBtn
18-
label="Go to login"
19-
color="primary"
20-
style="font-size: 16px; padding: 10px 22px"
21-
:to="{ name: 'login' }"
22-
/>
23+
<Gap style="height: 64px" />
24+
25+
<DeepBtn
26+
label="Go to login"
27+
color="primary"
28+
style="padding: 10px 22px"
29+
:to="{ name: 'login' }"
30+
/>
31+
</template>
2332
</ResponsiveContainer>
2433
</q-page>
2534
</template>
2635

2736
<script setup lang="ts">
37+
import { handleError } from 'src/code/utils/misc';
38+
2839
useMeta(() => ({
2940
title: 'Finish registration - DeepNotes',
3041
}));
42+
43+
async function resendVerificationEmail() {
44+
try {
45+
await trpcClient.users.account.resendVerificationEmail.mutate({
46+
email: internals.sessionStorage?.getItem('email'),
47+
});
48+
49+
$quasar().notify({
50+
message: 'Verification email resent.',
51+
type: 'positive',
52+
});
53+
} catch (error) {
54+
handleError(error);
55+
}
56+
}
3157
</script>

0 commit comments

Comments
 (0)