Skip to content

Commit a38106a

Browse files
authored
fest: email verification (#979)
1 parent 32adafb commit a38106a

File tree

38 files changed

+533
-215
lines changed

38 files changed

+533
-215
lines changed

api-types/errorCodes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export enum ErrorCodes {
2+
EmailNotVerified = 1,
3+
}

docs/auth.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Authentication System
2+
3+
We're using Auth0 to handle authentication in our application. This document outlines the authentication flow and how to set up your environment for development.
4+
5+
## Registration
6+
7+
1. User clicks on the "Sign Up" button.
8+
2. User is redirected to the Auth0 login page.
9+
3. User enters their email and password.
10+
4. Auth0 sends a verification email, which we leverage for the welcome email.
11+
5. User clicks on the verification link in the email.
12+
6. User is redirected to the application with a verification token. For more information about the redirection see the [docs](https://auth0.com/docs/customize/email/email-templates#configure-template-fields) - open the "Redirect the URL" section.
13+
> **ℹ️ Info**
14+
> Remember. The application.callback_domain variable will contain the origin of the first URL listed in the application's Allowed Callback URL list

netlify.toml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,3 @@
1515

1616
[[plugins]]
1717
package = "@netlify/plugin-nextjs"
18-
19-
[[redirects]]
20-
from = "/api/*"
21-
to = "/.netlify/functions/:splat"
22-
status = 200
Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import axios from 'axios'
1+
import axios, { type AxiosResponse } from 'axios'
22
import Config from '../config'
33

4-
export class Auth0Service {
4+
class Auth0Service {
55
private readonly auth0Domain = Config.auth0.backend.DOMAIN
66
private readonly clientId = Config.auth0.backend.CLIENT_ID
77
private readonly clientSecret = Config.auth0.backend.CLIENT_SECRET
88
private readonly audience = Config.auth0.backend.AUDIENCE
99

10-
async getAdminAccessToken(): Promise<any> {
10+
async getAdminAccessToken(): Promise<AxiosResponse<{ access_token: string }>['data']> {
1111
try {
1212
const response = await axios.post(`https://${this.auth0Domain}/oauth/token`, {
1313
client_id: this.clientId,
@@ -22,13 +22,19 @@ export class Auth0Service {
2222
}
2323
}
2424

25-
async getUserProfile(accessToken: string, userId: string): Promise<any> {
26-
const response = await axios.get(`https://${this.auth0Domain}/api/v2/users/${userId}`, {
27-
headers: {
28-
Authorization: `Bearer ${accessToken}`,
29-
},
30-
})
31-
return response.data
25+
async getUserProfile(auth0Id: string): Promise<AxiosResponse<{ email: string, nickname: string, picture: string }>['data']> {
26+
try {
27+
const { access_token } = await this.getAdminAccessToken();
28+
const response = await axios.get(`https://${this.auth0Domain}/api/v2/users/${auth0Id}`, {
29+
headers: {
30+
Authorization: `Bearer ${access_token}`,
31+
},
32+
})
33+
return response.data
34+
} catch (error) {
35+
console.error('getUserProfile, Error:', error)
36+
throw new Error('Error getting user profile')
37+
}
3238
}
3339

3440
async deleteUser(accessToken: string, userId: string): Promise<void> {
@@ -38,4 +44,36 @@ export class Auth0Service {
3844
},
3945
})
4046
}
47+
48+
async createVerificationEmailTicket(
49+
auth0UserId: string,
50+
) {
51+
try {
52+
const { access_token: accessToken } = await this.getAdminAccessToken();
53+
const [provider, userId] = auth0UserId.split('|');
54+
const payload = {
55+
result_url: Config.urls.CLIENT_BASE_URL,
56+
user_id: auth0UserId,
57+
identity: { user_id: userId, provider },
58+
};
59+
60+
const response = await axios.post(
61+
`https://${Config.auth0.backend.DOMAIN}/api/v2/tickets/email-verification`,
62+
payload,
63+
{
64+
headers: {
65+
Authorization: `Bearer ${accessToken}`,
66+
'content-type': 'application/json',
67+
},
68+
},
69+
);
70+
71+
return response.data;
72+
} catch (error) {
73+
console.error('createVerificationEmailTicket, Error:', error)
74+
throw error;
75+
}
76+
}
4177
}
78+
79+
export const auth0Service = new Auth0Service();

netlify/functions-src/functions/common/dto/user.dto.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { ObjectId } from 'mongodb'
22
import { Role } from '../interfaces/user.interface'
33

44
export class UserDto {
5-
_id?: ObjectId
5+
_id: ObjectId
66
auth0Id: string
77
email: string
88
name: string

netlify/functions-src/functions/common/email.service.ts

Lines changed: 0 additions & 90 deletions
This file was deleted.

netlify/functions-src/functions/common/interfaces/user.interface.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ export interface User {
1818
tags?: string[];
1919
}
2020

21+
export type ApplicationUser = User & {
22+
email_verified: boolean;
23+
}
24+
2125
export enum Role {
2226
ADMIN = 'Admin',
2327
MEMBER = 'Member',

netlify/functions-src/functions/config/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ const config = {
3333
pagination: {
3434
limit: 20,
3535
},
36+
urls: {
37+
CLIENT_BASE_URL: process.env.CLIENT_BASE_URL,
38+
},
3639
};
3740

3841
export default config;

netlify/functions-src/functions/data/users.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,10 @@ export const getUserById = async (id: string, currentUserAuth0Id?: string): Prom
8989
return getUserWithoutChannels(id);
9090
}
9191

92-
export const getUserByAuthId = async (auth0Id: string) => {
92+
export const getUserBy = async <T extends keyof Pick<User, '_id' | 'auth0Id' | 'email'>>(prop: T, value: User[T]) => {
9393
const user = await getCollection<User>('users')
94-
.findOne({ auth0Id });
94+
.findOne({ [prop]: value });
9595

96-
if (!user) {
97-
throw new DataError(404, 'User not found');
98-
}
9996
return user;
10097
}
10198

netlify/functions-src/functions/email/interfaces/email.interface.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,14 @@ interface MentorFreeze {
9595
};
9696
}
9797

98+
interface EmailVerification {
99+
name: 'email-verification';
100+
data: {
101+
name: string;
102+
link: string;
103+
};
104+
}
105+
98106
export type EmailParams = Required<Pick<MailData, 'to' | 'subject'>> &
99107
(
100108
| WelcomePayload
@@ -107,6 +115,7 @@ export type EmailParams = Required<Pick<MailData, 'to' | 'subject'>> &
107115
| MentorApplicationDeclined
108116
| MentorApplicationApproved
109117
| MentorNotActive
118+
| EmailVerification
110119
| MentorFreeze
111120
);
112121

0 commit comments

Comments
 (0)