Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/forgot password #39

Merged
merged 32 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8ffe1a2
😶 began working on forgotten password backend 😶
Gyoumi Nov 18, 2024
ffa83ec
👾 updated github actions to run tests on push
Gyoumi Nov 18, 2024
506d35c
👷 fixed up schema to fix ci issues
Gyoumi Nov 18, 2024
b7185a1
👷 making adjustments to try to fix cron job issues
Gyoumi Nov 18, 2024
f5e26ff
👷 making adjustments to try to fix cron job issues part 2
Gyoumi Nov 18, 2024
a5cc8ef
👷 making adjustments to try to fix cron job issues part 3
Gyoumi Nov 18, 2024
1c6268c
👷 making adjustments to try to fix cron job issues part 4
Gyoumi Nov 18, 2024
5424380
👷 making adjustments to try to fix cron job issues part 5
Gyoumi Nov 18, 2024
4fea80f
rewrote otp to use redis > db, and added verification route
Gyoumi Nov 19, 2024
ad81fd1
rewrote tests for redis
Gyoumi Dec 3, 2024
f737b76
fixed otp test for redis
Gyoumi Dec 3, 2024
8a0eb32
added conditional for ci
Gyoumi Dec 3, 2024
7e5d5ef
🎊 forgot password backend complete
Gyoumi Dec 8, 2024
8217882
rebasing password reset
Gyoumi Dec 8, 2024
72245a0
👾 updated github actions to run tests on push
Gyoumi Nov 18, 2024
bd85f81
👷 fixed up schema to fix ci issues
Gyoumi Nov 18, 2024
a6c2c63
👷 making adjustments to try to fix cron job issues
Gyoumi Nov 18, 2024
6b08e79
👷 making adjustments to try to fix cron job issues part 2
Gyoumi Nov 18, 2024
ad99efa
👷 making adjustments to try to fix cron job issues part 3
Gyoumi Nov 18, 2024
49c3b5c
👷 making adjustments to try to fix cron job issues part 4
Gyoumi Nov 18, 2024
fd18e86
👷 making adjustments to try to fix cron job issues part 5
Gyoumi Nov 18, 2024
0bbf778
rewrote otp to use redis > db, and added verification route
Gyoumi Nov 19, 2024
475563f
rewrote tests for redis
Gyoumi Dec 3, 2024
84e42ad
fixed otp test for redis
Gyoumi Dec 3, 2024
c9cb27f
🎊 forgot password backend complete
Gyoumi Dec 8, 2024
a2710a1
🪡 fixed merge conflicts with main
Gyoumi Dec 8, 2024
cbca83f
removed prisma from devDependencies
Gyoumi Dec 8, 2024
5cce90b
merged schema
Gyoumi Dec 8, 2024
e10a3e9
Merge branch 'feature/forgot-password' of https://github.com/devsoc-u…
Gyoumi Dec 8, 2024
41895ea
👉👇👈 username and email should be unique for User
Gyoumi Dec 8, 2024
9bd5bdc
fixed conflicts when merging with main
Gyoumi Dec 11, 2024
b09acf1
🔑 created forgot-password and update-password routes. ⏰ updated otp t…
Gyoumi Dec 11, 2024
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
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
- feature/login
- feature/event
- feature/societyJoin
- feature/forgot-password
pull_request:
branches:
- main
Expand Down
4 changes: 3 additions & 1 deletion backend/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ DATABASE_URL="postgres://postgres:postgres@localhost:5432"
DIRECT_URL="postgres://postgres:postgres@localhost:5432"
NODE_ENV=test
REDIS_PORT=6380
SESSION_SECRET=notsecret
SESSION_SECRET=notsecret
OTP_EXPIRES=1
CI=true
5 changes: 3 additions & 2 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
"main": "index.js",
"scripts": {
"start": "NODE_ENV=dev ts-node src/index.ts",
"start_win": "set NODE_ENV=dev && ts-node src/index.ts",
"test": "vitest",
"test": "vitest --inspect-brk --no-file-parallelism --disable-console-intercept",
"test:int": "./scripts/run-integration.sh"
},
"keywords": [],
Expand All @@ -20,6 +19,8 @@
"dayjs": "^1.11.13",
"express": "^4.21.2",
"express-session": "^1.18.1",
"form-data": "^4.0.1",
"mailgun.js": "^10.2.3",
"prisma": "^5.22.0",
"redis": "^4.7.0",
"zod": "^3.23.8",
Expand Down
55 changes: 55 additions & 0 deletions backend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ CREATE TABLE "_KeywordToUser" (
"B" INTEGER NOT NULL
);

-- CreateIndex
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");

-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

-- CreateIndex
CREATE UNIQUE INDEX "Society_name_key" ON "Society"("name");

