Skip to content

Commit c03bd52

Browse files
committed
PEER-208 Add service logic for login
Signed-off-by: SeeuSim <[email protected]>
1 parent 3f54f59 commit c03bd52

File tree

7 files changed

+182
-64
lines changed

7 files changed

+182
-64
lines changed

backend/user/src/controllers/auth/auth-controller.ts

Lines changed: 0 additions & 61 deletions
This file was deleted.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { Request, Response } from 'express';
2+
import { StatusCodes } from 'http-status-codes';
3+
4+
import { loginService } from '@/services/auth';
5+
import type { ILoginPayload } from '@/services/auth/types';
6+
7+
export async function login(req: Request, res: Response) {
8+
const { username, password }: Partial<ILoginPayload> = req.body;
9+
if (!username || !password) {
10+
return res.status(StatusCodes.UNPROCESSABLE_ENTITY).json('Malformed Request');
11+
}
12+
const { code, data, error } = await loginService({ username, password });
13+
if (error || code !== StatusCodes.OK || !data) {
14+
const sanitizedErr = error?.message ?? 'An error occurred.';
15+
return res.status(code).json(sanitizedErr);
16+
}
17+
return res
18+
.status(StatusCodes.OK)
19+
.cookie('jwtToken', data.cookie, { httpOnly: true })
20+
.json(data.user);
21+
}
22+
23+
export async function logout(_req: Request, res: Response) {
24+
return res
25+
.clearCookie('jwtToken', {
26+
secure: true,
27+
sameSite: 'none',
28+
})
29+
.status(StatusCodes.OK)
30+
.json('User has been logged out.');
31+
}

backend/user/src/routes/auth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import express from 'express';
22

3-
import { login, logout } from '@/controllers/auth/auth-controller';
3+
import { login, logout } from '@/controllers/auth';
44
import { limiter } from '@/lib/ratelimit';
55

66
const router = express.Router();
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { eq, getTableColumns, sql } from 'drizzle-orm';
2+
import { StatusCodes } from 'http-status-codes';
3+
import bcrypt from 'bcrypt';
4+
import jwt from 'jsonwebtoken';
5+
6+
import { db, users } from '@/lib/db';
7+
import type { ILoginPayload } from './types';
8+
9+
// TODO: Set env var and rotate automatically
10+
const _JWT_SECRET_KEY = 'secret';
11+
12+
const _FAILED_ATTEMPTS_ALLOWED = 3;
13+
const _getSchema = () => {
14+
const { id, username, password, email, failedAttempts, unlockTime } = getTableColumns(users);
15+
return {
16+
id,
17+
username,
18+
password,
19+
email,
20+
failedAttempts,
21+
unlockTime,
22+
};
23+
};
24+
export const loginService = async (payload: ILoginPayload) => {
25+
const rows = await db
26+
.select(_getSchema())
27+
.from(users)
28+
.where(eq(users.username, payload.username))
29+
.limit(1);
30+
31+
// 1. Cannot find
32+
if (rows.length === 0) {
33+
return {
34+
code: StatusCodes.NOT_FOUND,
35+
error: {
36+
message: 'Not Found',
37+
},
38+
};
39+
}
40+
const { unlockTime, password, failedAttempts, ...user } = rows[0];
41+
42+
// 2. Locked out
43+
if (unlockTime !== null) {
44+
const currentTime = new Date();
45+
if (unlockTime > currentTime) {
46+
return {
47+
code: StatusCodes.CONFLICT,
48+
error: {
49+
message: 'Too many failed attempts - try again later',
50+
},
51+
};
52+
}
53+
}
54+
55+
// 3. Wrong Password
56+
const isPasswordValid = bcrypt.compareSync(payload.password, password);
57+
if (!isPasswordValid) {
58+
const newFailedAttempts = (failedAttempts ?? 0) + 1;
59+
const updateValues = {
60+
failedAttempts: newFailedAttempts,
61+
unlockTime:
62+
newFailedAttempts >= _FAILED_ATTEMPTS_ALLOWED ? sql`NOW() + INTERVAL '1 hour'` : undefined,
63+
};
64+
await db.update(users).set(updateValues).where(eq(users.username, payload.username));
65+
return {
66+
code: StatusCodes.UNAUTHORIZED,
67+
error: {
68+
message: 'Incorrect Password',
69+
},
70+
};
71+
}
72+
73+
// 4. Correct Password
74+
if ((failedAttempts !== null && failedAttempts > 0) || unlockTime !== null) {
75+
await db.update(users).set({
76+
failedAttempts: null,
77+
unlockTime: null,
78+
});
79+
}
80+
const jwtToken = jwt.sign({ id: user.id }, _JWT_SECRET_KEY);
81+
return {
82+
code: StatusCodes.OK,
83+
data: {
84+
cookie: jwtToken,
85+
user,
86+
},
87+
};
88+
};
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export interface LoginCredentials {
1+
export type ILoginPayload = {
22
username: string;
33
password: string;
4-
}
4+
};

package-lock.json

Lines changed: 46 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/install-deps.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/bin/bash
2+
3+
npm i
4+
5+
cd frontend
6+
npm i
7+
cd ..
8+
9+
for package in backend/*; do
10+
cd "$package"
11+
npm i
12+
cd ../..
13+
done
14+

0 commit comments

Comments
 (0)