From b837745e2925f566f037ecb0aac6bd99639b8c8e 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 01/80] 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 33e24cafe7d723422c2af3a25db446f74cc9c5fd Mon Sep 17 00:00:00 2001 From: sgfost Date: Mon, 29 Jan 2024 16:07:45 -0700 Subject: [PATCH 02/80] 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 d2ba90557e7434f4c4a00928f385e4a298d163d6 Mon Sep 17 00:00:00 2001 From: sgfost Date: Mon, 5 Feb 2024 15:28:38 -0700 Subject: [PATCH 03/80] 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 b6036ac6d..4c5a2dfde 100644 --- a/client/src/router.ts +++ b/client/src/router.ts @@ -24,6 +24,8 @@ 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 ProlificStudy from "@port-of-mars/client/views/ProlificStudy.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, @@ -44,6 +46,8 @@ import { PRIVACY_PAGE, PROFILE_PAGE, PROLIFIC_STUDY_PAGE, + STUDENT_LOGIN_PAGE, + CLASSROOM_LOBBY_PAGE, } from "@port-of-mars/shared/routes"; Vue.use(VueRouter); @@ -96,6 +100,8 @@ const router = new VueRouter({ { ...PAGE_META[PRIVACY_PAGE], component: Privacy }, { ...PAGE_META[PROFILE_PAGE], component: Profile }, { ...PAGE_META[PROLIFIC_STUDY_PAGE], component: ProlificStudy }, + { ...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 5214b8f73..5787ba46d 100644 --- a/shared/src/routes.ts +++ b/shared/src/routes.ts @@ -15,6 +15,9 @@ export const ABOUT_PAGE = "About" as const; export const PRIVACY_PAGE = "Privacy" as const; export const PROFILE_PAGE = "Profile" as const; export const PROLIFIC_STUDY_PAGE = "ProlificStudy" 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" @@ -33,7 +36,10 @@ export type Page = | "Verify" | "Manual" | "ProlificStudy" - | "Privacy"; + | "Privacy" + | "StudentLogin" + | "EducatorLogin" + | "ClassroomLobby"; export const PAGES: Array = [ ADMIN_PAGE, @@ -53,6 +59,8 @@ export const PAGES: Array = [ ABOUT_PAGE, PRIVACY_PAGE, PROLIFIC_STUDY_PAGE, + STUDENT_LOGIN_PAGE, + EDUCATOR_LOGIN_PAGE, ]; export function getPagePath(page: Page): string { @@ -207,6 +215,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 fa204bbfd117d5d705c48a0459d5d25c499f1992 Mon Sep 17 00:00:00 2001 From: sgfost Date: Mon, 12 Feb 2024 17:21:09 -0700 Subject: [PATCH 04/80] feat: add edu mode toggle and separate vue router instance Co-authored-by: Allen Lee Co-authored-by: sbmota --- client/src/router.ts | 200 +++++++++++++++++++++++++---------------- shared/src/routes.ts | 1 + shared/src/settings.ts | 6 ++ 3 files changed, 132 insertions(+), 75 deletions(-) diff --git a/client/src/router.ts b/client/src/router.ts index 4c5a2dfde..de2dec186 100644 --- a/client/src/router.ts +++ b/client/src/router.ts @@ -48,63 +48,12 @@ import { PROLIFIC_STUDY_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 }, - { path: "studies", name: "AdminStudies", component: Studies, 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[PROLIFIC_STUDY_PAGE], component: ProlificStudy }, - { ...PAGE_META[STUDENT_LOGIN_PAGE], component: StudentLogin }, - { ...PAGE_META[CLASSROOM_LOBBY_PAGE], component: ClassroomLobby }, - ], -}); - function isFreePlayEnabled() { return store.state.isFreePlayEnabled; } @@ -121,11 +70,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 @@ -140,26 +94,122 @@ 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 }, + { path: "studies", name: "AdminStudies", component: Studies, 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 }, + { ...PAGE_META[PROLIFIC_STUDY_PAGE], component: ProlificStudy }, + ], + }); + + 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 }, + { ...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 5787ba46d..2404b1a56 100644 --- a/shared/src/routes.ts +++ b/shared/src/routes.ts @@ -72,6 +72,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 bcab532e962302f1ffbc52d93b6f566efa2a4fb5 Mon Sep 17 00:00:00 2001 From: sgfost Date: Mon, 12 Feb 2024 20:25:34 -0700 Subject: [PATCH 05/80] 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 de2dec186..da7d6ac6c 100644 --- a/client/src/router.ts +++ b/client/src/router.ts @@ -70,9 +70,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() { @@ -196,7 +195,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 bad356e74..1ddf57881 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"; @@ -105,6 +106,14 @@ export class ServiceProvider { return this._study; } + 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 2404b1a56..8554e7974 100644 --- a/shared/src/routes.ts +++ b/shared/src/routes.ts @@ -72,7 +72,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 a05848fca..705f3e6ed 100644 --- a/shared/src/types.ts +++ b/shared/src/types.ts @@ -25,6 +25,7 @@ export interface ClientSafeUser { name?: string; username: string; isAdmin: boolean; + isTeacher?: boolean; isMuted: boolean; isBanned: boolean; passedQuiz?: boolean; From 957a2430574e5b0a675e28a8bb3aa48f31e5f72b Mon Sep 17 00:00:00 2001 From: Saachi Mota Date: Mon, 19 Feb 2024 14:20:06 -0700 Subject: [PATCH 06/80] 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 38e3f017ec4198cd8b3d50f8318ff8b79c63f865 Mon Sep 17 00:00:00 2001 From: sgfost Date: Mon, 19 Feb 2024 14:54:12 -0700 Subject: [PATCH 07/80] 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 8554e7974..a17b8dffe 100644 --- a/shared/src/routes.ts +++ b/shared/src/routes.ts @@ -234,7 +234,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 9b3ccae338b51ff25a399daa26cdbffd33dcedf0 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 08/80] 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 b42f8c31d60e66f4496a6f1d11006c7eef759321 Mon Sep 17 00:00:00 2001 From: Saachi Mota Date: Mon, 11 Mar 2024 14:52:04 -0700 Subject: [PATCH 09/80] 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 6cfe53cdbb45006e1c9a52a61cf3ca74b43c6729 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 10/80] 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 465e84bcbd91abafa4229aad11fce6c9c90fcdb2 Mon Sep 17 00:00:00 2001 From: sgfost Date: Mon, 18 Mar 2024 16:42:52 -0700 Subject: [PATCH 11/80] 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 a17b8dffe..83c6d9cce 100644 --- a/shared/src/routes.ts +++ b/shared/src/routes.ts @@ -16,6 +16,7 @@ export const PRIVACY_PAGE = "Privacy" as const; export const PROFILE_PAGE = "Profile" as const; export const PROLIFIC_STUDY_PAGE = "ProlificStudy" 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; @@ -38,6 +39,7 @@ export type Page = | "ProlificStudy" | "Privacy" | "StudentLogin" + | "StudentConfirm" | "EducatorLogin" | "ClassroomLobby"; @@ -60,6 +62,7 @@ export const PAGES: Array = [ PRIVACY_PAGE, PROLIFIC_STUDY_PAGE, STUDENT_LOGIN_PAGE, + STUDENT_CONFIRM_PAGE, EDUCATOR_LOGIN_PAGE, ]; @@ -223,6 +226,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 06a951afaf9547b020bafb32b6754f2a53f228ad Mon Sep 17 00:00:00 2001 From: sgfost Date: Mon, 8 Apr 2024 16:54:56 -0700 Subject: [PATCH 14/80] feat: add classroom/teacher creation, student login + some minor cleanup CONTAINS MIGRATION --- client/src/App.vue | 10 ++- 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 | 49 +++++++++++++ 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, 183 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 2ffe92d97..5108b0e08 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -1,7 +1,8 @@