Expand Down
12 changes: 6 additions & 6 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ datasource db {

model User {
id Int @id @default(autoincrement())
username String
email String
username String @unique
email String @unique
password String
salt String
dateJoined DateTime
Expand Down Expand Up @@ -56,8 +56,8 @@ model Event {
}

model Keyword {
id Int @id @default(autoincrement())
text String @unique
id Int @id @default(autoincrement())
text String @unique
subscribers User[]
events Event[]
}
events Event[]
}
142 changes: 141 additions & 1 deletion backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ import prisma from "./prisma";
import RedisStore from "connect-redis";
import { createClient } from "redis";
import dayjs, { Dayjs } from "dayjs";
import { generateOTP } from "./routes/OTP/generateOTP";
import assert from "assert";
import { verifyOTP } from "./routes/OTP/verifyOTP";
import { findUserFromId, updateUserPasswordFromEmail } from "./routes/User/user";

declare module "express-session" {
interface SessionData {
userId: number;
Expand All @@ -26,6 +31,7 @@ declare module "express-session" {

// Initialize client.
if (process.env["REDIS_PORT"] === undefined) {
console.log(process.env);
console.error("Redis port not provided in .env file");
process.exit(1);
}
Expand All @@ -43,6 +49,7 @@ let redisStore = new RedisStore({

const app = express();
const SERVER_PORT = 5180;
const SALT_ROUNDS = 10;

app.use(cors());
app.use(express.json());
Expand Down Expand Up @@ -96,7 +103,7 @@ app.post(
.json({ error: "Account with same credentials already exists" });
}

const saltRounds: number = 10;
const saltRounds: number = SALT_ROUNDS;
const salt = await bcrypt.genSalt(saltRounds);
const hashedPassword = await bcrypt.hash(password, salt);

Expand All @@ -122,6 +129,139 @@ app.post(
}
);

app.post("/auth/otp/generate", async(req: Request, res: Response) => {
try {
const { email } : { email: string} = req.body;

if(!email) {
throw new Error("Email address expected.");
}

const token = await generateOTP(email);

if (token) {
const expiryTime = process.env["OTP_EXPIRES"];

try {
await redisClient.set(email, token, { EX: parseInt(expiryTime as string) });
} catch {
console.error("OTP expiration time not set in environment variable.");
}

if(process.env["CI"]) {
return res.status(200).json({message: token});
}
return res.status(200).json({ message: "ok" });
} else {
return res.status(400).json( {message: "Unexpected error while generating OTP."} );
}

} catch(error) {
return res.status(400).json({ message: (error as Error).message });
}
});

app.post("/auth/otp/verify", async(req: Request, res: Response) => {
Gyoumi marked this conversation as resolved.
Show resolved Hide resolved
try {
const { email, token } = req.body;

if(!email) {
throw new Error("Email address expected.");
}

if(!token) {
throw new Error("One time code expected.");
}

const otp = await redisClient.get(email);

verifyOTP(token, otp);

const expiryTime = process.env["OTP_EXPIRES"];

try {
await redisClient.set(email, token, { EX: parseInt(expiryTime as string) });
} catch {
console.error("OTP expiration time not set in environment variable.");
}

return res.status(200).json({message: "ok" });

} catch (error) {
return res.status(400).json({ message: (error as Error).message });
}
});

app.post("/auth/password/forgot", async(req: Request, res: Response) => {
try {
const { email, token, newPassword } = req.body;

if(!email) {
throw new Error("Email is expected.");
}

if(!token) {
throw new Error("One time code required to reset password.");
}

if(!newPassword) {
throw new Error("New password is invalid.");
}

const otp = await redisClient.get(email);

verifyOTP(token, otp);

await updateUserPasswordFromEmail(email, newPassword, SALT_ROUNDS);

return res.status(200).json({message: "ok"});

} catch (error) {
return res.status(400).json({ message: `Unable to update password. ${(error as Error).message}`});
}
});

app.post("/auth/password/update", async(req: Request, res: Response) => {
try {
const { oldPassword, newPassword } = req.body;

if(!oldPassword) {
throw new Error("Unable to validate existing password.");
}

if(!newPassword) {
throw new Error("Invalid new password.");
}

const currentId = req.session.userId;

if(!currentId) {
throw new Error("Invalid session.");
}


// validate old password
const user = await findUserFromId(currentId);

const validPassword = await bcrypt.compare(oldPassword, user.password);
if (!validPassword) {
throw new Error("Current password is incorrect.");
}

// save new password
await updateUserPasswordFromEmail(user.email, newPassword, SALT_ROUNDS);

// refresh session
req.session.cookie.expires = dayjs().add(1, "week").toDate();
req.session.save();

return res.status(200).json({message: "ok"});

} catch (error) {
return res.status(400).json({ message: `Unable to update password. ${(error as Error).message}`});
}
})

app.post("/auth/login", async (req: TypedRequest<LoginBody>, res: Response) => {
try {
const { username, password } = req.body;
Expand Down
Loading
Loading