Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 href="<%= 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