Skip to content

Commit 509842a

Browse files
committed
added email verification
1 parent 5f6c66d commit 509842a

File tree

21 files changed

+1090
-48
lines changed

21 files changed

+1090
-48
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
node_modules
22
# Keep environment variables out of version control
33
.env
4+
5+
build/

config/custom-environment-variables.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,11 @@ export default {
55
accessTokenPublicKey: 'JWT_ACCESS_TOKEN_PUBLIC_KEY',
66
refreshTokenPrivateKey: 'JWT_REFRESH_TOKEN_PRIVATE_KEY',
77
refreshTokenPublicKey: 'JWT_REFRESH_TOKEN_PUBLIC_KEY',
8+
9+
smtp: {
10+
host: 'EMAIL_HOST',
11+
pass: 'EMAIL_PASS',
12+
port: 'EMAIL_PORT',
13+
user: 'EMAIL_USER',
14+
},
815
};

example.env

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Environment variables declared in this file are automatically made available to Prisma.
2+
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
3+
4+
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB (Preview).
5+
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
6+
7+
DATABASE_URL=postgresql://admin:password123@localhost:6500/node_prisma?schema=public
8+
9+
PORT=8000
10+
NODE_ENV=development
11+
12+
POSTGRES_HOST=127.0.0.1
13+
POSTGRES_PORT=6500
14+
POSTGRES_USER=admin
15+
POSTGRES_PASSWORD=password123
16+
POSTGRES_DB=node_prisma
17+
18+
EMAIL_USER=maycgho4gm6paqnq@ethereal.email
19+
EMAIL_PASS=RbVrQdN94Ax5CJdnwp
20+
EMAIL_HOST=smtp.ethereal.email
21+
EMAIL_PORT=587
22+
23+
JWT_ACCESS_TOKEN_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT1FJQkFBSkJBSjdVblpyNUxpUGJxbDRENlo3VHVKK2NFMkI0Y3FzbnUzeUJaUHo2NmtqZDhJT1RFdjlNCkpEdmhMQ05PczYyWHBZcmFZYU5HS3UrN3Q4YVVjcWNoRzJNQ0F3RUFBUUpBRS84YXRKY29tdlVkOXVZeE5JRGQKWHFMc3dabUlma25yVGRxUWwxVVR5QWFPRWpIRGFnR0lGdEhRZE5IZTAybkp6a2Z1WkdWSkJVRmo1aTJJVyszMQpxUUloQU9LQVRxOVpSZHR0T0JDWWJLR3VpbjZnd1FVZ2YzUGkwSjh3Snh3cjNRVDFBaUVBczRRZ0lhR1c1V3NaCmNqWUQ3bVpwcXBiRkdubnBvMVR6QU1YS2psQ2dKL2NDSUQyZVZFbWx5cmhnSlNGMnBnN3lNZUV6RUcrNW9KTEIKUUtvZDZuWGloUFZGQWlCY0ZuUXhMR1p1NjhEUzhOaVZiQjNhYjV0TzJLazhxekE0L2ozSlFaeld3d0lnU2N1awoyZWlBM3E5ci9EdDQ3OUZHek4xSEVSdzJ2TXZjeEV6REZWRnV5aDg9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0t
24+
JWT_ACCESS_TOKEN_PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZ3d0RRWUpLb1pJaHZjTkFRRUJCUUFEU3dBd1NBSkJBSjdVblpyNUxpUGJxbDRENlo3VHVKK2NFMkI0Y3Fzbgp1M3lCWlB6NjZramQ4SU9URXY5TUpEdmhMQ05PczYyWHBZcmFZYU5HS3UrN3Q4YVVjcWNoRzJNQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ==
25+
JWT_REFRESH_TOKEN_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT0FJQkFBSkFmTmJzOVJhUGNnVG5RalB4cExLUXkvRHdBSVh5NFNiUWdlejFNU2pwWnozQ0tsTXdpWWwyCm5IUVg5Rk5NcU1pMmxnL29yUjJDTUJxRzFNdnlvN1EyTVFJREFRQUJBa0FKUStwU1JscGZHLzRONjgwRGJEMVMKNVk3cWV3YUxyMVhLVHN2ajJpVjRoQUdxb0d3K3NvbEZUalp1Y21xaW9vY1cydTNXYVdxdkNrVmtvamc2OFFBQgpBaUVBNElqNkFXbG91am84T1YvSEJ3elZpaDQzRllCNEpkZnd3SjFwYTBRVFlWRUNJUUNPVlhMdGZOTFh3cXFlCndZNVIrWktwY3JBb0tBMjVYM3M1cEhVNFhYVk80UUlnSDM0VzBxdmVMSUNPZ2QyVkpNQUFFMmM1Z3FLS040U2EKRituOEp6ZGRJSUVDSUZ4SllUeEU2L3lEdHRjNnpzbXVGWDhTNHM4V3NWZFpabStJaDR5bFpGTmhBaUFGRWJwSApqdlZqSHlPdlIyQ2F1clFmTmU3QldWVnVlODNQN3pYSXV3eHl5Zz09Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0t
26+
JWT_REFRESH_TOKEN_PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZzd0RRWUpLb1pJaHZjTkFRRUJCUUFEU2dBd1J3SkFmTmJzOVJhUGNnVG5RalB4cExLUXkvRHdBSVh5NFNiUQpnZXoxTVNqcFp6M0NLbE13aVlsMm5IUVg5Rk5NcU1pMmxnL29yUjJDTUJxRzFNdnlvN1EyTVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t

