feat: review backend and frontend

- update to the latest version of Java/SpringBoot
- update to the latest version NuxtJS
- add account/password update
- add account creation
- add account password reset
- add bundle to regroup questions and add default questions on user creation
- add bundle creation
This commit is contained in:
2024-07-03 15:55:34 +02:00
parent f86d794239
commit b6e86f0641
207 changed files with 5570 additions and 40453 deletions

47
frontend/store/account.ts Normal file
View File

@@ -0,0 +1,47 @@
import {defineStore} from 'pinia';
export const useAccountStore = defineStore('account', {
state: () => ({}),
actions: {
update(username: string, email: string) {
return useApi("/account", {
method: "PUT",
body: {
email, username
}
});
},
updatePassword(currentPassword: string, newPassword: string, confirmationPassword: string) {
return useApi("/account/password", {
method: "PUT",
body: {
currentPassword, newPassword, confirmationPassword
}
});
},
requestPasswordReset(email: string) {
return useApi("/account/password/notify-reset-request", {
method: "POST",
body: {
email
}
}, false);
},
resetPassword(token: string, email: string, newPassword: string, confirmationPassword: string) {
return useApi("/account/password/reset", {
method: "POST",
body: {
token, email, newPassword, confirmationPassword
}
}, false);
},
create(username: string, email: string, password: string) {
return useApi("/auth/register", {
method: "POST",
body: {
username, email, password
}
}, false);
}
},
});

76
frontend/store/auth.ts Normal file
View File

@@ -0,0 +1,76 @@
import {defineStore} from 'pinia';
import {useApi} from "~/composables/fetch-api";
export interface Auth {
token: {
type: string;
value: string;
expireAt: Date;
},
refreshToken: string;
}
export interface User {
id: number
email: string;
username: string;
}
export const useAuthStore = defineStore('auth', {
state: () => ({
authenticated: ref<boolean>(useCookie("auth").value !== undefined),
auth: ref<Auth>(useCookie("auth").value),
user: ref<User>(useCookie("user").value)
}),
getters: {
},
actions: {
async login(email: string, password: string) {
return useApi('auth/login', {
method: 'post',
body: {
email,
password,
},
}, false).then(data => {
useCookie('auth').value = JSON.stringify(data);
this.authenticated = true;
this.auth = data;
useApi('auth/me').then(data => {
this.user = data;
useCookie("user").value = JSON.stringify(data)
});
});
},
logout() {
useApi('auth/logout', {
method: 'post',
body: {
userId: this.user.id,
},
}).finally(() => {
this.authenticated = false;
useCookie('auth').value = undefined;
useCookie("user").value = undefined;
});
},
refreshSession() {
// Use useFetch to not call
return useFetch('auth/refresh-token', {
baseURL: useRuntimeConfig().public.baseURL,
method: 'post',
body: {
refreshToken: this.auth.refreshToken,
},
}, false).then((response) => {
this.authenticated = true;
this.auth.token = response.data;
useCookie('auth').value = JSON.stringify(this.auth);
}).catch(() => {
this.authenticated = false;
useCookie('auth').value = undefined;
useCookie("user").value = undefined;
});
}
},
});

19
frontend/store/axe.ts Normal file
View File

@@ -0,0 +1,19 @@
import {defineStore} from 'pinia';
export interface Axe {
id: number;
identifier: number;
shortTitle: string;
description: string;
title: string;
color: string;
}
export const useAxeStore = defineStore('axe', {
state: () => ({}),
actions: {
findAxes() {
return useApi("axes");
}
}
});

45
frontend/store/bundle.ts Normal file
View File

@@ -0,0 +1,45 @@
import {defineStore} from 'pinia';
export interface Bundle {
id: number;
label: string;
presentation: string;
lastQuizzDate: string;
numberOfQuizzes: number;
}
export interface QuestionCreation {
label: string;
description: string;
axeId: number;
index: number;
}
export interface BundleCreationRequest {
label: string;
presentation: string;
questions: QuestionCreation[];
}
export const useBundleStore = defineStore('bundle', {
state: () => ({
selectedBundle: ref<Bundle>()
}),
actions: {
findAll(): Bundle[] {
return useApi("bundles");
},
setCurrentBundle(bundle: Bundle) {
this.selectedBundle = bundle;
},
create(request: BundleCreationRequest) {
return useApi("bundles", {
method: "POST",
body: request
});
}
}
});

View File

@@ -1,5 +0,0 @@
import { Store } from 'vuex'
import { initialiseStores } from '~/utils/store-accessor'
const initializer = (store: Store<any>) => initialiseStores(store)
export const plugins = [initializer]
export * from '~/utils/store-accessor'

View File

@@ -0,0 +1,29 @@
import {defineStore} from 'pinia';
interface Notification {
message?: string;
details?: string[] | string;
}
export const useNotificationStore = defineStore('notification', {
state: () => ({
hasNotification: false,
type: "info",
notification: {},
}),
actions: {
pushNotification(type: 'warn' | 'info' | 'success', notification: Notification) {
this.notification = notification;
this.hasNotification = true;
this.type = type;
setTimeout(() => {
this.clearNotification();
}, 5000);
},
clearNotification() {
this.notification = {};
this.type = 'info';
this.hasNotification = false;
}
}
});

View File

