diff --git a/src/common/models/session.ts b/src/common/models/session.ts index e4503496..d9829bfa 100644 --- a/src/common/models/session.ts +++ b/src/common/models/session.ts @@ -1,8 +1,8 @@ -import type { ParseableUser, User } from 'src/common/models/users'; +import type { SessionUser, User } from 'src/common/models/users'; import { parseBoolean } from './parsers'; export interface SessionInfo { - user: ParseableUser; + user: SessionUser; logged_in: boolean; message: string; } diff --git a/src/common/models/users.ts b/src/common/models/users.ts index 713151cf..741766c3 100644 --- a/src/common/models/users.ts +++ b/src/common/models/users.ts @@ -6,16 +6,20 @@ import { isNonNegInt, isValidUsername, isValidEmail } import { Dictionary, Model } from 'src/common/models'; import { UserRole } from 'src/stores/permissions'; -export interface ParseableUser { - user_id?: number; - username?: string; +// This defined the required fields of a user in the Session + +export interface SessionUser { + user_id: number; + username: string; email?: string; first_name?: string; last_name?: string; - is_admin?: boolean; + is_admin: boolean; student_id?: string; } +export type ParseableUser = Partial; + /** * @class User */ @@ -34,15 +38,15 @@ export class User extends Model { get all_field_names(): string[] { return User.ALL_FIELDS; } get param_fields(): string[] { return []; } - constructor(params: ParseableUser = {}) { + constructor(params: ParseableUser = { user_id: 0, username: '', is_admin: false }) { super(); this.set(params); } set(params: ParseableUser) { if (params.username) this.username = params.username; - if (params.user_id != undefined) this.user_id = params.user_id; - if (params.is_admin != undefined) this.is_admin = params.is_admin; + if (params.user_id) this.user_id = params.user_id; + if (params.is_admin) this.is_admin = params.is_admin; if (params.email) this.email = params.email; if (params.first_name) this.first_name = params.first_name; if (params.last_name) this.last_name = params.last_name; @@ -72,7 +76,7 @@ export class User extends Model { set student_id(value: string) { this._student_id = value; } clone() { - return new User(this.toObject() as ParseableUser); + return new User(this.toObject() as unknown as ParseableUser); } // The email can be the empty string or valid email address. diff --git a/src/components/common/Login.vue b/src/components/common/Login.vue index 1c707276..23c8f9a1 100644 --- a/src/components/common/Login.vue +++ b/src/components/common/Login.vue @@ -24,7 +24,6 @@ import { useRouter } from 'vue-router'; import { ref } from 'vue'; import { useI18n } from 'vue-i18n'; -import { checkPassword } from 'src/common/api-requests/session'; import { useSessionStore } from 'src/stores/session'; import { usePermissionStore } from 'src/stores/permissions'; @@ -54,25 +53,17 @@ const login = async () => { username: username.value, password: password.value }; - const session_info = await checkPassword(username_info); - - if (!session_info.logged_in || !session_info.user.user_id) { - message.value = i18n.t('authentication.failure'); - } else { - // success - session.updateSessionInfo(session_info); - - // permissions require access to user courses and respective roles - await session.fetchUserCourses(); - await permission_store.fetchRoles(); - await permission_store.fetchRoutePermissions(); - + const login_successful = await session.login(username_info); + if (login_successful) { + const user = session.user; let forward = localStorage.getItem('afterLogin'); - forward ||= (session_info.user.is_admin) ? + forward ||= (user.is_admin) ? '/admin' : - `/users/${session_info.user.user_id}/courses`; + `/users/${user.user_id}/courses`; localStorage.removeItem('afterLogin'); void router.push(forward); + } else { + message.value = i18n.t('authentication.failure'); } }; diff --git a/src/stores/session.ts b/src/stores/session.ts index 12341d37..a977da8f 100644 --- a/src/stores/session.ts +++ b/src/stores/session.ts @@ -2,8 +2,8 @@ import { defineStore } from 'pinia'; import { api } from 'boot/axios'; -import { ParseableUser, User } from 'src/common/models/users'; -import type { SessionInfo } from 'src/common/models/session'; +import { SessionUser, User } from 'src/common/models/users'; +import { ParseableSessionInfo, parseSessionInfo, SessionInfo, UserPassword } from 'src/common/models/session'; import { ParseableUserCourse } from 'src/common/models/courses'; import { logger } from 'boot/logger'; import { ResponseError } from 'src/common/api-requests/errors'; @@ -11,7 +11,7 @@ import { ResponseError } from 'src/common/api-requests/errors'; import { useUserStore } from 'src/stores/users'; import { useSettingsStore } from 'src/stores/settings'; import { useProblemSetStore } from 'src/stores/problem_sets'; -import { UserRole } from 'src/stores/permissions'; +import { usePermissionStore, UserRole } from 'src/stores/permissions'; interface CourseInfo { course_name: string; @@ -25,18 +25,20 @@ interface CourseInfo { export interface SessionState { logged_in: boolean; expiry: number; - user: ParseableUser; + user: SessionUser; course: CourseInfo; user_courses: ParseableUserCourse[]; } +const logged_out_user = { username: 'logged_out', user_id: 0, is_admin: false }; + export const useSessionStore = defineStore('session', { // Stores this in localStorage. persist: true, state: (): SessionState => ({ logged_in: false, expiry: 0, - user: { username: 'logged_out' }, + user: logged_out_user, course: { course_id: 0, role: '', @@ -45,7 +47,7 @@ export const useSessionStore = defineStore('session', { user_courses: [] }), getters: { - full_name: (state): string => `${state.user?.first_name ?? ''} ${state.user?.last_name ?? ''}`, + full_name: (state): string => `${state.user.first_name ?? ''} ${state.user.last_name ?? ''}`, getUser: (state): User => new User(state.user), }, actions: { @@ -63,7 +65,7 @@ export const useSessionStore = defineStore('session', { if (this.logged_in) { this.user = session_info.user; } else { - this.user = new User({ username: 'logged_out' }).toObject(); + this.user = logged_out_user; } }, setCourse(course_id: number): void { @@ -102,9 +104,29 @@ export const useSessionStore = defineStore('session', { throw response.data as ResponseError; } }, + /** + * Attempt to login to webwork3 with username/password. If successful, fetch + * needed data (usercourses, roles, permissions). + */ + async login(user_pass: UserPassword): Promise { + const response = await api.post('login', user_pass); + const session_info = parseSessionInfo(response.data as ParseableSessionInfo); + if (!session_info.logged_in || !session_info.user.user_id) { + return false; + } else { + // success + this.updateSessionInfo(session_info); + const permission_store = usePermissionStore(); + // permissions require access to user courses and respective roles + await this.fetchUserCourses(); + await permission_store.fetchRoles(); + await permission_store.fetchRoutePermissions(); + return true; + } + }, logout() { this.logged_in = false; - this.user = new User({ username: 'logged_out' }).toObject(); + this.user = logged_out_user; this.course = { course_id: 0, role: '', course_name: '' }; useProblemSetStore().clearAll(); useSettingsStore().clearAll(); diff --git a/tests/stores/session.spec.ts b/tests/stores/session.spec.ts index f26b224f..2e5940f2 100644 --- a/tests/stores/session.spec.ts +++ b/tests/stores/session.spec.ts @@ -19,7 +19,7 @@ import { checkPassword } from 'src/common/api-requests/session'; import { Course, UserCourse } from 'src/common/models/courses'; import { SessionInfo } from 'src/common/models/session'; -import { ParseableUser, User } from 'src/common/models/users'; +import { SessionUser, User } from 'src/common/models/users'; import { cleanIDs, loadCSV } from '../utils'; @@ -30,7 +30,7 @@ describe('Session Store', () => { let lisa: User; // session now just stores objects not models: - const user: ParseableUser = { + const user: SessionUser = { first_name: 'Homer', last_name: 'Simpson', user_id: 1234, @@ -39,14 +39,10 @@ describe('Session Store', () => { is_admin: false, }; - const logged_out: ParseableUser = { + const logged_out: SessionUser = { username: 'logged_out', - email: '', - last_name: '', - first_name: '', user_id: 0, is_admin: false, - student_id: '' }; const session_info: SessionInfo = { @@ -114,19 +110,12 @@ describe('Session Store', () => { }); test('Login as a user', async () => { - // test logging in as lisa gives the proper courses. - const session_info = await checkPassword({ - username: 'lisa', password: 'lisa' - }); - expect(session_info.logged_in).toBe(true); - expect(session_info.user).toStrictEqual(lisa.toObject()); - + // test logging in as lisa gives the proper courses. const session = useSessionStore(); - session.updateSessionInfo(session_info); - + const logged_in = await session.login({ username: 'lisa', password: 'lisa' }); + expect(logged_in).toBe(true); expect(session.logged_in).toBe(true); expect(session.user).toStrictEqual(lisa.toObject()); - }); // sort by course name and clean up the _id tags. @@ -137,7 +126,6 @@ describe('Session Store', () => { test('check user courses', async () => { const session_store = useSessionStore(); - await session_store.fetchUserCourses(); expect(sortAndClean(session_store.user_courses.map(c => new UserCourse(c)))) .toStrictEqual(sortAndClean(lisa_courses)); }); diff --git a/tests/stores/set_problems.spec.ts b/tests/stores/set_problems.spec.ts index 0d4bfb2d..273dd4a1 100644 --- a/tests/stores/set_problems.spec.ts +++ b/tests/stores/set_problems.spec.ts @@ -27,7 +27,6 @@ import { Dictionary, generic } from 'src/common/models'; import { loadCSV, cleanIDs } from '../utils'; import { checkPassword } from 'src/common/api-requests/session'; -import { logger } from 'src/boot/logger'; const app = createApp({}); diff --git a/tests/stores/user_sets.spec.ts b/tests/stores/user_sets.spec.ts index d5c40249..08f0de6d 100644 --- a/tests/stores/user_sets.spec.ts +++ b/tests/stores/user_sets.spec.ts @@ -11,7 +11,6 @@ import { createApp } from 'vue'; import { createPinia, setActivePinia } from 'pinia'; import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; -import { api } from 'boot/axios'; import { useCourseStore } from 'src/stores/courses'; import { useProblemSetStore } from 'src/stores/problem_sets'; diff --git a/tests/unit-tests/users.spec.ts b/tests/unit-tests/users.spec.ts index 31cdb58c..29cf778b 100644 --- a/tests/unit-tests/users.spec.ts +++ b/tests/unit-tests/users.spec.ts @@ -96,7 +96,7 @@ describe('Testing User and CourseUsers', () => { describe('Testing for valid and invalid users.', () => { test('setting invalid email', () => { - const user = new User({ username: 'test' }); + const user = new User({ username: 'test', user_id: 10, is_admin: true }); expect(user.isValid()).toBe(true); user.email = 'bad@email@address.com'; @@ -104,7 +104,7 @@ describe('Testing User and CourseUsers', () => { }); test('setting invalid user_id', () => { - const user = new User({ username: 'test' }); + const user = new User({ username: 'test', user_id: 10, is_admin: true }); expect(user.isValid()).toBe(true); user.user_id = -15; @@ -112,7 +112,7 @@ describe('Testing User and CourseUsers', () => { }); test('setting invalid username', () => { - const user = new User({ username: 'my username' }); + const user = new User({ username: 'my username', user_id: 10, is_admin: true }); expect(user.isValid()).toBe(false); }); });