Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions api-types/errorCodes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export enum ErrorCodes {
EmailNotVerified = 1,
}
14 changes: 14 additions & 0 deletions docs/auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Authentication System

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.

## Registration

1. User clicks on the "Sign Up" button.
2. User is redirected to the Auth0 login page.
3. User enters their email and password.
4. Auth0 sends a verification email, which we leverage for the welcome email.
5. User clicks on the verification link in the email.
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.
> **ℹ️ Info**
> Remember. The application.callback_domain variable will contain the origin of the first URL listed in the application's Allowed Callback URL list
5 changes: 0 additions & 5 deletions netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,3 @@

[[plugins]]
package = "@netlify/plugin-nextjs"

[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200
58 changes: 48 additions & 10 deletions netlify/functions-src/functions/common/auth0.service.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import axios from 'axios'
import axios, { type AxiosResponse } from 'axios'
import Config from '../config'

export class Auth0Service {
class Auth0Service {
private readonly auth0Domain = Config.auth0.backend.DOMAIN
private readonly clientId = Config.auth0.backend.CLIENT_ID
private readonly clientSecret = Config.auth0.backend.CLIENT_SECRET
private readonly audience = Config.auth0.backend.AUDIENCE

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

async getUserProfile(accessToken: string, userId: string): Promise<any> {
const response = await axios.get(`https://${this.auth0Domain}/api/v2/users/${userId}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
return response.data
async getUserProfile(auth0Id: string): Promise<AxiosResponse<{ email: string, nickname: string, picture: string }>['data']> {
try {
const { access_token } = await this.getAdminAccessToken();
const response = await axios.get(`https://${this.auth0Domain}/api/v2/users/${auth0Id}`, {
headers: {
Authorization: `Bearer ${access_token}`,
},
})
return response.data
} catch (error) {
console.error('getUserProfile, Error:', error)
throw new Error('Error getting user profile')
}
}

async deleteUser(accessToken: string, userId: string): Promise<void> {
Expand All @@ -38,4 +44,36 @@ export class Auth0Service {
},
})
}

async createVerificationEmailTicket(
auth0UserId: string,
) {
try {
const { access_token: accessToken } = await this.getAdminAccessToken();
const [provider, userId] = auth0UserId.split('|');
const payload = {
result_url: Config.urls.CLIENT_BASE_URL,
user_id: auth0UserId,
identity: { user_id: userId, provider },
};

const response = await axios.post(
`https://${Config.auth0.backend.DOMAIN}/api/v2/tickets/email-verification`,
payload,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'content-type': 'application/json',
},
},
);

return response.data;
} catch (error) {
console.error('createVerificationEmailTicket, Error:', error)
throw error;
}
}
}

export const auth0Service = new Auth0Service();
2 changes: 1 addition & 1 deletion netlify/functions-src/functions/common/dto/user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ObjectId } from 'mongodb'
import { Role } from '../interfaces/user.interface'

export class UserDto {
_id?: ObjectId
_id: ObjectId
auth0Id: string
email: string
name: string
Expand Down
90 changes: 0 additions & 90 deletions netlify/functions-src/functions/common/email.service.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export interface User {
tags?: string[];
}

export type ApplicationUser = User & {
email_verified: boolean;
}

export enum Role {
ADMIN = 'Admin',
MEMBER = 'Member',
Expand Down
3 changes: 3 additions & 0 deletions netlify/functions-src/functions/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ const config = {
pagination: {
limit: 20,
},
urls: {
CLIENT_BASE_URL: process.env.CLIENT_BASE_URL,
},
};

export default config;
7 changes: 2 additions & 5 deletions netlify/functions-src/functions/data/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,10 @@ export const getUserById = async (id: string, currentUserAuth0Id?: string): Prom
return getUserWithoutChannels(id);
}

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

if (!user) {
throw new DataError(404, 'User not found');
}
return user;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ interface MentorFreeze {
};
}

interface EmailVerification {
name: 'email-verification';
data: {
name: string;
link: string;
};
}

export type EmailParams = Required<Pick<MailData, 'to' | 'subject'>> &
(
| WelcomePayload
Expand All @@ -107,6 +115,7 @@ export type EmailParams = Required<Pick<MailData, 'to' | 'subject'>> &
| MentorApplicationDeclined
| MentorApplicationApproved
| MentorNotActive
| EmailVerification
| MentorFreeze
);