@@ -0,0 +1,29 @@
import {defineStore} from 'pinia';
export interface Question {
id: number;
label: string;
description: string;
}
export const useQuestionStore = defineStore('question', {
state: () => ({}),
actions: {
findDefaults(axeId: number): Promise<Question> {
return useApi("/questions/search/defaults", {
params: {
axeId
}
});
},
findAll(axeId: number): Promise<Question[]> {
return useApi("/questions/search", {
params: {
axeId,
}
});
}
}
});

View File

@@ -1,76 +1,155 @@
import {Module, VuexModule, Mutation} from 'vuex-module-decorators'
import {Question} from "~/repositories/models/question.model";
import {QuizRate, Response} from "~/repositories/models/quiz.model";
import {defineStore} from 'pinia';
import {useBundleStore} from "~/store/bundle";
import {Axe, useAxeStore} from "~/store/axe";
import type {Question} from "~/store/question";
@Module({
name: 'quiz',
stateFactory: true,
namespaced: true,
})
export default class Quiz extends VuexModule {
responses = new Map<number, QuizRate>;
questionsRatedPerAxe = new Map<number, { questionId: number; rated: boolean }[]>;
@Mutation
initialize(questions: Map<number, Question[]>) {
questions.forEach((questions, axeId) => this.questionsRatedPerAxe.set(axeId, questions.map(value => {
return {
questionId: value.id,
rated: this.responses.has(value.id)
}
})));
}
@Mutation
reset() {
this.responses.clear();
this.questionsRatedPerAxe.forEach((questions) => {
questions
.map(value => {
value.rated = false;
return value;
});
});
}
@Mutation
updateScoreResponse(response: Response) {
const previous = this.responses.get(response.questionId);
if (previous) {
this.responses.set(response.questionId, {
comment: previous.comment,
score: response.score
});
} else {
this.responses.set(response.questionId, {
score: response.score
});
}
const questionsRated = this.questionsRatedPerAxe.get(response.axeId);
if (questionsRated) {
questionsRated
.filter(value => value.questionId === response.questionId)
.map(value => {
value.rated = true;
return value;
});
}
// else should not happen
}
@Mutation
updateCommentResponse(response: Response) {
const previous = this.responses.get(response.questionId);
if (previous) {
this.responses.set(response.questionId, {
score: previous.score,
comment: response.comment
});
} else {
this.responses.set(response.questionId, {
comment: response.comment
});
}
}
export interface QuizResponse {
question: string;
score: number;
comment: string;
}
export interface AxeResponses {
axeIdentifier: number;
average: number;
responses: QuizResponse[];
}
export interface Quiz {
id: number;
createdDate: string;
axes: AxeResponses[]
}
export interface Response {
axeId: number;
questionId: number;
score?: number;
comment?: string;
}
export interface QuizRate { // ?
score?: number;
comment?: string;
}
export const useQuizStore = defineStore('quiz', {
state: () => ({
axes: ref<Axe[]>([]),
questions: ref<Map<number, Question[]>>(new Map()),
responses: ref<Map<number, QuizRate>>(new Map()),
questionsRatedPerAxe: ref<Map<number, Array<{ questionId: number; rated: boolean }>>>(new Map())
}),
actions: {
initialize() {
const bundle = useBundleStore().selectedBundle;
return useAxeStore().findAxes().then(axes => {
const promises: any[] = [];
this.axes = axes;
axes.forEach(axe => {
promises.push(
useApi(`/bundles/${bundle.id}/questions/search`, {
params: {
axeId: axe.id
}
})
.then((questions: Question[]) => {
return {
axeId: axe.identifier,
questions: questions
};
}));
});
Promise.all(promises).then((axeQuestions) => {
axeQuestions.forEach(axeQuestion => {
this.questions.set(axeQuestion.axeId, axeQuestion.questions)
});
this.questions.forEach((questions, axeId) => this.questionsRatedPerAxe.set(axeId, questions.map(value => {
return {
questionId: value.id,
rated: this.responses.has(value.id)
}
})));
});
});
},
resetResponses() {
this.responses.clear();
this.questionsRatedPerAxe.forEach((questions) => {
questions
.map(value => {
value.rated = false;
return value;
});
});
},
updateScoreResponse(response: Response) {
const previous = this.responses.get(response.questionId);
if (previous) {
this.responses.set(response.questionId, {
comment: previous.comment,
score: response.score
});
} else {
this.responses.set(response.questionId, {
score: response.score
});
}
const questionsRated = this.questionsRatedPerAxe.get(response.axeId);
if (questionsRated) {
questionsRated
.filter(value => value.questionId === response.questionId)
.map(value => {
value.rated = true;
return value;
});
}
// else should not happen
},
updateCommentResponse(response: Response) {
const previous = this.responses.get(response.questionId);
if (previous) {
this.responses.set(response.questionId, {
score: previous.score,
comment: response.comment
});
} else {
this.responses.set(response.questionId, {
comment: response.comment
});
}
},
findQuizzes(bundleId: number) {
return useApi("/quizzes/search", {
params: {
bundleId: bundleId,
sort: "createdDate,desc"
}
});
},
findById(quizId: number) {
return useApi(`/quizzes/${quizId}`);
},
save() {
const responses = [];
this.responses.forEach((value, key) => {
responses.push({
score: value.score ? value.score : 0,
comment: value.comment,
questionId: key
});
});
return useApi("/quizzes", {
method: "POST",
body: {responses, bundleId: useBundleStore().selectedBundle.id}
}).finally(() => this.resetResponses());
}
}
});