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
2 changes: 1 addition & 1 deletion client/src/api/auth/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { StudentType } from "../student/student.type";
import { TeacherType } from "../teacher/teacher.type";

export type Role = "student" | "teacher";
export type Role = "student" | "teacher" | "moderator";

export type RegisterFormTypes = {
firstName: string;
Expand Down
7 changes: 7 additions & 0 deletions client/src/api/moderator/moderator.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { LoginFormTypes } from "../auth/types.ts";
import { apiPublic } from "../api.ts";

export async function loginModeratorApi(data: LoginFormTypes) {
const res = await apiPublic.post("/api/moderator/auth/login", data);
return res.data as { accessToken: string };
}
75 changes: 41 additions & 34 deletions client/src/components/auth/loginForm/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,22 +61,24 @@ export const LoginForm = ({
type="password"
/>
</div>

<div className="auth-actions">
<div className="auth-actions-inner">
<Button
as={NavLink}
variant="link"
to={
role === "teacher"
? authRoutesVariables.recoveryTeacher
: authRoutesVariables.recoveryStudent
}
className="auth-link-underline"
type="button"
>
Forgot Password?
</Button>
{role !== "moderator" && (
<Button
as={NavLink}
variant="link"
to={
role === "teacher"
? authRoutesVariables.recoveryTeacher
: authRoutesVariables.recoveryStudent
}
className="auth-link-underline"
type="button"
>
Forgot Password?
</Button>
)}

<Button
variant="secondary"
size="auth"
Expand All @@ -85,27 +87,32 @@ export const LoginForm = ({
>
Sign In
</Button>
<Button
as={NavLink}
to={
role === "teacher"
? authRoutesVariables.registerTutor
: authRoutesVariables.registerStudent
}
variant="tertiary"
size="auth"
type="button"
>
Sign Up
</Button>
</div>

<div className="auth-divider">
<div className="auth-divider-line" />
<span className="auth-divider-text">Or</span>
<div className="auth-divider-line" />
{role !== "moderator" && (
<Button
as={NavLink}
to={
role === "teacher"
? authRoutesVariables.registerTutor
: authRoutesVariables.registerStudent
}
variant="tertiary"
size="auth"
type="button"
>
Sign Up
</Button>
)}
</div>
<GoogleAuthButton role={role} intent="login" />
{role !== "moderator" && (
<>
<div className="auth-divider">
<div className="auth-divider-line" />
<span className="auth-divider-text">Or</span>
<div className="auth-divider-line" />
</div>
<GoogleAuthButton role={role} intent="login" />
</>
)}
</div>
</div>
{loading && <Loader />}
Expand Down
31 changes: 31 additions & 0 deletions client/src/features/moderator/mutation/useModeratorLogin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import { useAuthSessionStore } from "../../../store/authSession.store.ts";
import { useNotificationStore } from "../../../store/notification.store.ts";
import { LoginFormTypes } from "../../../api/auth/types.ts";
import { queryKeys } from "../../queryKeys.ts";
import { getErrorMessage } from "../../../util/ErrorUtil.ts";
import { loginModeratorApi } from "../../../api/moderator/moderator.api.ts";

export function useLoginModeratorMutation() {
const qc = useQueryClient();
const navigate = useNavigate();
const setAccessToken = useAuthSessionStore((s) => s.setAccessToken);
const success = useNotificationStore((s) => s.success);
const notifyError = useNotificationStore((s) => s.error);

return useMutation({
mutationFn: (data: LoginFormTypes) => loginModeratorApi(data),
onSuccess: async ({ accessToken }) => {
setAccessToken(accessToken);
success("Successfully logged in");
localStorage.setItem("hadSession", "1");
navigate("/", { replace: true });
await qc.invalidateQueries({ queryKey: queryKeys.me });
},
onError: (error) => {
const msg = getErrorMessage(error);
notifyError(msg);
},
});
}
22 changes: 22 additions & 0 deletions client/src/pages/loginModeratorPage/loginModeratorPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { LoginFinalType } from "../../api/auth/types.ts";
import { LoginForm } from "../../components/auth/loginForm/LoginForm.tsx";
import { useLoginModeratorMutation } from "../../features/moderator/mutation/useModeratorLogin.ts";

export const LoginModeratorPage = () => {
const { mutateAsync, isPending } = useLoginModeratorMutation();

const onSubmit = async (data: LoginFinalType) => {
await mutateAsync({ ...data });
};

return (
<div className="auth-page">
<LoginForm
loading={isPending}
onSubmit={onSubmit}
title="Moderator"
role="moderator"
/>
</div>
);
};
6 changes: 5 additions & 1 deletion client/src/router/routesVariables/authRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { SignUpPage } from "../../pages/signUpPage/SignUpPage";
import { LoginPage } from "../../pages/loginPage/LoginPage";
import { RecoveryPage } from "../../pages/recoveryPage/RecoveryPage";
import { ResetPasswordPage } from "../../pages/resetPasswordPage/resetPasswordPage";
import { LoginModeratorPage } from "../../pages/loginModeratorPage/loginModeratorPage.tsx";

export const authRoutes: RouteObject[] = [
{
Expand All @@ -14,7 +15,10 @@ export const authRoutes: RouteObject[] = [
path: `${authRoutesVariables.loginTutor}`,
element: <LoginPage role="teacher" />,
},

{
path: `${authRoutesVariables.loginModerator}`,
element: <LoginModeratorPage />,
},
{
path: `${authRoutesVariables.registerStudent}`,
element: <SignUpPage role="student" />,
Expand Down
1 change: 1 addition & 0 deletions client/src/router/routesVariables/pathVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const authRoutesVariables = {
recoveryTeacher: "/recovery/teacher",

resetPassword: "/reset-password",
loginModerator: "/login/moderator",
};

export const chatRoutes = {
Expand Down
2 changes: 1 addition & 1 deletion server/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type Role = "student" | "teacher";
export type Role = "student" | "teacher" | "moderator";
export declare global {
namespace Express {
export interface Request {
Expand Down
2 changes: 2 additions & 0 deletions server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { streamRouter } from "./routes/streamRoute.js";
import { videoCallRouter } from "./routes/videoCallRoute.js";
import { subjectRouter } from "./routes/subjectRoute.js";
import { uploadRouter } from "./routes/uploadRoute.js";
import { moderatorRouter } from "./routes/moderatorRoute.js";

// Create an express server
const app = express();
Expand All @@ -32,6 +33,7 @@ app.use("/api/students", studentRouter);
app.use("/api/chat", chatRouter);
app.use("/api/stream", streamRouter);
app.use("/api/video-calls", videoCallRouter);
app.use("/api/moderator", moderatorRouter);
app.use("/api/upload", uploadRouter);
app.use(globalErrorMiddleware);

Expand Down
3 changes: 3 additions & 0 deletions server/src/composition/composition.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,7 @@ export const TYPES = {
//subjects
SubjectsController: Symbol.for("SubjectsController"),
SubjectsQuery: Symbol.for("SubjectsQuery"),
//moderator
ModeratorController: Symbol.for("ModeratorController"),
ModeratorQuery: Symbol.for("ModeratorQuery"),
};
7 changes: 7 additions & 0 deletions server/src/composition/compositionRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import { VideoCallQuery } from "../repositories/queryRepositories/videoCall.quer
import { VideoCallService } from "../services/video/videoCall.service.js";
import { SubjectsController } from "../controllers/subjects.controller.js";
import { SubjectsQuery } from "../repositories/queryRepositories/subjects.query.js";
import { ModeratorController } from "../controllers/moderator.controller.js";
import { ModeratorQuery } from "../repositories/queryRepositories/moderator.query.js";

export const container = new Container();

Expand Down Expand Up @@ -104,3 +106,8 @@ container
.to(VideoCallController);
container.bind<VideoCallQuery>(TYPES.VideoCallQuery).to(VideoCallQuery);
container.bind<VideoCallCommand>(TYPES.VideoCallCommand).to(VideoCallCommand);
//moderator
container
.bind<ModeratorController>(TYPES.ModeratorController)
.to(ModeratorController);
container.bind<ModeratorQuery>(TYPES.ModeratorQuery).to(ModeratorQuery);
30 changes: 22 additions & 8 deletions server/src/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { injectable } from "inversify";
import { RequestWithBody } from "../types/common.types.js";
import { RequestWithBody, UsersRole } from "../types/common.types.js";
import { StudentViewType } from "../types/student/student.types.js";
import { inject } from "inversify";
import { TYPES } from "../composition/composition.types.js";
Expand All @@ -16,14 +16,15 @@ import {
LoginType,
RegistrationType,
} from "../types/auth/auth.types.js";
import { Role } from "../../index.js";

import { ModeratorQuery } from "../repositories/queryRepositories/moderator.query.js";
import { HttpError } from "../utils/error.util.js";
@injectable()
export class AuthController {
constructor(
@inject(TYPES.StudentService) protected studentService: StudentService,
@inject(TYPES.TeacherService) protected teacherService: TeacherService,
@inject(TYPES.AuthService) protected authService: AuthService,
@inject(TYPES.ModeratorQuery) protected moderatorQuery: ModeratorQuery,
@inject(TYPES.JwtService) protected jwtService: JwtService,
@inject(TYPES.StudentQuery) protected studentQuery: StudentQuery,
@inject(TYPES.TeacherQuery) protected teacherQuery: TeacherQuery,
Expand Down Expand Up @@ -68,7 +69,7 @@ export class AuthController {
const { email, password, role } = req.body;
try {
const credentialCheck: Record<
Role,
UsersRole,
(e: string, p: string) => Promise<StudentViewType | TeacherViewType>
> = {
student: this.authService.checkAuthStudentCredentials.bind(
Expand Down Expand Up @@ -108,10 +109,17 @@ export class AuthController {
try {
const { userId, role } = req.auth!;

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

if (role === "student") {
me = await this.studentQuery.getStudentById(userId);
} else if (role === "teacher") {
me = await this.teacherQuery.getTeacherById(userId);
} else if (role === "moderator") {
me = await this.moderatorQuery.getModeratorById(userId);
} else {
return res.sendStatus(403);
}

if (!me) {
return res.sendStatus(401);
Expand Down Expand Up @@ -229,6 +237,12 @@ export class AuthController {
const auth = req.auth;
if (!auth) return res.sendStatus(401);

if (auth.role !== "student" && auth.role !== "teacher") {
throw new HttpError(
403,
"Password change is not allowed for this role",
);
}
await this.authService.changePasswordForAuthenticatedUser({
userId: auth.userId,
role: auth.role,
Expand Down
1 change: 0 additions & 1 deletion server/src/controllers/chat.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export class ChatController {
async getConversations(req: Request, res: Response, next: NextFunction) {
try {
const { userId, role } = req.auth!;

const items = await this.chatService.getConversationList({
userId,
role,
Expand Down
69 changes: 69 additions & 0 deletions server/src/controllers/moderator.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { TYPES } from "../composition/composition.types.js";
import { NextFunction, Request, Response } from "express";
import { inject, injectable } from "inversify";
import { TeacherService } from "../services/teacher/teacher.service.js";
import { RequestWithParamsAndBody } from "../types/common.types.js";
import { TeacherStatus } from "../db/schemes/types/teacher.types.js";
import { AuthService } from "../services/auth/auth.service.js";
import { JwtService } from "../services/jwt/jwt.service.js";

@injectable()
export class ModeratorController {
constructor(
@inject(TYPES.TeacherService) private teacherService: TeacherService,
@inject(TYPES.AuthService) protected authService: AuthService,
@inject(TYPES.JwtService) protected jwtService: JwtService,
) {}

async changeTeacherStatus(
req: RequestWithParamsAndBody<{ id: string }, { status: TeacherStatus }>,
res: Response,
next: NextFunction,
) {
const { status } = req.body;
const { id } = req.params;
Comment thread
Dmytro-Doronin marked this conversation as resolved.
try {
await this.teacherService.changeTeacherStatus({ id, status });
return res.sendStatus(204);
} catch (err) {
return next(err);
}
}

async loginModeratorController(
req: Request,
res: Response,
next: NextFunction,
) {
const { email, password } = req.body;

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

const accessToken = this.jwtService.createJWTAccessToken({
userId: user.id,
role: user.role,
});

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

res.cookie("refreshToken", refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
path: "/",
maxAge: 2 * 60 * 60 * 1000,
sameSite: "lax",
});
res.status(200).send({ accessToken });
return;
} catch (error) {
next(error);
}
}
}
4 changes: 3 additions & 1 deletion server/src/controllers/stream.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ export class StreamController {
if (!auth) return res.sendStatus(401);

const { userId, role } = auth;

if (role !== "student" && role !== "teacher") {
return res.sendStatus(403);
}
const streamData = createStreamToken({ userId, role });
return res.status(200).json(streamData);
} catch (error) {
Expand Down
Loading
Loading