Add student picker window
This commit is contained in:
parent
d7ee500ba6
commit
c9fee0cad4
6 changed files with 206 additions and 20 deletions
|
@ -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');
|
||||||
|
|
|
@ -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: '/',
|
||||||
|
|
|
@ -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 są nieprawidłowe
|
Dane logowania są 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue