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
17 changes: 16 additions & 1 deletion apps/backend/src/modules/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
import { Request, Response } from "express";
import { StatusCodes } from "http-status-codes";
import { sendOtp, verifyOtp, refreshAccessToken, logout } from "./auth.service";
import {
sendOtp,
checkOtpStatus,
verifyOtp,
refreshAccessToken,
logout,
} from "./auth.service";

const sendOtpController = async (req: Request, res: Response) => {
const { phoneNumber } = req.body;
await sendOtp(phoneNumber);
res.status(StatusCodes.OK).json({ message: "OTP was sent successfully." });
};

const checkOtpStatusController = async (req: Request, res: Response) => {
const { phoneNumber } = req.body;
await checkOtpStatus(phoneNumber);
res
.status(StatusCodes.OK)
.json({ message: "OTP status checked successfully." });
};

const verifyOtpController = async (req: Request, res: Response) => {
const { phoneNumber, otp } = req.body;
const { accessToken, refreshToken } = await verifyOtp(phoneNumber, otp);
res.cookie(process.env.JWT_ACCESS_TOKEN_COOKIE_KEY, accessToken, {

Check warning on line 28 in apps/backend/src/modules/auth/auth.controller.ts

View workflow job for this annotation

GitHub Actions / quality-checks / Lint

JWT_ACCESS_TOKEN_COOKIE_KEY is not listed as a dependency in the root turbo.json or workspace () turbo.json
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: process.env.JWT_ACCESS_TOKEN_MAX_AGE,

Check warning on line 32 in apps/backend/src/modules/auth/auth.controller.ts

View workflow job for this annotation

GitHub Actions / quality-checks / Lint

JWT_ACCESS_TOKEN_MAX_AGE is not listed as a dependency in the root turbo.json or workspace () turbo.json
});
res.cookie(process.env.JWT_REFRESH_TOKEN_COOKIE_KEY, refreshToken, {
httpOnly: true,
Expand All @@ -29,11 +43,11 @@
const refreshTokenController = async (req: Request, res: Response) => {
const refreshToken = req.cookies[process.env.JWT_REFRESH_TOKEN_COOKIE_KEY];
const accessToken = await refreshAccessToken(refreshToken);
res.cookie(process.env.JWT_ACCESS_TOKEN_COOKIE_KEY, accessToken, {

Check warning on line 46 in apps/backend/src/modules/auth/auth.controller.ts

View workflow job for this annotation

GitHub Actions / quality-checks / Lint

JWT_ACCESS_TOKEN_COOKIE_KEY is not listed as a dependency in the root turbo.json or workspace () turbo.json
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: process.env.JWT_ACCESS_TOKEN_MAX_AGE,

Check warning on line 50 in apps/backend/src/modules/auth/auth.controller.ts

View workflow job for this annotation

GitHub Actions / quality-checks / Lint

JWT_ACCESS_TOKEN_MAX_AGE is not listed as a dependency in the root turbo.json or workspace () turbo.json
});
res.status(StatusCodes.OK).json({ accessToken });
};
Expand All @@ -41,13 +55,14 @@
const logoutController = async (req: Request, res: Response) => {
const refreshToken = req.cookies[process.env.JWT_REFRESH_TOKEN_COOKIE_KEY];
await logout(refreshToken);
res.clearCookie(process.env.JWT_ACCESS_TOKEN_COOKIE_KEY);

Check warning on line 58 in apps/backend/src/modules/auth/auth.controller.ts

View workflow job for this annotation

GitHub Actions / quality-checks / Lint

JWT_ACCESS_TOKEN_COOKIE_KEY is not listed as a dependency in the root turbo.json or workspace () turbo.json
res.clearCookie(process.env.JWT_REFRESH_TOKEN_COOKIE_KEY);
res.status(StatusCodes.OK).json({ message: "Logged out successfully." });
};

export {
sendOtpController,
checkOtpStatusController,
verifyOtpController,
refreshTokenController,
logoutController,
Expand Down
36 changes: 32 additions & 4 deletions apps/backend/src/modules/auth/auth.route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Router } from "express";
import validateHandler from "@repo/backend/middlewares/validation";
import rateLimitHandler from "@repo/backend/middlewares/rate-limit";
import {
sendOtpController,
checkOtpStatusController,
verifyOtpController,
refreshTokenController,
logoutController,
Expand All @@ -10,22 +12,48 @@ import {
sendOtpSchema,
verifyOtpSchema,
refreshTokenSchema,
statusOtpSchema,
} from "./auth.schema";

const authRouter = Router();

authRouter.post("/send-otp", validateHandler(sendOtpSchema), sendOtpController);
const sendOtpLimiter = rateLimitHandler({
endpoint: "/otp/send",
timeSpan: "5m",
limit: 5,
message: "Too many SMS messages have been sent to this number recently",
});

authRouter.post(
"/verify-otp",
"/otp/send",
sendOtpLimiter,
validateHandler(sendOtpSchema),
sendOtpController
);

const verifyOtplimiter = rateLimitHandler({
endpoint: "/otp/verify",
timeSpan: "5m",
limit: 5,
});

authRouter.post(
"/otp/status",
validateHandler(statusOtpSchema),
checkOtpStatusController
);

authRouter.post(
"/otp/verify",
verifyOtplimiter,
validateHandler(verifyOtpSchema),
verifyOtpController,
verifyOtpController
);

authRouter.get(
"/refresh-token",
validateHandler(refreshTokenSchema),
refreshTokenController,
refreshTokenController
);

authRouter.get("/logout", logoutController);
Expand Down
8 changes: 7 additions & 1 deletion apps/backend/src/modules/auth/auth.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ const sendOtpSchema = z.object({
}),
});

const statusOtpSchema = z.object({
body: z.object({
phoneNumber: z.string().min(10).max(15),
}),
});

const verifyOtpSchema = sendOtpSchema.extend({
body: z.object({
otp: z.string().min(6).max(6),
Expand All @@ -18,4 +24,4 @@ const refreshTokenSchema = z.object({
}),
});

export { sendOtpSchema, verifyOtpSchema, refreshTokenSchema };
export { sendOtpSchema, statusOtpSchema, verifyOtpSchema, refreshTokenSchema };
35 changes: 25 additions & 10 deletions apps/backend/src/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {

const twilioClient = twilio(
process.env.TWILIO_ACCOUNT_SID,
process.env.TWILIO_AUTH_TOKEN,
process.env.TWILIO_AUTH_TOKEN
);

const sendOtp = async (phoneNumber: string) => {
Expand All @@ -28,7 +28,7 @@ const sendOtp = async (phoneNumber: string) => {
expiresAt,
})
.onConflictDoUpdate({
target: otps.phoneNumber, // The unique constraint column
target: otps.phoneNumber,
set: { otp, expiresAt, updatedAt: new Date() },
});

Expand All @@ -39,6 +39,18 @@ const sendOtp = async (phoneNumber: string) => {
});
};

const checkOtpStatus = async (phoneNumber: string) => {
const otp = await db.query.otps.findFirst({
where: eq(otps.phoneNumber, phoneNumber),
});

if (!otp || otp.expiresAt < new Date()) {
throw new AuthError("OTP expired or invalid.");
}

return true;
};

const verifyOtp = async (phoneNumber: string, otp: string) => {
const existingOtp = await db.query.otps.findFirst({
where: eq(otps.phoneNumber, phoneNumber),
Expand Down Expand Up @@ -71,13 +83,16 @@ const verifyOtp = async (phoneNumber: string, otp: string) => {
const refreshToken = jwtSignRefreshToken({ userId: user.id });

const hashedToken = await argon2.hash(refreshToken);

await db.delete(refreshTokens).where(eq(refreshTokens.userId, user.id));

await db.insert(refreshTokens).values({
userId: user.id,
token: hashedToken,
});
await db
.insert(refreshTokens)
.values({
userId: user.id,
token: hashedToken,
})
.onConflictDoUpdate({
target: refreshTokens.userId,
set: { token: hashedToken, updatedAt: new Date() },
});

return { accessToken, refreshToken };
};
Expand Down Expand Up @@ -120,4 +135,4 @@ const logout = async (refreshToken: string) => {
}
};

export { sendOtp, verifyOtp, refreshAccessToken, logout };
export { sendOtp, checkOtpStatus, verifyOtp, refreshAccessToken, logout };
16 changes: 8 additions & 8 deletions apps/backend/src/modules/auth/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ describe("Auth API", () => {
process.env.JWT_REFRESH_TOKEN_COOKIE_KEY = "refreshToken";
});

describe("POST /send-otp", () => {
describe("POST /otp/send", () => {
it("should send an OTP successfully", async () => {
const phoneNumber = "1234567890";

const res = await supertest(createServer())
.post("/api/v1/auth/send-otp")
.post("/api/v1/auth/otp/send")
.set("Content-Type", "application/json")
.send({ phoneNumber });

Expand All @@ -56,14 +56,14 @@ describe("Auth API", () => {

it("should return 400 if phoneNumber is missing", async () => {
const res = await supertest(createServer())
.post("/api/v1/auth/send-otp")
.post("/api/v1/auth/otp/send")
.send({});

expect(res.status).toBe(400);
});
});

describe("POST /auth/verify-otp", () => {
describe("POST /auth/otp/verify", () => {
it("should verify OTP, create user if not exists, and return tokens", async () => {
const phoneNumber = "1234567890";
const otpCode = "123456";
Expand All @@ -76,7 +76,7 @@ describe("Auth API", () => {
});

const response = await supertest(createServer())
.post("/api/v1/auth/verify-otp")
.post("/api/v1/auth/otp/verify")
.send({ phoneNumber, otp: otpCode });

expect(response.status).toBe(201);
Expand Down Expand Up @@ -118,7 +118,7 @@ describe("Auth API", () => {
});

const response = await supertest(createServer())
.post("/api/v1/auth/verify-otp")
.post("/api/v1/auth/otp/verify")
.send({ phoneNumber, otp: otpCode });

expect(response.status).toBe(401);
Expand All @@ -137,7 +137,7 @@ describe("Auth API", () => {
});

const response = await supertest(createServer())
.post("/api/v1/auth/verify-otp")
.post("/api/v1/auth/otp/verify")
.send({ phoneNumber, otp: otpCode });

expect(response.status).toBe(401);
Expand All @@ -156,7 +156,7 @@ describe("Auth API", () => {
});

const response = await supertest(createServer())
.post("/api/v1/auth/verify-otp")
.post("/api/v1/auth/otp/verify")
.send({ phoneNumber, otp: "123456" });

expect(response.status).toBe(401);
Expand Down
1 change: 1 addition & 0 deletions packages/database/src/schema/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const otps = pgTable("otps", {
export const refreshTokens = pgTable("refresh_tokens", {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer()
.unique()
.notNull()
.references(() => users.id, { onDelete: "cascade", onUpdate: "cascade" }),
token: text().notNull().unique(),
Expand Down
Loading