From a9f3c4c735f7ba189294b82b388e51506ad861bd Mon Sep 17 00:00:00 2001 From: saachibm <123594505+saachibm@users.noreply.github.com> Date: Sun, 3 Dec 2023 11:48:07 -0700 Subject: [PATCH 001/137] feat: add supporting entity definitions - a Teacher can have multiple Classrooms - a Classroom can have multiple Students - Students are one-to-one with Users which lets them participate in Games - Teachers are one-to-one with Users which lets them sign into the website (may need to revisit this part of the schema) Co-authored-by: Tkawamura02 Co-authored-by: Allen Lee Co-authored-by: Scott Foster Co-authored-by: Sabrina Nelson --- server/src/entity/Classroom.ts | 21 +++++++++++++++++++++ server/src/entity/Student.ts | 22 ++++++++++++++++++++++ server/src/entity/Teacher.ts | 19 +++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 server/src/entity/Classroom.ts create mode 100644 server/src/entity/Student.ts create mode 100644 server/src/entity/Teacher.ts diff --git a/server/src/entity/Classroom.ts b/server/src/entity/Classroom.ts new file mode 100644 index 000000000..9fc65afb7 --- /dev/null +++ b/server/src/entity/Classroom.ts @@ -0,0 +1,21 @@ +import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm"; +import { Student } from "./Student"; +import { Teacher } from "./Teacher"; + +@Entity() +export class Classroom { + @PrimaryGeneratedColumn() + id!: number; + + @OneToMany(type => Student, student => student.classroom) + students!: Student; + + @ManyToOne(type => Teacher, teacher => teacher.classrooms) + teacher!: Teacher; + + @Column() + teacherId!: number; + + @Column() + authToken!: string; +} diff --git a/server/src/entity/Student.ts b/server/src/entity/Student.ts new file mode 100644 index 000000000..647344163 --- /dev/null +++ b/server/src/entity/Student.ts @@ -0,0 +1,22 @@ +import { Column, JoinColumn, Entity, ManyToOne, OneToOne, PrimaryGeneratedColumn } from "typeorm"; +import { User } from "@port-of-mars/server/entity/User"; +import { Classroom } from "./Classroom"; + +@Entity() +export class Student { + @PrimaryGeneratedColumn() + id!: number; + + @OneToOne(type => User, user => user.student, { nullable: false }) + @JoinColumn() + user!: User; + + @Column() + userId!: number; + + @ManyToOne(type => Classroom, classroom => classroom.students) + classroom!: Classroom; + + @Column() + classroomId!: number; +} diff --git a/server/src/entity/Teacher.ts b/server/src/entity/Teacher.ts new file mode 100644 index 000000000..70c2a4bb1 --- /dev/null +++ b/server/src/entity/Teacher.ts @@ -0,0 +1,19 @@ +import { Column, JoinColumn, Entity, OneToMany, OneToOne, PrimaryGeneratedColumn } from "typeorm"; +import { Classroom } from "./Classroom"; +import { User } from "@port-of-mars/server/entity/User"; + +@Entity() +export class Teacher { + @PrimaryGeneratedColumn() + id!: number; + + @OneToOne(type => User, user => user.teacher, { nullable: false }) + @JoinColumn() + user!: User; + + @Column() + userId!: number; + + @OneToMany(type => Classroom, classroom => classroom.teacher) + classrooms!: Classroom; +} From 8880f9b0ab7e6077e79d6db3afdbf8e38557da12 Mon Sep 17 00:00:00 2001 From: sgfost Date: Mon, 29 Jan 2024 16:07:45 -0700 Subject: [PATCH 002/137] chore: add migration for education mode entities --- server/src/entity/Student.ts | 2 +- server/src/entity/Teacher.ts | 2 +- .../1706569597345-AddEducationModels.ts | 26 +++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 server/src/migration/1706569597345-AddEducationModels.ts diff --git a/server/src/entity/Student.ts b/server/src/entity/Student.ts index 647344163..218e7bec6 100644 --- a/server/src/entity/Student.ts +++ b/server/src/entity/Student.ts @@ -7,7 +7,7 @@ export class Student { @PrimaryGeneratedColumn() id!: number; - @OneToOne(type => User, user => user.student, { nullable: false }) + @OneToOne(type => User, { nullable: false }) @JoinColumn() user!: User; diff --git a/server/src/entity/Teacher.ts b/server/src/entity/Teacher.ts index 70c2a4bb1..7ae381496 100644 --- a/server/src/entity/Teacher.ts +++ b/server/src/entity/Teacher.ts @@ -7,7 +7,7 @@ export class Teacher { @PrimaryGeneratedColumn() id!: number; - @OneToOne(type => User, user => user.teacher, { nullable: false }) + @OneToOne(type => User, user => user, { nullable: false }) @JoinColumn() user!: User; diff --git a/server/src/migration/1706569597345-AddEducationModels.ts b/server/src/migration/1706569597345-AddEducationModels.ts new file mode 100644 index 000000000..355eba084 --- /dev/null +++ b/server/src/migration/1706569597345-AddEducationModels.ts @@ -0,0 +1,26 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class AddEducationModels1706569597345 implements MigrationInterface { + name = 'AddEducationModels1706569597345' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "student" ("id" SERIAL NOT NULL, "userId" integer NOT NULL, "classroomId" integer NOT NULL, CONSTRAINT "REL_b35463776b4a11a3df3c30d920" UNIQUE ("userId"), CONSTRAINT "PK_3d8016e1cb58429474a3c041904" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "teacher" ("id" SERIAL NOT NULL, "userId" integer NOT NULL, CONSTRAINT "REL_4f596730e16ee49d9b081b5d8e" UNIQUE ("userId"), CONSTRAINT "PK_2f807294148612a9751dacf1026" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "classroom" ("id" SERIAL NOT NULL, "teacherId" integer NOT NULL, "authToken" character varying NOT NULL, CONSTRAINT "PK_729f896c8b7b96ddf10c341e6ff" PRIMARY KEY ("id"))`); + await queryRunner.query(`ALTER TABLE "student" ADD CONSTRAINT "FK_b35463776b4a11a3df3c30d920a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "student" ADD CONSTRAINT "FK_426224f5597213259b1d58fc0f4" FOREIGN KEY ("classroomId") REFERENCES "classroom"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "teacher" ADD CONSTRAINT "FK_4f596730e16ee49d9b081b5d8e5" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "classroom" ADD CONSTRAINT "FK_2b3c1fa62762d7d0e828c139130" FOREIGN KEY ("teacherId") REFERENCES "teacher"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "classroom" DROP CONSTRAINT "FK_2b3c1fa62762d7d0e828c139130"`); + await queryRunner.query(`ALTER TABLE "teacher" DROP CONSTRAINT "FK_4f596730e16ee49d9b081b5d8e5"`); + await queryRunner.query(`ALTER TABLE "student" DROP CONSTRAINT "FK_426224f5597213259b1d58fc0f4"`); + await queryRunner.query(`ALTER TABLE "student" DROP CONSTRAINT "FK_b35463776b4a11a3df3c30d920a"`); + await queryRunner.query(`DROP TABLE "classroom"`); + await queryRunner.query(`DROP TABLE "teacher"`); + await queryRunner.query(`DROP TABLE "student"`); + } + +} From 2d09e8fbbc92f25330e4ccd475c793dbfd33f035 Mon Sep 17 00:00:00 2001 From: sgfost Date: Mon, 5 Feb 2024 15:28:38 -0700 Subject: [PATCH 003/137] feat(WIP): set up initial client-side routes for edu-mode pending a feature flag, these routes/components will be part of an entirely different layout Co-authored-by: sbmota Co-authored-by: Allen Lee Co-authored-by: Sabrina Nelson --- client/src/components/global/Navbar.vue | 27 +++++ client/src/router.ts | 6 ++ client/src/views/ClassroomLobby.vue | 132 ++++++++++++++++++++++++ client/src/views/StudentLogin.vue | 104 +++++++++++++++++++ shared/src/routes.ts | 31 +++++- 5 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 client/src/views/ClassroomLobby.vue create mode 100644 client/src/views/StudentLogin.vue diff --git a/client/src/components/global/Navbar.vue b/client/src/components/global/Navbar.vue index 3684dce49..565fcdc37 100644 --- a/client/src/components/global/Navbar.vue +++ b/client/src/components/global/Navbar.vue @@ -65,6 +65,17 @@ > Join Mars Madness + + + Classroom lobby + + Sign In + + Sign In (Student) + + @@ -111,6 +132,9 @@ import { LEADERBOARD_PAGE, PROFILE_PAGE, TOURNAMENT_DASHBOARD_PAGE, + STUDENT_LOGIN_PAGE, + EDUCATOR_LOGIN_PAGE, + CLASSROOM_LOBBY_PAGE, } from "@port-of-mars/shared/routes"; @Component({}) @@ -134,6 +158,9 @@ export default class Navbar extends Vue { tournamentDashboard = { name: TOURNAMENT_DASHBOARD_PAGE }; freePlayLobby = { name: FREE_PLAY_LOBBY_PAGE }; profile = { name: PROFILE_PAGE }; + studentLogin = { name: STUDENT_LOGIN_PAGE }; + educatorLogin = { name: EDUCATOR_LOGIN_PAGE }; + classroomLobby = { name: CLASSROOM_LOBBY_PAGE }; get username() { return this.$tstore.state.user.username; diff --git a/client/src/router.ts b/client/src/router.ts index a005fbfd7..8e35fedd1 100644 --- a/client/src/router.ts +++ b/client/src/router.ts @@ -22,6 +22,8 @@ import Manual from "@port-of-mars/client/views/Manual.vue"; import Home from "@port-of-mars/client/views/Home.vue"; import Privacy from "@port-of-mars/client/views/Privacy.vue"; import Profile from "@port-of-mars/client/views/Profile.vue"; +import StudentLogin from "@port-of-mars/client/views/StudentLogin.vue"; +import ClassroomLobby from "@port-of-mars/client/views/ClassroomLobby.vue"; import store from "@port-of-mars/client/store"; import { ADMIN_PAGE, @@ -41,6 +43,8 @@ import { ABOUT_PAGE, PRIVACY_PAGE, PROFILE_PAGE, + STUDENT_LOGIN_PAGE, + CLASSROOM_LOBBY_PAGE, } from "@port-of-mars/shared/routes"; Vue.use(VueRouter); @@ -91,6 +95,8 @@ const router = new VueRouter({ { ...PAGE_META[ABOUT_PAGE], component: Home }, { ...PAGE_META[PRIVACY_PAGE], component: Privacy }, { ...PAGE_META[PROFILE_PAGE], component: Profile }, + { ...PAGE_META[STUDENT_LOGIN_PAGE], component: StudentLogin }, + { ...PAGE_META[CLASSROOM_LOBBY_PAGE], component: ClassroomLobby }, ], }); diff --git a/client/src/views/ClassroomLobby.vue b/client/src/views/ClassroomLobby.vue new file mode 100644 index 000000000..81d076a70 --- /dev/null +++ b/client/src/views/ClassroomLobby.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/client/src/views/StudentLogin.vue b/client/src/views/StudentLogin.vue new file mode 100644 index 000000000..658c7a9bc --- /dev/null +++ b/client/src/views/StudentLogin.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/shared/src/routes.ts b/shared/src/routes.ts index 7682f8ed0..0e18818ed 100644 --- a/shared/src/routes.ts +++ b/shared/src/routes.ts @@ -14,6 +14,9 @@ export const HOME_PAGE = "Home" as const; export const ABOUT_PAGE = "About" as const; export const PRIVACY_PAGE = "Privacy" as const; export const PROFILE_PAGE = "Profile" as const; +export const STUDENT_LOGIN_PAGE = "StudentLogin" as const; +export const EDUCATOR_LOGIN_PAGE = "EducatorLogin" as const; +export const CLASSROOM_LOBBY_PAGE = "ClassroomLobby" as const; export type Page = | "Admin" @@ -31,7 +34,10 @@ export type Page = | "Profile" | "Verify" | "Manual" - | "Privacy"; + | "Privacy" + | "StudentLogin" + | "EducatorLogin" + | "ClassroomLobby"; export const PAGES: Array = [ ADMIN_PAGE, @@ -50,6 +56,8 @@ export const PAGES: Array = [ HOME_PAGE, ABOUT_PAGE, PRIVACY_PAGE, + STUDENT_LOGIN_PAGE, + EDUCATOR_LOGIN_PAGE, ]; export function isPage(pageName: string): pageName is Page { @@ -200,6 +208,27 @@ export const PAGE_META: { requiresConsent: true, }, }, + [STUDENT_LOGIN_PAGE]: { + path: "/student-login", + name: STUDENT_LOGIN_PAGE, + meta: { + requiresAuth: false, + }, + }, + [EDUCATOR_LOGIN_PAGE]: { + path: "/educator-login", + name: EDUCATOR_LOGIN_PAGE, + meta: { + requiresAuth: false, + }, + }, + [CLASSROOM_LOBBY_PAGE]: { + path: "/classroom", + name: CLASSROOM_LOBBY_PAGE, + meta: { + requiresAuth: true, + }, + }, }; export const PAGE_DEFAULT = PAGE_META[HOME_PAGE]; From 53bcf4d68d6c409b9c71bceb4000af4be3beab84 Mon Sep 17 00:00:00 2001 From: sgfost Date: Mon, 12 Feb 2024 17:21:09 -0700 Subject: [PATCH 004/137] feat: add edu mode toggle and separate vue router instance Co-authored-by: Allen Lee Co-authored-by: sbmota --- client/src/router.ts | 197 ++++++++++++++++++++++++++--------------- shared/src/routes.ts | 1 + shared/src/settings.ts | 6 ++ 3 files changed, 131 insertions(+), 73 deletions(-) diff --git a/client/src/router.ts b/client/src/router.ts index 8e35fedd1..e9934f139 100644 --- a/client/src/router.ts +++ b/client/src/router.ts @@ -45,61 +45,12 @@ import { PROFILE_PAGE, STUDENT_LOGIN_PAGE, CLASSROOM_LOBBY_PAGE, + EDUCATOR_LOGIN_PAGE, } from "@port-of-mars/shared/routes"; +import { isEducatorMode } from "@port-of-mars/shared/settings"; Vue.use(VueRouter); -const ADMIN_META = PAGE_META[ADMIN_PAGE].meta; -const FREE_PLAY_LOBBY_META = PAGE_META[FREE_PLAY_LOBBY_PAGE].meta; - -const router = new VueRouter({ - mode: "hash", - routes: [ - { - ...PAGE_META[ADMIN_PAGE], - component: Admin, - children: [ - { path: "", name: "Admin", redirect: { name: "AdminOverview" }, meta: ADMIN_META }, - { path: "overview", name: "AdminOverview", component: Overview, meta: ADMIN_META }, - { path: "games", name: "AdminGames", component: Games, meta: ADMIN_META }, - { path: "rooms", name: "AdminRooms", component: Rooms, meta: ADMIN_META }, - { path: "reports", name: "AdminReports", component: Reports, meta: ADMIN_META }, - { path: "settings", name: "AdminSettings", component: Settings, meta: ADMIN_META }, - ], - }, - { ...PAGE_META[LOGIN_PAGE], component: Login }, - { - ...PAGE_META[FREE_PLAY_LOBBY_PAGE], - component: FreePlayLobby, - children: [ - { path: "", name: "FreePlayLobby", component: LobbyRoomList, meta: FREE_PLAY_LOBBY_META }, - { - path: "room/:id", - name: "FreePlayLobbyRoom", - component: LobbyRoom, - meta: FREE_PLAY_LOBBY_META, - props: true, - }, - ], - }, - { ...PAGE_META[TOURNAMENT_LOBBY_PAGE], component: TournamentLobby }, - { ...PAGE_META[TOURNAMENT_DASHBOARD_PAGE], component: TournamentDashboard }, - { ...PAGE_META[GAME_PAGE], component: Game }, - { ...PAGE_META[SOLO_GAME_PAGE], component: SoloGame }, - { ...PAGE_META[LEADERBOARD_PAGE], component: Leaderboard }, - { ...PAGE_META[PLAYER_HISTORY_PAGE], component: PlayerHistory }, - { ...PAGE_META[CONSENT_PAGE], component: Consent }, - { ...PAGE_META[VERIFY_PAGE], component: Verify }, - { ...PAGE_META[MANUAL_PAGE], component: Manual }, - { ...PAGE_META[HOME_PAGE], component: Home }, - { ...PAGE_META[ABOUT_PAGE], component: Home }, - { ...PAGE_META[PRIVACY_PAGE], component: Privacy }, - { ...PAGE_META[PROFILE_PAGE], component: Profile }, - { ...PAGE_META[STUDENT_LOGIN_PAGE], component: StudentLogin }, - { ...PAGE_META[CLASSROOM_LOBBY_PAGE], component: ClassroomLobby }, - ], -}); - function isFreePlayEnabled() { return store.state.isFreePlayEnabled; } @@ -116,11 +67,16 @@ function isAdmin() { return store.getters.isAdmin; } +function isEducator() { + // FIXME: we need an educator flag on the user that gets passed to the client state + return true; +} + function hasConsented() { return store.getters.hasConsented; } -router.beforeEach((to: any, from: any, next: NavigationGuardNext) => { +function initStoreOnFirstRoute(from: any, next: NavigationGuardNext) { if (from === VueRouter.START_LOCATION) { console.log("initializing store"); store @@ -135,26 +91,121 @@ router.beforeEach((to: any, from: any, next: NavigationGuardNext) => { } else { next(); } -}); - -router.beforeEach((to: any, from: any, next: NavigationGuardNext) => { - // somewhat ugly but alternatives are worse, consider cleaning up the whole router - // setup at some point as its been gradually outgrowing the original design - if (to.meta.requiresAuth && !isAuthenticated()) { - next({ name: LOGIN_PAGE }); - } else if (to.meta.requiresConsent && !hasConsented()) { - next({ name: CONSENT_PAGE }); - } else if (to.meta.requiresAdmin && !isAdmin()) { - next({ name: HOME_PAGE }); - } else if (to.meta.requiresTournamentEnabled && !isTournamentEnabled()) { - next({ name: HOME_PAGE }); - } else if (to.meta.requiresFreePlayEnabled && !isFreePlayEnabled()) { - next({ name: HOME_PAGE }); - } else if (to.name === LOGIN_PAGE && isAuthenticated()) { - next({ name: HOME_PAGE }); - } else { - next(); - } -}); +} + +const ADMIN_META = PAGE_META[ADMIN_PAGE].meta; +const FREE_PLAY_LOBBY_META = PAGE_META[FREE_PLAY_LOBBY_PAGE].meta; + +const sharedRoutes = [ + // routes shared between educator and default mode + { + ...PAGE_META[ADMIN_PAGE], + component: Admin, + children: [ + { path: "", name: "Admin", redirect: { name: "AdminOverview" }, meta: ADMIN_META }, + { path: "overview", name: "AdminOverview", component: Overview, meta: ADMIN_META }, + { path: "games", name: "AdminGames", component: Games, meta: ADMIN_META }, + { path: "rooms", name: "AdminRooms", component: Rooms, meta: ADMIN_META }, + { path: "reports", name: "AdminReports", component: Reports, meta: ADMIN_META }, + { path: "settings", name: "AdminSettings", component: Settings, meta: ADMIN_META }, + ], + }, + { ...PAGE_META[GAME_PAGE], component: Game }, + { ...PAGE_META[LEADERBOARD_PAGE], component: Leaderboard }, + { ...PAGE_META[PLAYER_HISTORY_PAGE], component: PlayerHistory }, + { ...PAGE_META[MANUAL_PAGE], component: Manual }, + { ...PAGE_META[PRIVACY_PAGE], component: Privacy }, + { ...PAGE_META[PROFILE_PAGE], component: Profile }, +]; + +function getDefaultRouter() { + const router = new VueRouter({ + mode: "hash", + routes: [ + ...sharedRoutes, + { ...PAGE_META[LOGIN_PAGE], component: Login }, + { + ...PAGE_META[FREE_PLAY_LOBBY_PAGE], + component: FreePlayLobby, + children: [ + { path: "", name: "FreePlayLobby", component: LobbyRoomList, meta: FREE_PLAY_LOBBY_META }, + { + path: "room/:id", + name: "FreePlayLobbyRoom", + component: LobbyRoom, + meta: FREE_PLAY_LOBBY_META, + props: true, + }, + ], + }, + { ...PAGE_META[TOURNAMENT_LOBBY_PAGE], component: TournamentLobby }, + { ...PAGE_META[TOURNAMENT_DASHBOARD_PAGE], component: TournamentDashboard }, + { ...PAGE_META[SOLO_GAME_PAGE], component: SoloGame }, + { ...PAGE_META[CONSENT_PAGE], component: Consent }, + { ...PAGE_META[VERIFY_PAGE], component: Verify }, + { ...PAGE_META[MANUAL_PAGE], component: Manual }, + { ...PAGE_META[HOME_PAGE], component: Home }, + { ...PAGE_META[ABOUT_PAGE], component: Home }, + ], + }); + + router.beforeEach((to: any, from: any, next: NavigationGuardNext) => { + initStoreOnFirstRoute(from, next); + // somewhat ugly but alternatives are worse, consider cleaning up the whole router + // setup at some point as its been gradually outgrowing the original design + if (to.meta.requiresAuth && !isAuthenticated()) { + next({ name: LOGIN_PAGE }); + } else if (to.meta.requiresConsent && !hasConsented()) { + next({ name: CONSENT_PAGE }); + } else if (to.meta.requiresAdmin && !isAdmin()) { + next({ name: HOME_PAGE }); + } else if (to.meta.requiresTournamentEnabled && !isTournamentEnabled()) { + next({ name: HOME_PAGE }); + } else if (to.meta.requiresFreePlayEnabled && !isFreePlayEnabled()) { + next({ name: HOME_PAGE }); + } else if (to.name === LOGIN_PAGE && isAuthenticated()) { + next({ name: HOME_PAGE }); + } else { + next(); + } + }); + + return router; +} + +function getEducatorRouter() { + const router = new VueRouter({ + mode: "hash", + routes: [ + ...sharedRoutes, + // redirect straight to student login page + { path: "", name: "Home", redirect: { name: STUDENT_LOGIN_PAGE } }, + { ...PAGE_META[STUDENT_LOGIN_PAGE], component: StudentLogin }, + // FIXME: add EDUCATOR_LOGIN_PAGE + { ...PAGE_META[CLASSROOM_LOBBY_PAGE], component: ClassroomLobby }, + ], + }); + + router.beforeEach((to: any, from: any, next: NavigationGuardNext) => { + initStoreOnFirstRoute(from, next); + if (to.meta.requiresAuth && !isAuthenticated()) { + next({ name: STUDENT_LOGIN_PAGE }); + } else if (to.meta.requiresAdmin && !isAdmin()) { + next({ name: EDUCATOR_LOGIN_PAGE }); + } else if (to.meta.requiresEducator && !isEducator()) { + next({ name: EDUCATOR_LOGIN_PAGE }); + } else if ( + (to.name === STUDENT_LOGIN_PAGE || to.name === EDUCATOR_LOGIN_PAGE) && + isAuthenticated() + ) { + next({ name: CLASSROOM_LOBBY_PAGE }); + } else { + next(); + } + }); + + return router; +} +const router = isEducatorMode() ? getEducatorRouter() : getDefaultRouter(); export default router; diff --git a/shared/src/routes.ts b/shared/src/routes.ts index 0e18818ed..66c1bf6d7 100644 --- a/shared/src/routes.ts +++ b/shared/src/routes.ts @@ -73,6 +73,7 @@ export interface RouteMeta { requiresAuth: boolean; requiresConsent?: boolean; requiresAdmin?: boolean; + requiresEducator?: boolean; requiresTournamentEnabled?: boolean; requiresFreePlayEnabled?: boolean; } diff --git a/shared/src/settings.ts b/shared/src/settings.ts index 23d246997..45bd2c8ec 100644 --- a/shared/src/settings.ts +++ b/shared/src/settings.ts @@ -35,6 +35,12 @@ export const BASE_URL = baseUrlMap[ENVIRONMENT]; export const SERVER_URL_WS = isDev() ? "ws://localhost:2567" : ""; export const SERVER_URL_HTTP = isDev() ? "http://localhost:2567" : ""; +export function isEducatorMode(): boolean { + // FIXME: APP_MODE is in config.ts temporarily. ideally config.ts can be replaced + // entirely with an env file + return APP_MODE === "educator"; +} + export function isDev(): boolean { return ENVIRONMENT === "development"; } From df79288c60762ddea6a893d1dde62c92c3d3a416 Mon Sep 17 00:00:00 2001 From: sgfost Date: Mon, 12 Feb 2024 20:25:34 -0700 Subject: [PATCH 005/137] feat: add requiresTeacher client-side route guard isTeacher flag gets added to the user object in client state when needed We should be able to get away with no other (or very minimal) changes to the client state by repurposing the lobby object similar to how we do so for freeplay/tournament lobbies --- client/src/router.ts | 7 +++---- client/src/store/getters.ts | 4 ++++ server/src/routes/status.ts | 8 +++++++- server/src/services/educator.ts | 13 +++++++++++++ server/src/services/index.ts | 9 +++++++++ shared/src/routes.ts | 2 +- shared/src/types.ts | 1 + 7 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 server/src/services/educator.ts diff --git a/client/src/router.ts b/client/src/router.ts index e9934f139..c6e7a8d61 100644 --- a/client/src/router.ts +++ b/client/src/router.ts @@ -67,9 +67,8 @@ function isAdmin() { return store.getters.isAdmin; } -function isEducator() { - // FIXME: we need an educator flag on the user that gets passed to the client state - return true; +function isTeacher() { + return store.getters.isTeacher; } function hasConsented() { @@ -192,7 +191,7 @@ function getEducatorRouter() { next({ name: STUDENT_LOGIN_PAGE }); } else if (to.meta.requiresAdmin && !isAdmin()) { next({ name: EDUCATOR_LOGIN_PAGE }); - } else if (to.meta.requiresEducator && !isEducator()) { + } else if (to.meta.requiresTeacher && !isTeacher()) { next({ name: EDUCATOR_LOGIN_PAGE }); } else if ( (to.name === STUDENT_LOGIN_PAGE || to.name === EDUCATOR_LOGIN_PAGE) && diff --git a/client/src/store/getters.ts b/client/src/store/getters.ts index fe02e1ddf..c42191681 100644 --- a/client/src/store/getters.ts +++ b/client/src/store/getters.ts @@ -22,6 +22,10 @@ export default { return state.user?.isAdmin; }, + isTeacher(state: State): boolean { + return state.user?.isTeacher || false; + }, + hasConsented(state: State): boolean { return !!state.user?.dateConsented; }, diff --git a/server/src/routes/status.ts b/server/src/routes/status.ts index cc4aba47f..25cd39ef4 100644 --- a/server/src/routes/status.ts +++ b/server/src/routes/status.ts @@ -3,6 +3,7 @@ import { User } from "@port-of-mars/server/entity"; import { toClientSafeUser } from "@port-of-mars/server/util"; import { getServices } from "@port-of-mars/server/services"; import { ClientInitStatus } from "@port-of-mars/shared/types"; +import { isEducatorMode } from "@port-of-mars/shared/settings"; export const statusRouter = Router(); @@ -11,7 +12,12 @@ statusRouter.get("/", async (req: Request, res: Response, next) => { try { const services = getServices(); const user = req.user as User; - const safeUser = user ? { ...toClientSafeUser(user) } : null; + let isTeacher = false; + if (user && isEducatorMode()) { + // check if user is a teacher, only bother in educator mode + isTeacher = !!(await services.educator.getTeacherByUserId(user.id)); + } + const safeUser = user ? { ...toClientSafeUser(user), isTeacher } : null; const settings = await services.settings.getSettings(); const { isFreePlayEnabled, isTournamentEnabled, announcementBannerText } = settings; let tournamentStatus = null; diff --git a/server/src/services/educator.ts b/server/src/services/educator.ts new file mode 100644 index 000000000..32a440921 --- /dev/null +++ b/server/src/services/educator.ts @@ -0,0 +1,13 @@ +import { BaseService } from "@port-of-mars/server/services/db"; +import { Teacher } from "@port-of-mars/server/entity/Teacher"; +// import { settings } from "@port-of-mars/server/settings"; + +// const logger = settings.logging.getLogger(__filename); + +export class EducatorService extends BaseService { + async getTeacherByUserId(userId: number) { + return this.em.getRepository(Teacher).findOne({ + where: { userId }, + }); + } +} diff --git a/server/src/services/index.ts b/server/src/services/index.ts index b9e61d179..fa7a821a1 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -9,6 +9,7 @@ import { StatsService } from "@port-of-mars/server/services/stats"; import { TimeService } from "@port-of-mars/server/services/time"; import { GameService } from "@port-of-mars/server/services/game"; import { SoloGameService } from "@port-of-mars/server/services/sologame"; +import { EducatorService } from "@port-of-mars/server/services/educator"; import { RedisSettings } from "@port-of-mars/server/services/settings"; import dataSource from "@port-of-mars/server/datasource"; import { createClient, RedisClient } from "redis"; @@ -96,6 +97,14 @@ export class ServiceProvider { return this._admin; } + private _educator?: EducatorService; + get educator() { + if (!this._educator) { + this._educator = new EducatorService(this); + } + return this._educator; + } + private _settings?: RedisSettings; get settings() { if (!this._settings) { diff --git a/shared/src/routes.ts b/shared/src/routes.ts index 66c1bf6d7..e54778210 100644 --- a/shared/src/routes.ts +++ b/shared/src/routes.ts @@ -73,7 +73,7 @@ export interface RouteMeta { requiresAuth: boolean; requiresConsent?: boolean; requiresAdmin?: boolean; - requiresEducator?: boolean; + requiresTeacher?: boolean; requiresTournamentEnabled?: boolean; requiresFreePlayEnabled?: boolean; } diff --git a/shared/src/types.ts b/shared/src/types.ts index eb268efd6..77dbb3b37 100644 --- a/shared/src/types.ts +++ b/shared/src/types.ts @@ -24,6 +24,7 @@ export interface ClientSafeUser { name?: string; username: string; isAdmin: boolean; + isTeacher?: boolean; isMuted: boolean; isBanned: boolean; passedQuiz?: boolean; From 1409e31649eeaaf486b0a56437f2b463098bd383 Mon Sep 17 00:00:00 2001 From: Saachi Mota Date: Mon, 19 Feb 2024 14:20:06 -0700 Subject: [PATCH 006/137] WIP: Student login page with game code text field --- client/src/views/StudentLogin.vue | 134 +++++++++++++----------------- 1 file changed, 58 insertions(+), 76 deletions(-) diff --git a/client/src/views/StudentLogin.vue b/client/src/views/StudentLogin.vue index 658c7a9bc..e35ad4904 100644 --- a/client/src/views/StudentLogin.vue +++ b/client/src/views/StudentLogin.vue @@ -1,58 +1,24 @@ @@ -63,30 +29,16 @@ import { isDevOrStaging } from "@port-of-mars/shared/settings"; @Component export default class StudentLogin extends Vue { - isDevMode: boolean = false; - toggleDevLogin: boolean = false; - shouldSkipVerification: boolean = true; - devLoginUsername: string = ""; - error: string = ""; + gameCode: string = ''; created() { - this.isDevMode = isDevOrStaging(); + } - async devLogin(e: Event) { - e.preventDefault(); - const devLoginData: any = { - username: this.devLoginUsername, - password: "testing", - }; - try { - console.log(this.shouldSkipVerification); - await this.$ajax.devLogin(devLoginData, this.shouldSkipVerification); - } catch (e) { - if (e instanceof Error) { - this.error = e.message; - } - } + enterGame() { + // Handle entering the game here + console.log("Enter button clicked"); + console.log("Game code entered:", this.gameCode); } } @@ -95,10 +47,40 @@ export default class StudentLogin extends Vue { #login-container { padding: 2rem; width: 30rem; + text-align: center; /* Center the content horizontally */ +} + +.join-game-label { + color: white; + font-size: 1.5rem; + margin-bottom: 1rem; + display: block; /* Ensure label takes full width */ + font-weight: bold; /* Make the label bold */ + position: relative; /* Set position to relative */ + top: -110px; /* Move the label up by 50px */ + left: 310px; /* Move the label to the right by 50px */ +} + +.rounded-input, .rounded-button { + width: 70%; /* Set the width to 70% for both */ + border-radius: 20px; + margin-bottom: 1rem; + padding: 0.5rem; /* Add padding to make it visually appealing */ + display: flex; /* Use flexbox */ + justify-content: center; /* Center horizontally */ + align-items: center; /* Center vertically */ + text-align: center; + font-size: 20px; + font-weight: bold; +} + +.rounded-button { + min-width: 150px; + margin-top: 1rem; /* Add margin to separate the button from the text field */ } -ul { - list-style: circle !important; - padding-left: 2rem; +.enter-text { + font-weight: bold; /* Make the button text bold */ + font-size: 20px; } From 8cedaae31d3a618353b3131507efa84d026bb004 Mon Sep 17 00:00:00 2001 From: sgfost Date: Mon, 19 Feb 2024 14:54:12 -0700 Subject: [PATCH 007/137] fix: compile error with .ts settings, allow access to lobby --- shared/src/routes.ts | 3 ++- shared/src/settings.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/shared/src/routes.ts b/shared/src/routes.ts index e54778210..f470249b1 100644 --- a/shared/src/routes.ts +++ b/shared/src/routes.ts @@ -227,7 +227,8 @@ export const PAGE_META: { path: "/classroom", name: CLASSROOM_LOBBY_PAGE, meta: { - requiresAuth: true, + // FIXME: temp + requiresAuth: false, }, }, }; diff --git a/shared/src/settings.ts b/shared/src/settings.ts index 45bd2c8ec..0439ace8b 100644 --- a/shared/src/settings.ts +++ b/shared/src/settings.ts @@ -38,7 +38,7 @@ export const SERVER_URL_HTTP = isDev() ? "http://localhost:2567" : ""; export function isEducatorMode(): boolean { // FIXME: APP_MODE is in config.ts temporarily. ideally config.ts can be replaced // entirely with an env file - return APP_MODE === "educator"; + return (APP_MODE as any) === "educator"; } export function isDev(): boolean { From b763da818cdc1d2c00ffdbd70e3835a49d082ee5 Mon Sep 17 00:00:00 2001 From: sabrinanel3 <66537526+sabrinanel3@users.noreply.github.com> Date: Mon, 18 Mar 2024 13:49:57 -0700 Subject: [PATCH 008/137] FEAT (WIP): Add display wrapover for player grid Co-authored-by: sgfost --- client/src/views/ClassroomLobby.vue | 116 ++++++++++++++++++---------- 1 file changed, 77 insertions(+), 39 deletions(-) diff --git a/client/src/views/ClassroomLobby.vue b/client/src/views/ClassroomLobby.vue index 81d076a70..5af12ee15 100644 --- a/client/src/views/ClassroomLobby.vue +++ b/client/src/views/ClassroomLobby.vue @@ -1,44 +1,23 @@ @@ -129,4 +108,63 @@ export default class TournamentLobby extends Vue { } - + From 617082b4c7838d4ae4213ad4fd59dc4b7c5f78f2 Mon Sep 17 00:00:00 2001 From: Saachi Mota Date: Mon, 11 Mar 2024 14:52:04 -0700 Subject: [PATCH 009/137] WIP: re-join game checkbox and password text field --- client/src/views/StudentLogin.vue | 69 ++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 16 deletions(-) diff --git a/client/src/views/StudentLogin.vue b/client/src/views/StudentLogin.vue index e35ad4904..eb33a759c 100644 --- a/client/src/views/StudentLogin.vue +++ b/client/src/views/StudentLogin.vue @@ -4,11 +4,23 @@
- + + + + - Enter - + Join Game + Re-join Game + + + + + Already joined a game? +
+ @@ -52,23 +82,23 @@ export default class StudentLogin extends Vue { .join-game-label { color: white; - font-size: 1.5rem; + font-size: 2rem; margin-bottom: 1rem; display: block; /* Ensure label takes full width */ - font-weight: bold; /* Make the label bold */ - position: relative; /* Set position to relative */ - top: -110px; /* Move the label up by 50px */ - left: 310px; /* Move the label to the right by 50px */ + font-weight: bold; + position: relative; + top: -165px; + left: 335px; } .rounded-input, .rounded-button { - width: 70%; /* Set the width to 70% for both */ + width: 70%; border-radius: 20px; margin-bottom: 1rem; - padding: 0.5rem; /* Add padding to make it visually appealing */ - display: flex; /* Use flexbox */ - justify-content: center; /* Center horizontally */ - align-items: center; /* Center vertically */ + padding: 0.5rem; + display: flex; + justify-content: center; + align-items: center; text-align: center; font-size: 20px; font-weight: bold; @@ -76,11 +106,18 @@ export default class StudentLogin extends Vue { .rounded-button { min-width: 150px; - margin-top: 1rem; /* Add margin to separate the button from the text field */ + margin-top: 1rem; } .enter-text { - font-weight: bold; /* Make the button text bold */ + font-weight: bold; font-size: 20px; } + +.already-joined-checkbox { + color: white; + margin-bottom: 1rem; + padding: 0.5rem; +} + From 4b7dd3a2d68faafd9fd45d6aaaa61b593af7126c Mon Sep 17 00:00:00 2001 From: sabrinanel3 <66537526+sabrinanel3@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:34:32 -0700 Subject: [PATCH 010/137] FIX: Classroom Lobby layout & WIP Classroom Navbar --- .../src/components/global/ClassroomNavbar.vue | 197 ++++++++++++++++++ client/src/views/ClassroomLobby.vue | 56 +---- 2 files changed, 208 insertions(+), 45 deletions(-) create mode 100644 client/src/components/global/ClassroomNavbar.vue diff --git a/client/src/components/global/ClassroomNavbar.vue b/client/src/components/global/ClassroomNavbar.vue new file mode 100644 index 000000000..a0ea3c93d --- /dev/null +++ b/client/src/components/global/ClassroomNavbar.vue @@ -0,0 +1,197 @@ + + + + + diff --git a/client/src/views/ClassroomLobby.vue b/client/src/views/ClassroomLobby.vue index 5af12ee15..6b84b7f39 100644 --- a/client/src/views/ClassroomLobby.vue +++ b/client/src/views/ClassroomLobby.vue @@ -1,10 +1,11 @@ @@ -67,7 +64,11 @@ export default class TournamentLobby extends Vue { get clients() { // mocked clients for UI building return [ - { username: "Player 1", id: 1, dateJoined: new Date().getTime() }, + { + username: "Player 1", + id: 1, + dateJoined: new Date().getTime(), + }, { username: "Player 2", id: 2, dateJoined: new Date().getTime() }, { username: "Player 3", id: 3, dateJoined: new Date().getTime() }, { username: "Player 4", id: 4, dateJoined: new Date().getTime() }, @@ -85,26 +86,6 @@ export default class TournamentLobby extends Vue { return []; // return this.$tstore.state.lobby.chat; } - - // async created() { - // const hasActiveGame = await this.api.hasActiveGame("tournament"); - // if (hasActiveGame) { - // this.$router.push(this.game); - // return; - // } - // try { - // const room = await this.$client.joinOrCreate(TOURNAMENT_LOBBY_NAME); - // applyLobbyResponses(room, this, "tournament"); - // this.api.connect(room); - // } catch (e) { - // this.$router.push(this.dashboard); - // } - // } - - // beforeDestroy() { - // this.api.leave(); - // this.$tstore.commit("RESET_LOBBY_STATE"); - // } } @@ -112,6 +93,7 @@ export default class TournamentLobby extends Vue { .backdrop { display: flex; flex-direction: column; + position: relative; } .player-count { @@ -119,26 +101,9 @@ export default class TournamentLobby extends Vue { top: 0; right: 0; padding: 1rem; // Padding to not stick to the edges - z-index: 10; // Ensure it's above other elements -} - -.total-joined { - position: absolute; - top: 0; - right: 0; - padding: 7rem; z-index: 10; } -// .header { -// text-align: center; -// background-color: transparent; - -// p { -// color: #ffffff; -// } -// } - .player-grid { display: grid; @@ -153,6 +118,7 @@ export default class TournamentLobby extends Vue { @media (min-width: 1600px) { grid-template-columns: repeat(5, 1fr); } + justify-content: center; grid-gap: 1.5rem; width: 100%; max-width: 900px; @@ -164,7 +130,7 @@ export default class TournamentLobby extends Vue { border-radius: 1rem; padding: 1rem; text-align: center; - height: 70px; - width: 210px; + height: 4rem; + width: 12rem; } From 6c3e955565d07145453d3cad94d53a0520a0912f Mon Sep 17 00:00:00 2001 From: sgfost Date: Mon, 18 Mar 2024 16:42:52 -0700 Subject: [PATCH 011/137] feat: add student login with classroom code * moved dev login function on the client to AuthAPI TODO: * add generation of a passcode for signing back in as an existing student. either create a new strategy or modify existing to also take this auth token for signing back in * create the rest of the login flow, intermediate page for entering name, signing back in (passcode can be the response from a set-name call) Co-authored-by: saachibm Co-authored-by: Sabrina Nelson --- client/src/api/auth/request.ts | 56 ++++++++++++++++++++++++++++ client/src/plugins/ajax.ts | 18 --------- client/src/views/ClassroomLobby.vue | 2 +- client/src/views/Login.vue | 7 +++- client/src/views/StudentLogin.vue | 58 ++++++++++++++++------------- server/src/entity/index.ts | 3 ++ server/src/index.ts | 25 ++++++++++++- server/src/routes/auth.ts | 17 +++++---- server/src/services/educator.ts | 33 +++++++++++++++- 9 files changed, 162 insertions(+), 57 deletions(-) create mode 100644 client/src/api/auth/request.ts diff --git a/client/src/api/auth/request.ts b/client/src/api/auth/request.ts new file mode 100644 index 000000000..aea07dbd5 --- /dev/null +++ b/client/src/api/auth/request.ts @@ -0,0 +1,56 @@ +import { url } from "@port-of-mars/client/util"; +import { TStore } from "@port-of-mars/client/plugins/tstore"; +import { AjaxRequest } from "@port-of-mars/client/plugins/ajax"; +import { CONSENT_PAGE, FREE_PLAY_LOBBY_PAGE } from "@port-of-mars/shared/routes"; +import VueRouter from "vue-router"; + +export class AuthAPI { + constructor(public store: TStore, public ajax: AjaxRequest, public router: VueRouter) {} + + async devLogin(formData: { username: string; password: string }, shouldSkipVerification = true) { + try { + const devLoginUrl = url(`/auth/dev-login?shouldSkipVerification=${shouldSkipVerification}`); + await this.ajax.post( + devLoginUrl, + ({ data, status }) => { + if (status === 200) { + this.store.commit("SET_USER", data.user); + // FIXME: not terribly important but we might want to move to the tournament dashboard if isTournamentEnabled + if (data.user.isVerified) this.router.push({ name: FREE_PLAY_LOBBY_PAGE }); + else this.router.push({ name: CONSENT_PAGE }); + } else { + return data; + } + }, + formData + ); + } catch (e) { + console.log("Unable to login"); + console.log(e); + throw e; + } + } + + async studentLogin(classroomAuthToken: string) { + try { + const loginUrl = url("/auth/student-login"); + await this.ajax.post( + loginUrl, + ({ data, status }) => { + if (status === 200) { + this.store.commit("SET_USER", data.user); + // FIXME: route to the page after the login page that asks for a name and gives rejoin code + // this.router.push({ name: }); + } else { + return data; + } + }, + { classroomAuthToken, password: "unused" } + ); + } catch (e) { + console.log("Unable to login"); + console.log(e); + throw e; + } + } +} diff --git a/client/src/plugins/ajax.ts b/client/src/plugins/ajax.ts index 6f8d3a256..ae61fba7b 100644 --- a/client/src/plugins/ajax.ts +++ b/client/src/plugins/ajax.ts @@ -53,24 +53,6 @@ export class AjaxRequest { return this._roomId; } - async devLogin(formData: { username: string; password: string }, shouldSkipVerification = true) { - const devLoginUrl = url(`/auth/login?shouldSkipVerification=${shouldSkipVerification}`); - await this.post( - devLoginUrl, - ({ data, status }) => { - if (status === 200) { - this.store.commit("SET_USER", data.user); - // FIXME: not terribly important but we might want to move to the tournament dashboard if isTournamentEnabled - if (data.user.isVerified) this.router.push({ name: FREE_PLAY_LOBBY_PAGE }); - else this.router.push({ name: CONSENT_PAGE }); - } else { - return data; - } - }, - formData - ); - } - async forgetLoginCreds() { document.cookie = "connect.sid= ;expires=Thu, 01 Jan 1970 00:00:00 GMT"; this.store.commit("SET_USER", initialUserState); diff --git a/client/src/views/ClassroomLobby.vue b/client/src/views/ClassroomLobby.vue index 6b84b7f39..28d790453 100644 --- a/client/src/views/ClassroomLobby.vue +++ b/client/src/views/ClassroomLobby.vue @@ -45,7 +45,7 @@ import LobbyChat from "@port-of-mars/client/components/lobby/LobbyChat.vue"; LobbyChat, }, }) -export default class TournamentLobby extends Vue { +export default class ClassroomLobby extends Vue { @Inject() readonly $client!: Client; // @Provide() api: TournamentLobbyRequestAPI = new TournamentLobbyRequestAPI(this.$ajax); // accountApi!: AccountAPI; diff --git a/client/src/views/Login.vue b/client/src/views/Login.vue index 22971d660..e357c6922 100644 --- a/client/src/views/Login.vue +++ b/client/src/views/Login.vue @@ -69,6 +69,7 @@ import { url } from "@port-of-mars/client/util"; import { isDevOrStaging } from "@port-of-mars/shared/settings"; import { CONSENT_PAGE } from "@port-of-mars/shared/routes"; import AgeTooltip from "@port-of-mars/client/components/global/AgeTooltip.vue"; +import { AuthAPI } from "@port-of-mars/client/api/auth/request"; @Component({ components: { @@ -76,6 +77,8 @@ import AgeTooltip from "@port-of-mars/client/components/global/AgeTooltip.vue"; }, }) export default class Login extends Vue { + authApi!: AuthAPI; + isDevMode: boolean = false; toggleDevLogin: boolean = false; shouldSkipVerification: boolean = true; @@ -85,6 +88,7 @@ export default class Login extends Vue { consent = { name: CONSENT_PAGE }; async created() { + this.authApi = new AuthAPI(this.$store, this.$ajax, this.$router); this.isDevMode = isDevOrStaging(); } @@ -103,8 +107,7 @@ export default class Login extends Vue { password: "testing", }; try { - console.log(this.shouldSkipVerification); - await this.$ajax.devLogin(devLoginData, this.shouldSkipVerification); + await this.authApi.devLogin(devLoginData, this.shouldSkipVerification); } catch (e) { if (e instanceof Error) { this.error = e.message; diff --git a/client/src/views/StudentLogin.vue b/client/src/views/StudentLogin.vue index eb33a759c..2cdd07735 100644 --- a/client/src/views/StudentLogin.vue +++ b/client/src/views/StudentLogin.vue @@ -1,7 +1,11 @@ - + + diff --git a/shared/src/routes.ts b/shared/src/routes.ts index f470249b1..f01cbd4ce 100644 --- a/shared/src/routes.ts +++ b/shared/src/routes.ts @@ -15,6 +15,7 @@ export const ABOUT_PAGE = "About" as const; export const PRIVACY_PAGE = "Privacy" as const; export const PROFILE_PAGE = "Profile" as const; export const STUDENT_LOGIN_PAGE = "StudentLogin" as const; +export const STUDENT_CONFIRM_PAGE = "StudentConfirm" as const; export const EDUCATOR_LOGIN_PAGE = "EducatorLogin" as const; export const CLASSROOM_LOBBY_PAGE = "ClassroomLobby" as const; @@ -36,6 +37,7 @@ export type Page = | "Manual" | "Privacy" | "StudentLogin" + | "StudentConfirm" | "EducatorLogin" | "ClassroomLobby"; @@ -57,6 +59,7 @@ export const PAGES: Array = [ ABOUT_PAGE, PRIVACY_PAGE, STUDENT_LOGIN_PAGE, + STUDENT_CONFIRM_PAGE, EDUCATOR_LOGIN_PAGE, ]; @@ -216,6 +219,13 @@ export const PAGE_META: { requiresAuth: false, }, }, + [STUDENT_CONFIRM_PAGE]: { + path: "/student-confirm", + name: STUDENT_CONFIRM_PAGE, + meta: { + requiresAuth: false, //FIXME: change back to true + }, + }, [EDUCATOR_LOGIN_PAGE]: { path: "/educator-login", name: EDUCATOR_LOGIN_PAGE, From 764b860fe94cb55f9fd9f73a078a44d31243037b Mon Sep 17 00:00:00 2001 From: sgfost Date: Mon, 8 Apr 2024 16:54:56 -0700 Subject: [PATCH 014/137] feat: add classroom/teacher creation, student login + some minor cleanup CONTAINS MIGRATION --- client/src/App.vue | 12 +++- client/src/api/auth/request.ts | 9 ++- .../src/components/global/ClassroomNavbar.vue | 58 ++------------- client/src/components/global/Navbar.vue | 10 --- client/src/views/ClassroomLobby.vue | 4 +- client/src/views/StudentConfirm.vue | 70 +++++++++++++------ server/src/cli.ts | 52 ++++++++++++++ server/src/entity/Classroom.ts | 3 + server/src/entity/Teacher.ts | 3 + .../1712620270893-AddTeacherPassword.ts | 16 +++++ server/src/services/account.ts | 13 ++-- server/src/services/educator.ts | 35 ++++++++++ 12 files changed, 188 insertions(+), 97 deletions(-) create mode 100644 server/src/migration/1712620270893-AddTeacherPassword.ts diff --git a/client/src/App.vue b/client/src/App.vue index 2d95996b8..9c1de6835 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -1,7 +1,10 @@