Add student picker window

This commit is contained in:
Dominik Korsa 2021-01-21 00:20:45 +01:00
parent d7ee500ba6
commit c9fee0cad4
No known key found for this signature in database
GPG key ID: 546F986F71A6FE6E
6 changed files with 206 additions and 20 deletions

View file

@ -45,6 +45,12 @@ export default async function registerWebsiteApi(server: MyFastifyInstance): Pro
server.log.error(error); server.log.error(error);
throw server.httpErrors.internalServerError(); throw server.httpErrors.internalServerError();
} }
// TODO: Find why the promise never resolves
reply.clearCookie(`epk-${request.query.prompt_id}`);
// In case execution of setCookie takes some time
// TODO: Remove
await new Promise((resolve) => setTimeout(resolve, 100));
const sessionData = getSessionData(request.session); const sessionData = getSessionData(request.session);
const prompt = sessionData.prompts.get(request.query.prompt_id); const prompt = sessionData.prompts.get(request.query.prompt_id);
if (!prompt) throw server.httpErrors.badRequest('Prompt data not found'); if (!prompt) throw server.httpErrors.badRequest('Prompt data not found');

View file

@ -56,7 +56,7 @@ export default class LoginResolver {
availableStudentIds: students.map(({ studentId }) => studentId), availableStudentIds: students.map(({ studentId }) => studentId),
}; };
// TODO: Find why the promise never resolves // TODO: Find why the promise never resolves
reply.setCookie('epk', encryptedPrivateKey, { reply.setCookie(`epk-${promptId}`, encryptedPrivateKey, {
sameSite: 'strict', sameSite: 'strict',
httpOnly: true, httpOnly: true,
path: '/', path: '/',

View file

@ -1,11 +1,22 @@
<template> <template>
<div> <div>
<v-form @submit.prevent="submit"> <v-form @submit.prevent="submit" v-model="formValid" ref="form">
<v-card-title class="d-block">Zaloguj się do konta VULCAN UONET+</v-card-title> <v-card-title class="d-block">Zaloguj się do konta VULCAN UONET+</v-card-title>
<div class="mx-4"> <div class="mx-4">
<v-select v-model="host" label="Odmiana dziennika" :items="hosts" outlined /> <v-select v-model="host" label="Odmiana dziennika" :items="hosts" outlined />
<v-text-field v-model="username" label="Nazwa użytkownika" outlined /> <v-text-field
<v-text-field v-model="password" type="password" label="Hasło" outlined /> v-model="username"
label="Nazwa użytkownika"
outlined
:rules="requiredRules"
/>
<v-text-field
v-model="password"
type="password"
label="Hasło"
outlined
:rules="requiredRules"
/>
</div> </div>
<v-alert type="error" class="mx-2" :value="error === 'invalid-credentials'"> <v-alert type="error" class="mx-2" :value="error === 'invalid-credentials'">
Dane logowania nieprawidłowe Dane logowania nieprawidłowe
@ -18,7 +29,7 @@
Wróć Wróć
</v-btn> </v-btn>
<v-spacer /> <v-spacer />
<v-btn color="primary" :loading="loading" type="submit"> <v-btn color="primary" :loading="loading" type="submit" :disabled="!formValid">
Zaloguj się Zaloguj się
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
@ -27,9 +38,17 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'; import {
Component, Prop, Ref, Vue,
} from 'vue-property-decorator';
import { PromptInfo } from '@/types'; import { PromptInfo } from '@/types';
import { hasErrorCode, sdk } from '@/pages/authenticate-prompt/sdk'; import { hasErrorCode, sdk } from '@/pages/authenticate-prompt/sdk';
import { InputValidationRules } from 'vuetify';
interface VForm extends HTMLFormElement {
validate(): boolean;
resetValidation(): void;
}
@Component({ @Component({
name: 'LoginWindow', name: 'LoginWindow',
@ -41,6 +60,8 @@ export default class LoginWindow extends Vue {
}) })
promptInfo!: PromptInfo; promptInfo!: PromptInfo;
@Ref('form') form!: VForm;
readonly hosts = [ readonly hosts = [
{ {
text: 'Vulcan', text: 'Vulcan',
@ -52,6 +73,12 @@ export default class LoginWindow extends Vue {
}, },
] ]
readonly requiredRules: InputValidationRules = [
(v) => v !== '' || 'To pole jest wymagane',
]
formValid = false;
host = ''; host = '';
username = ''; username = '';
@ -66,10 +93,11 @@ export default class LoginWindow extends Vue {
this.host = 'fakelog.cf'; this.host = 'fakelog.cf';
this.username = ''; this.username = '';
this.password = ''; this.password = '';
this.form.resetValidation();
} }
async submit() { async submit() {
if (this.loading) return; if (this.loading || !this.formValid) return;
this.error = null; this.error = null;
this.loading = true; this.loading = true;
try { try {
@ -80,7 +108,7 @@ export default class LoginWindow extends Vue {
password: this.password, password: this.password,
}); });
const { students } = login; const { students } = login;
console.log(students); this.$emit('login', { students });
this.reset(); this.reset();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -94,7 +122,7 @@ export default class LoginWindow extends Vue {
this.$emit('back'); this.$emit('back');
} }
created() { mounted() {
this.reset(); this.reset();
} }
} }

View file

@ -0,0 +1,125 @@
<template>
<div>
<v-form @submit.prevent="submit">
<v-card-title
v-if="mode === StudentsMode.One"
>
Wybierz ucznia
</v-card-title>
<v-card-title
v-else
>
Wybierz uczniów
</v-card-title>
<v-alert
v-if="mode === StudentsMode.None"
type="info"
text
class="mx-2"
>
Aplikacja nie wymaga dostępu do dzienników uczniów
</v-alert>
<template v-else>
<v-divider />
<v-list>
<v-list-item-group
v-model="studentsValue"
:multiple="mode === StudentsMode.Many"
color="primary"
>
<v-list-item
v-for="student in students"
:key="student.studentId"
:value="student.studentId"
>
<template v-slot:default="{ active }">
<v-list-item-action>
<v-icon v-if="active">
{{ mode === StudentsMode.Many ? '$checkboxOn' : '$radioOn' }}
</v-icon>
<v-icon v-else>
{{ mode === StudentsMode.Many ? '$checkboxOff' : '$radioOff' }}
</v-icon>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title>{{ student.name }}</v-list-item-title>
</v-list-item-content>
</template>
</v-list-item>
</v-list-item-group>
</v-list>
<v-divider />
</template>
<v-card-actions>
<v-btn color="primary" text outlined @click="back">
Wróć
</v-btn>
<v-spacer />
<v-btn color="primary" :href="allowUrl" type="submit" :disabled="!valid">
Przydziel dostęp
</v-btn>
</v-card-actions>
</v-form>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { PromptInfo, Student, StudentsMode } from '@/types';
@Component({
name: 'StudentsWindow',
})
export default class StudentsWindow extends Vue {
@Prop({
required: true,
type: Object,
})
promptInfo!: PromptInfo
@Prop({
required: true,
type: Array,
})
students!: Student[]
readonly StudentsMode = StudentsMode;
get mode() {
return this.promptInfo.studentsMode;
}
get studentsValue() {
if (this.mode === StudentsMode.One) {
return this.pickedStudents[0] ?? undefined;
}
return this.pickedStudents;
}
set studentsValue(value: string[] | string | undefined) {
if (value === undefined) this.pickedStudents = [];
else if (value instanceof Array) this.pickedStudents = value;
else this.pickedStudents = [value];
}
pickedStudents: string[] = [];
get valid() {
if (this.mode === StudentsMode.None) return this.pickedStudents.length === 0;
if (this.mode === StudentsMode.One) return this.pickedStudents.length === 1;
if (this.mode === StudentsMode.Many) return this.pickedStudents.length >= 1;
return false;
}
get allowUrl() {
if (!this.valid) return null;
if (this.mode === StudentsMode.One) return `/api/website/allow?prompt_id=${this.promptInfo.id}`;
const studentIds = this.pickedStudents.map(encodeURIComponent).join('+');
return `/api/website/allow?prompt_id=${this.promptInfo.id}&student_ids=${studentIds}`;
}
back() {
this.$emit('back');
}
}
</script>

View file

@ -23,7 +23,7 @@
</div> </div>
<template v-else> <template v-else>
<div class="pb-1 text--secondary"> <div class="pb-1 text--secondary">
Krok <span class="primary--text">{{ step }}/5</span> Krok <span class="primary--text">{{ step }}/3</span>
</div> </div>
<v-card outlined> <v-card outlined>
<div class="d-flex justify-center mn-16 avatar__wrapper"> <div class="d-flex justify-center mn-16 avatar__wrapper">
@ -70,12 +70,25 @@
<v-window :value="step"> <v-window :value="step">
<v-window-item :value="1"> <v-window-item :value="1">
<overview-window <overview-window
@next="overviewNext"
:promptInfo="promptInfo" :promptInfo="promptInfo"
@next="toLoginWindow"
/> />
</v-window-item> </v-window-item>
<v-window-item :value="2" eager> <v-window-item :value="2" eager>
<login-window @back="loginBack" ref="loginWindow" :prompt-info="promptInfo" /> <login-window
ref="loginWindow"
:prompt-info="promptInfo"
@login="login"
@back="loginBack"
/>
</v-window-item>
<v-window-item :value="3">
<students-window
v-if="students !== null"
:prompt-info="promptInfo"
:students="students"
@back="toLoginWindow"
/>
</v-window-item> </v-window-item>
</v-window> </v-window>
</v-card> </v-card>
@ -115,13 +128,14 @@
<script lang="ts"> <script lang="ts">
import { Component, Ref, Vue } from 'vue-property-decorator'; import { Component, Ref, Vue } from 'vue-property-decorator';
import OverviewWindow from '@/compontents/authenticate-prompt-windows/overview-window.vue'; import OverviewWindow from '@/compontents/authenticate-prompt-windows/overview-window.vue';
import { PromptInfo } from '@/types'; import { PromptInfo, Student } from '@/types';
import LoginWindow from '@/compontents/authenticate-prompt-windows/login-window.vue'; import LoginWindow from '@/compontents/authenticate-prompt-windows/login-window.vue';
import StudentsWindow from '@/compontents/authenticate-prompt-windows/students-window.vue';
import { sdk } from '@/pages/authenticate-prompt/sdk'; import { sdk } from '@/pages/authenticate-prompt/sdk';
@Component({ @Component({
name: 'AuthenticatePromptApp', name: 'AuthenticatePromptApp',
components: { LoginWindow, OverviewWindow }, components: { LoginWindow, OverviewWindow, StudentsWindow },
}) })
export default class AuthenticatePromptApp extends Vue { export default class AuthenticatePromptApp extends Vue {
@Ref() readonly loginWindow!: LoginWindow @Ref() readonly loginWindow!: LoginWindow
@ -132,6 +146,8 @@ export default class AuthenticatePromptApp extends Vue {
promptInfoError = false; promptInfoError = false;
students: Student[] | null = null;
step = 1; step = 1;
async loadPromptInfo() { async loadPromptInfo() {
@ -158,13 +174,19 @@ export default class AuthenticatePromptApp extends Vue {
await this.loadPromptInfo(); await this.loadPromptInfo();
} }
toLoginWindow() {
this.step = 2;
this.loginWindow.reset();
this.students = null;
}
loginBack() { loginBack() {
this.step = 1; this.step = 1;
} }
overviewNext() { login({ students }: { students: Student[] }) {
this.step = 2; this.students = students;
this.loginWindow.reset(); this.step = 3;
} }
} }
</script> </script>

View file

@ -1,7 +1,7 @@
export enum StudentsMode { export enum StudentsMode {
None = 'none', None = 'None',
One = 'one', One = 'One',
Many = 'many', Many = 'Many',
} }
export interface PromptInfo { export interface PromptInfo {
@ -15,3 +15,8 @@ export interface PromptInfo {
verified: boolean; verified: boolean;
}; };
} }
export interface Student {
studentId: string;
name: string;
}