package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,31 @@
1313
"@types/cookie-parser": "^1.4.3",
1414
"@types/cors": "^2.8.12",
1515
"@types/express": "^4.17.13",
16+
"@types/html-to-text": "^8.1.0",
1617
"@types/jsonwebtoken": "^8.5.8",
1718
"@types/lodash": "^4.14.182",
1819
"@types/morgan": "^1.9.3",
1920
"@types/node": "^17.0.31",
20-
"prisma": "^3.13.0",
21+
"@types/nodemailer": "^6.4.4",
22+
"@types/pug": "^2.0.6",
23+
"prisma": "^3.14.0",
2124
"typescript": "^4.6.4"
2225
},
2326
"dependencies": {
24-
"@prisma/client": "^3.13.0",
27+
"@prisma/client": "^3.14.0",
2528
"bcryptjs": "^2.4.3",
2629
"config": "^3.3.7",
2730
"cookie-parser": "^1.4.6",
2831
"cors": "^2.8.5",
2932
"dotenv": "^16.0.0",
3033
"envalid": "^7.3.1",
3134
"express": "^4.18.1",
35+
"html-to-text": "^8.2.0",
3236
"jsonwebtoken": "^8.5.1",
3337
"lodash": "^4.17.21",
3438
"morgan": "^1.10.0",
39+
"nodemailer": "^6.7.5",
40+
"pug": "^3.0.2",
3541
"redis": "^4.1.0",
3642
"ts-node-dev": "^1.1.8",
3743
"zod": "^3.15.1"

prisma/migrations/20220509153453_added_user_model/migration.sql renamed to prisma/migrations/20220516191659_user_model/migration.sql

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ CREATE TABLE "users" (
1010
"verified" BOOLEAN DEFAULT false,
1111
"password" TEXT NOT NULL,
1212
"role" "RoleEnumType" DEFAULT E'user',
13+
"verificationCode" TEXT,
1314
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
1415
"updatedAt" TIMESTAMP(3) NOT NULL,
1516

@@ -18,3 +19,9 @@ CREATE TABLE "users" (
1819

1920
-- CreateIndex
2021
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
22+
23+
-- CreateIndex
24+
CREATE UNIQUE INDEX "users_verificationCode_key" ON "users"("verificationCode");
25+
26+
-- CreateIndex
27+
CREATE INDEX "users_email_verificationCode_idx" ON "users"("email", "verificationCode");
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- DropIndex
2+
DROP INDEX "users_verificationCode_key";
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
Warnings:
3+
4+
- A unique constraint covering the columns `[verificationCode]` on the table `users` will be added. If there are existing duplicate values, this will fail.
5+
- A unique constraint covering the columns `[email,verificationCode]` on the table `users` will be added. If there are existing duplicate values, this will fail.
6+
7+
*/
8+
-- CreateIndex
9+
CREATE UNIQUE INDEX "users_verificationCode_key" ON "users"("verificationCode");
10+
11+
-- CreateIndex
12+
CREATE UNIQUE INDEX "users_email_verificationCode_key" ON "users"("email", "verificationCode");

prisma/schema.prisma

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,13 @@ model User{
2121
password String
2222
role RoleEnumType? @default(user)
2323
24+
verificationCode String? @db.Text @unique
25+
2426
createdAt DateTime @default(now())
2527
updatedAt DateTime @updatedAt
28+
29+
@@unique([email, verificationCode])
30+
@@index([email, verificationCode])
2631
}
2732

2833
enum RoleEnumType {

src/app.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ const prisma = new PrismaClient();
1616
const app = express();
1717

1818
async function bootstrap() {
19+
// TEMPLATE ENGINE
20+
app.set('view engine', 'pug');
21+
app.set('views', `${__dirname}/views`);
22+
1923
// MIDDLEWARE
2024

2125
// 1.Body Parser
@@ -39,6 +43,14 @@ async function bootstrap() {
3943
app.use('/api/auth', authRouter);
4044
app.use('/api/users', userRouter);
4145

46+
// Testing
47+
app.get('/api/healthchecker', (_, res: Response) => {
48+
res.status(200).json({
49+
status: 'success',
50+
message: 'Welcome to NodeJs with Prisma and PostgreSQL',
51+
});
52+
});
53+
4254
// UNHANDLED ROUTES
4355
app.all('*', (req: Request, res: Response, next: NextFunction) => {
4456
next(new AppError(404, `Route ${req.originalUrl} not found`));

src/controllers/auth.controller.ts

Lines changed: 89 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
1+
import crypto from 'crypto';
12
import { CookieOptions, NextFunction, Request, Response } from 'express';
23
import bcrypt from 'bcryptjs';
3-
import { LoginUserInput, RegisterUserInput } from '../schemas/user.schema';
4+
import {
5+
LoginUserInput,
6+
RegisterUserInput,
7+
VerifyEmailInput,
8+
} from '../schemas/user.schema';
49
import {
510
createUser,
611
findUniqueUser,
12+
findUser,
713
signTokens,
14+
updateUser,
815
} from '../services/user.service';
916
import { Prisma } from '@prisma/client';
1017
import config from 'config';
1118
import AppError from '../utils/appError';
1219
import redisClient from '../utils/connectRedis';
1320
import { signJwt, verifyJwt } from '../utils/jwt';
21+
import Email from '../utils/email';
1422

1523
const cookiesOptions: CookieOptions = {
1624
httpOnly: true,
@@ -43,18 +51,38 @@ export const registerUserHandler = async (
4351
try {
4452
const hashedPassword = await bcrypt.hash(req.body.password, 12);
4553

54+
const verifyCode = crypto.randomBytes(32).toString('hex');
55+
const verificationCode = crypto
56+
.createHash('sha256')
57+
.update(verifyCode)
58+
.digest('hex');
59+
4660
const user = await createUser({
4761
name: req.body.name,
4862
email: req.body.email.toLowerCase(),
4963
password: hashedPassword,
64+
verificationCode,
5065
});
5166

52-
res.status(201).json({
53-
status: 'success',
54-
data: {
55-
user,
56-
},
57-
});
67+
const redirectUrl = `${config.get<string>(
68+
'origin'
69+
)}/verifyemail/${verifyCode}`;
70+
try {
71+
await new Email(user, redirectUrl).sendVerificationCode();
72+
await updateUser({ id: user.id }, { verificationCode });
73+
74+
res.status(201).json({
75+
status: 'success',
76+
message:
77+
'An email with a verification code has been sent to your email',
78+
});
79+
} catch (error) {
80+
await updateUser({ id: user.id }, { verificationCode: null });
81+
return res.status(500).json({
82+
status: 'error',
83+
message: 'There was an error sending email, please try again',
84+
});
85+
}
5886
} catch (err: any) {
5987
if (err instanceof Prisma.PrismaClientKnownRequestError) {
6088
if (err.code === 'P2002') {
@@ -76,7 +104,24 @@ export const loginUserHandler = async (
76104
try {
77105
const { email, password } = req.body;
78106

79-
const user = await findUniqueUser({ email });
107+
const user = await findUniqueUser(
108+
{ email: email.toLowerCase() },
109+
{ id: true, email: true, verified: true, password: true }
110+
);
111+
112+
if (!user) {
113+
return next(new AppError(400, 'Invalid email or password'));
114+
}
115+
116+
// Check if user is verified
117+
if (!user.verified) {
118+
return next(
119+
new AppError(
120+
401,
121+
'You are not verified, please verify your email to login'
122+
)
123+
);
124+
}
80125

81126
if (!user || !(await bcrypt.compare(password, user.password))) {
82127
return next(new AppError(400, 'Invalid email or password'));
@@ -182,3 +227,39 @@ export const logoutUserHandler = async (
182227
next(err);
183228
}
184229
};
230+
231+
export const verifyEmailHandler = async (
232+
req: Request<VerifyEmailInput>,
233+
res: Response,
234+
next: NextFunction
235+
) => {
236+
try {
237+
const verificationCode = crypto
238+
.createHash('sha256')
239+
.update(req.params.verificationCode)
240+
.digest('hex');
241+
242+
const user = await updateUser(
243+
{ verificationCode },
244+
{ verified: true, verificationCode: null },
245+
{ email: true }
246+
);
247+
248+
if (!user) {
249+
return next(new AppError(401, 'Could not verify email'));
250+
}
251+
252+
res.status(200).json({
253+
status: 'success',
254+
message: 'Email verified successfully',
255+
});
256+
} catch (err: any) {
257+
if (err.code === 'P2025') {
258+
return res.status(403).json({
259+
status: 'fail',
260+
message: `Verification code is invalid or user doesn't exist`,
261+
});
262+
}
263+
next(err);
264+
}
265+
};

0 commit comments

Comments
 (0)