Expand Down
9 changes: 7 additions & 2 deletions netlify/functions-src/functions/email/templates/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
### Run

```bash
nodemon --config nodemon-emails.json
nodemon --config netlify/functions-src/functions/email/templates/nodemon-emails.json
```

> **ℹ️ Info**
> The welcome email template is managed by Auth0 as part of the email verification process.
> You can view and edit the template in the [Auth0 Dashboard](https://manage.auth0.com/dashboard/eu/codingcoach/templates).

### Links

|||
Expand All @@ -17,4 +21,5 @@ nodemon --config nodemon-emails.json
|Mentorship reminder|http://localhost:3003/mentorship-reminder?data={%22menteeName%22:%22Moshe%22,%22mentorName%22:%22Brent%22,%22message%22:%22because%22}|
|Mentor application received|http://localhost:3003/mentor-application-received?data={%22name%22:%22Brent%22}|
|Mentorship application denied|http://localhost:3003/mentor-application-declined?data={%22name%22:%22Moshe%22,%22reason%22:%22your%20avatar%20is%20not%20you%22}|
|Mentorship application approved|http://localhost:3003/mentor-application-approved?data={%22name%22:%22Moshe%22}|
|Mentorship application approved|http://localhost:3003/mentor-application-approved?data={%22name%22:%22Moshe%22}|
|Email Verification|http://localhost:3003/email-verification?data={%22name%22:%22Moshe%22,%20%22link%22:%20%22https://mentors.codingcoach.io%22}|
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<div style="text-align: center; font-size: 18px">
<table width="100%">
<tbody>
<tr>
<td align="center">
<img
style="width: 39%; max-width: 234px"
src="http://cdn.mcauto-images-production.sendgrid.net/83a8af126d5ca8ff/70fa68eb-a954-47c6-9798-306089b6a4e3/600x370.jpg"
alt="Illustration"
/>
</td>
</tr>
</tbody>
</table>
<h2 style="font-size: 26px; font-weight: normal">Hey <%= name %></h2>
<h1
style="
font-size: 32px;
line-height: 42px;
font-weight: normal;
color: #69d5b1;
"
>
You're almost there!
</h1>
<p>Please click the link below to verify your email</p>
<p style="margin-top: 10px">
<a
href="<%= link %>"
style="
background-color: #00bc89;
border: 1px solid #333333;
border-color: #00bc89;
border-radius: 6px;
border-width: 1px;
color: #ffffff;
display: inline-block;
font-size: 16px;
font-weight: normal;
letter-spacing: 0px;
line-height: 16px;
padding: 12px 18px 12px 18px;
text-align: center;
text-decoration: none;
"
target="_blank"
>Verify</a
>
</p>
<p style="margin-top: 10px">
<small>
(Or copy and paste this url
<a herf="<%= link %>" target="_blank"><%= link %></a> into your browser)
</p>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"watch": ["."],
"ext": "html,js",
"exec": "node netlify/functions-src/functions/email/templates/show.js"
}
4 changes: 2 additions & 2 deletions netlify/functions-src/functions/email/templates/show.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const fs = require('fs');

const app = express();
const port = 3003;
const layout = fs.readFileSync('content/email_templates/layout.html', {
const layout = fs.readFileSync(`${__dirname}/layout.html`, {
encoding: 'utf8',
});

Expand All @@ -20,7 +20,7 @@ app.get('/:templateName', function (req, res) {
if (templateName.includes('.')) return;
const { data } = req.query;
const template = fs.readFileSync(
`content/email_templates/${templateName}.html`,
`${__dirname}/${templateName}.html`,
{ encoding: 'utf8' },
);
const content = injectData(
Expand Down
8 changes: 6 additions & 2 deletions netlify/functions-src/functions/email/templates/welcome.html
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,16 @@
text-align: left;
word-wrap: break-word;
">
<p style="font-size: 14px; line-height: 140%">
<p style="font-size: 14px; line-height: 140%; text-align: center;">
<span style="
font-family: verdana, geneva;
font-size: 18px;
line-height: 25.2px;
">We're so glad you've joined us. Here is what you can do next:</span>
">We're so glad you've joined us<br />Please click <a href="{{ url }}">here</a> to verify your email.</span>
</p>
<p>&nbsp;</p>
<p>
More things to do:
</p>
<ul style="margin-bottom: 0;">
<li>
Expand Down
Loading