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
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export function useLoginMutation(role: Role) {
mutationFn,
onSuccess: async ({ accessToken }) => {
setAccessToken(accessToken);
navigate("/", { replace: true });
localStorage.setItem("hadSession", "1");
navigate("/", { replace: true });
await qc.invalidateQueries({ queryKey: queryKeys.me });
},
onError: (error) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { queryKeys } from "../../queryKeys";
import { getErrorMessage } from "../../../util/ErrorUtil";
import { RegisterFinalType, Role } from "../../../api/auth/types";

export const useRegisterStudentMutation = (role: Role) => {
export const useRegisterMutation = (role: Role) => {
const qc = useQueryClient();

const mutationFn = (data: RegisterFinalType) => {
Expand Down
1 change: 0 additions & 1 deletion client/src/features/auth/query/useAuthInit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export const useAuthInit = () => {
clearSession();
return;
}

(async () => {
try {
const { accessToken } = await refreshApi();
Expand Down
2 changes: 1 addition & 1 deletion client/src/pages/loginPage/LoginPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { LoginFinalType, Role } from "../../api/auth/types";
import { LoginForm } from "../../components/auth/loginForm/LoginForm";
import { useLoginMutation } from "../../features/auth/mutations/useLoginStudentMutation";
import { useLoginMutation } from "../../features/auth/mutations/useLoginMutation";

export const LoginPage = ({ role }: { role: Role }) => {
const { mutateAsync, isPending } = useLoginMutation(role);
Expand Down
4 changes: 2 additions & 2 deletions client/src/pages/signUpPage/SignUpPage.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { SignUpForm } from "../../components/auth/signUpForm/SignUpForm";
import { RegisterFinalType, Role } from "../../api/auth/types";
import { useRegisterStudentMutation } from "../../features/auth/mutations/useRegisterStudentMutation";
import { useRegisterMutation } from "../../features/auth/mutations/useRegisterMutation";

export const SignUpPage = ({ role }: { role: Role }) => {
const { mutateAsync, isPending } = useRegisterStudentMutation(role);
const { mutateAsync, isPending } = useRegisterMutation(role);

const onSubmit = (data: RegisterFinalType) => {
mutateAsync({ ...data, role });
Expand Down
1 change: 1 addition & 0 deletions server/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export declare global {
namespace Express {
export interface Request {
auth?: { userId: string; role: Role };
refresh?: { token: string; payload: RefreshTokenPayload };
}
}
}
4 changes: 3 additions & 1 deletion server/src/composition/composition.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ export const TYPES = {
JwtService: Symbol.for("JwtService"),
//middlewares
AuthMiddleware: Symbol.for("AuthMiddleware"),
VerifyMiddleware: Symbol.for("VerifyMiddleware"),
RefreshTokenMiddleware: Symbol.for("RefreshTokenMiddleware"),
//auth
AuthController: Symbol.for("AuthController"),
AuthService: Symbol.for("AuthService"),
//refresh
RefreshSessionRepository: Symbol.for("RefreshSessionRepository"),
};
11 changes: 9 additions & 2 deletions server/src/composition/compositionRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import { TeacherController } from "../controllers/teacher.controller.js";
import { JwtService } from "../services/jwt/jwt.service.js";
import { AuthMiddleware } from "../middlewares/authMiddlewareWithBearer.js";
import { AuthService } from "../services/auth/auth.service.js";
import { VerifyMiddleware } from "../middlewares/verifyToken.middleware.js";
import { RefreshSessionRepository } from "../repositories/commandRepositories/refreshSession.repository.js";
import { RefreshTokenMiddleware } from "../middlewares/refreshToken.middleware.js";

export const container = new Container();

Expand All @@ -22,9 +23,15 @@ container.bind<AuthController>(TYPES.AuthController).to(AuthController);
container.bind<AuthService>(TYPES.AuthService).to(AuthService);
//jwt
container.bind<JwtService>(TYPES.JwtService).to(JwtService);
//refresh
container
.bind<RefreshSessionRepository>(TYPES.RefreshSessionRepository)
.to(RefreshSessionRepository);
//middleware
container.bind<AuthMiddleware>(TYPES.AuthMiddleware).to(AuthMiddleware);
container.bind<VerifyMiddleware>(TYPES.VerifyMiddleware).to(VerifyMiddleware);
container
.bind<RefreshTokenMiddleware>(TYPES.RefreshTokenMiddleware)
.to(RefreshTokenMiddleware);

// student
container.bind<StudentCommand>(TYPES.StudentCommand).to(StudentCommand);
Expand Down
55 changes: 34 additions & 21 deletions server/src/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,17 +80,26 @@ export class AuthController {
const { email, password } = req.body;

try {
const user = await this.authService.checkAuthStudentCredentials(
const student = await this.authService.checkAuthStudentCredentials(
email,
password,
);

const accessToken = await this.jwtService.createJWTAccessToken(user);
const refreshToken = await this.jwtService.createJWTRefreshToken(user);
const accessToken = this.jwtService.createJWTAccessToken({
userId: student.id,
role: "student",
});

const { refreshToken } = await this.authService.createRefreshSession({
userId: student.id,
role: "student",
});

res.cookie("refreshToken", refreshToken, {
httpOnly: true,
secure: true,
path: "/api/auth",
maxAge: 2 * 60 * 60 * 1000,
});
res.status(200).send({ accessToken });
return;
Expand All @@ -112,12 +121,21 @@ export class AuthController {
password,
);

const accessToken = await this.jwtService.createJWTAccessToken(teacher);
const refreshToken = await this.jwtService.createJWTRefreshToken(teacher);
const accessToken = this.jwtService.createJWTAccessToken({
userId: teacher.id,
role: "teacher",
});

const { refreshToken } = await this.authService.createRefreshSession({
userId: teacher.id,
role: "teacher",
});

res.cookie("refreshToken", refreshToken, {
httpOnly: true,
secure: true,
path: "/api/auth",
maxAge: 2 * 60 * 60 * 1000,
});
res.status(200).send({ accessToken });
return;
Expand Down Expand Up @@ -147,26 +165,21 @@ export class AuthController {

async refreshController(req: Request, res: Response, next: NextFunction) {
try {
const { userId, role } = req.auth!;

const user =
role === "student"
? await this.studentQuery.getStudentById(userId)
: await this.teacherQuery.getTeacherById(userId);

if (!user) {
return res.sendStatus(401);
}

const accessToken = await this.jwtService.createJWTAccessToken(user);
const refreshToken = await this.jwtService.createJWTRefreshToken(user);

res.cookie("refreshToken", refreshToken, {
const { token, payload } = req.refresh!;
const { newAccessToken, newRefreshToken } =
await this.authService.rotateRefreshToken({
refreshToken: token,
payload,
});

res.cookie("refreshToken", newRefreshToken, {
httpOnly: true,
secure: true,
path: "/api/auth",
maxAge: 2 * 60 * 60 * 1000,
});

return res.status(200).send({ accessToken });
return res.status(200).send({ accessToken: newAccessToken });
} catch (e) {
return next(e);
}
Expand Down
23 changes: 23 additions & 0 deletions server/src/db/schemes/session.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import mongoose from "mongoose";
import { RefreshSessionDB } from "./types/session.types.js";

const RefreshSessionSchema = new mongoose.Schema<RefreshSessionDB>(
{
id: { type: String, required: true, unique: true, index: true },
userId: { type: String, required: true, index: true },
role: { type: String, required: true, enum: ["teacher", "student"] },
refreshTokenHash: { type: String, required: true },

expiresAt: { type: Date, required: true },
createdAt: { type: Date, default: Date.now },

revokedAt: { type: Date, default: null },
replacedBySessionId: { type: String, default: null },
},
{ versionKey: false },
);
RefreshSessionSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });

export const RefreshSessionModel =
mongoose.models.RefreshSession ||
mongoose.model<RefreshSessionDB>("RefreshSession", RefreshSessionSchema);
13 changes: 13 additions & 0 deletions server/src/db/schemes/types/session.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type RefreshSessionDB = {
id: string;
userId: string;
role: "teacher" | "student";
refreshTokenHash: string;
expiresAt: Date;
createdAt: Date;
revokedAt: Date | null;
replacedBySessionId: string | null;
};
export type RefreshSessionPatch = Partial<
Omit<RefreshSessionDB, "id" | "createdAt">
>;
2 changes: 1 addition & 1 deletion server/src/middlewares/authMiddlewareWithBearer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class AuthMiddleware {
}
const token = req.headers.authorization.split(" ")[1];

const payload = await this.jwtService.verifyToken(token);
const payload = this.jwtService.verifyAccessToken(token);

if (!payload?.userId || !payload?.role) {
res.sendStatus(401);
Expand Down
15 changes: 9 additions & 6 deletions server/src/middlewares/global.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@ export const globalErrorMiddleware: ErrorRequestHandler = (
res,
next,
) => {
logError(err);

if (res.headersSent) return next(err);
if (res.headersSent) {
return next(err);
}

if (err instanceof HttpError) {
return res.status(err.statusCode).json({
message: err.message,
});
if (err.statusCode >= 500) {
logError(err);
}

return res.status(err.statusCode).json({ message: err.message });
}

logError(err);
return res.status(500).json({ message: "Internal server error" });
};
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import { inject, injectable } from "inversify";
import { TYPES } from "../composition/composition.types.js";
import { inject, injectable } from "inversify";
import { JwtService } from "../services/jwt/jwt.service.js";
import { NextFunction, Request, Response } from "express";

@injectable()
export class VerifyMiddleware {
export class RefreshTokenMiddleware {
constructor(@inject(TYPES.JwtService) private jwtService: JwtService) {}

verify = async (req: Request, res: Response, next: NextFunction) => {
handle = (req: Request, res: Response, next: NextFunction) => {
const token = req.cookies?.refreshToken;
if (!token) {
return res.sendStatus(401);
}

const payload = await this.jwtService.verifyToken(token);

if (!payload?.userId || !payload?.role) {
const payload = this.jwtService.verifyRefreshToken(token);
if (!payload) {
return res.sendStatus(401);
}
req.auth = { userId: payload.userId, role: payload.role };

req.refresh = { token, payload };
return next();
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { RefreshSessionModel } from "../../db/schemes/session.schema.js";
import {
RefreshSessionDB,
RefreshSessionPatch,
} from "../../db/schemes/types/session.types.js";
import { injectable } from "inversify";
import { HttpError } from "../../utils/error.util.js";

@injectable()
export class RefreshSessionRepository {
async create(session: RefreshSessionDB): Promise<void> {
try {
await RefreshSessionModel.create(session);
} catch (err: unknown) {
throw new HttpError(500, "Session was not created", { cause: err });
}
}

async findById(sessionId: string): Promise<RefreshSessionDB | null> {
try {
return await RefreshSessionModel.findOne({ id: sessionId }).lean();
} catch (err: unknown) {
throw new HttpError(500, "Session was not found", { cause: err });
}
}

async revoke(
sessionId: string,
revokedAt: Date = new Date(),
): Promise<boolean> {
try {
const res = await RefreshSessionModel.updateOne(
{ id: sessionId, revokedAt: null },
{ $set: { revokedAt } },
);
return res.modifiedCount === 1;
} catch (err: unknown) {
throw new HttpError(500, "Session was not updated", { cause: err });
}
}

async revokeAllForUser(
userId: string,
role?: RefreshSessionDB["role"],
revokedAt: Date = new Date(),
): Promise<number> {
try {
const filter: Record<string, unknown> = { userId, revokedAt: null };
if (role) {
filter.role = role;
}

const res = await RefreshSessionModel.updateMany(filter, {
$set: { revokedAt },
});

return res.modifiedCount;
} catch (err: unknown) {
throw new HttpError(500, "Sessions were not updated", { cause: err });
}
}

updateById(id: string, patch: RefreshSessionPatch) {
return RefreshSessionModel.updateOne({ id }, { $set: patch });
}
async replace(oldSessionId: string, newSessionId: string): Promise<boolean> {
try {
const res = await RefreshSessionModel.updateOne(
{ id: oldSessionId, revokedAt: null },
{ $set: { revokedAt: new Date(), replacedBySessionId: newSessionId } },
);
return res.modifiedCount === 1;
} catch (err: unknown) {
throw new HttpError(500, "Session was not updated", { cause: err });
}
}

async deleteExpiredNow(): Promise<number> {
try {
const res = await RefreshSessionModel.deleteMany({
expiresAt: { $lte: new Date() },
});
return res.deletedCount ?? 0;
} catch (err: unknown) {
throw new HttpError(500, "Session was not deleted", { cause: err });
}
}
}
Loading
Loading