diff --git a/packages/website/src/App.vue b/packages/website/src/App.vue index f105cb77..84f4ac38 100644 --- a/packages/website/src/App.vue +++ b/packages/website/src/App.vue @@ -61,7 +61,7 @@ async function loginState() { console.log(`Logged In: ${loggedIn.value} New User: `, newUser); if (!loggedIn.value && newUser) { - await userStore.changeUser(newUser, "login"); + await userStore.changeUser(newUser); } else { throw Error(); } diff --git a/packages/website/src/Httpclient.ts b/packages/website/src/Httpclient.ts index 56b06ef1..25b225ac 100644 --- a/packages/website/src/Httpclient.ts +++ b/packages/website/src/Httpclient.ts @@ -336,7 +336,6 @@ export const HttpClient = { return Promise.reject(new Error("already logged in")); } if (psw !== pswRepeat) { - // TODO show incorrect password return Promise.reject(new Error("repeated password does not match new password")); } @@ -347,7 +346,7 @@ export const HttpClient = { }, logout(): Promise { - return this.queryServer(serverRestApi.api.user.logout.post).then((result) => result.loggedOut); + return this.queryServer(serverRestApi.api.user.logout.post); }, getExternalUser(): Promise { diff --git a/packages/website/src/components/app-header.vue b/packages/website/src/components/app-header.vue index 0cd49b34..2e47fe7a 100644 --- a/packages/website/src/components/app-header.vue +++ b/packages/website/src/components/app-header.vue @@ -55,6 +55,10 @@ const userStore = useUserStore(); // DATA const loggedInItems = [ + { + label: "Home", + to: { name: "home" }, + }, { label: "Add Medium", to: { name: "addMedium" }, @@ -131,12 +135,7 @@ const notifications = ref([]); // COMPUTED const menuItems = computed(() => { - const items: MenuItem[] = [ - { - label: "Home", - to: { name: "home" }, - }, - ]; + const items: MenuItem[] = []; if (userStore.loggedIn) { items.push( @@ -177,6 +176,7 @@ function logout(): void { }); }); } + function checkNotifications() { getLatestNotifications(); // check every minute at most @@ -186,6 +186,11 @@ function checkNotifications() { async function getLatestNotifications() { await userStore.checkNotificationCounts(); + if (!userStore.loggedIn) { + notifications.value = []; + return; + } + const now = new Date(); now.setDate(now.getDate() - 5); const data = await HttpClient.getNotifications(now, false, 5); diff --git a/packages/website/src/components/external-user.vue b/packages/website/src/components/external-user.vue index 19cbbe6e..8683fc19 100644 --- a/packages/website/src/components/external-user.vue +++ b/packages/website/src/components/external-user.vue @@ -87,14 +87,15 @@ const data = reactive({ const filteredData = computed(() => { return externalUserStore.externalUser .filter((value) => value) - .map((value): ExternalUserItem => { + .map((value): ExternalUserItem | undefined => { const host = data.hosts.find((hostValue) => hostValue.value === value.type); if (!host) { - // FIXME: do not throw error, display error and filter this out - throw Error("no host for external user: " + JSON.stringify(value)); + console.warn("no host for external user:", value); + return undefined; } return { ...value, host }; - }); + }) + .filter((value): value is ExternalUserItem => !!value); }); // FUNCTIONS diff --git a/packages/website/src/components/modal/add-unused-modal.vue b/packages/website/src/components/modal/add-unused-modal.vue index e07d571c..122cbd48 100644 --- a/packages/website/src/components/modal/add-unused-modal.vue +++ b/packages/website/src/components/modal/add-unused-modal.vue @@ -148,7 +148,7 @@ - +
- - - - - - - - diff --git a/packages/website/src/router.ts b/packages/website/src/router.ts index 6110baf3..286b3525 100644 --- a/packages/website/src/router.ts +++ b/packages/website/src/router.ts @@ -1,4 +1,6 @@ import { createRouter, createWebHistory } from "vue-router"; +import { pinia } from "./store/pinia"; +import { useUserStore } from "./store/store"; const router = createRouter({ history: createWebHistory(process.env.BASE_URL), @@ -228,4 +230,12 @@ const router = createRouter({ ], }); +const userStore = useUserStore(pinia); + +router.beforeEach((to) => { + if (!userStore.loggedIn && (!to.name || !["login", "register"].includes(to.name.toString()))) { + return { name: "login" }; + } +}); + export default router; diff --git a/packages/website/src/store/lists.ts b/packages/website/src/store/lists.ts index 99bea18d..09718c13 100644 --- a/packages/website/src/store/lists.ts +++ b/packages/website/src/store/lists.ts @@ -20,18 +20,9 @@ export const useListStore = defineStore("lists", { userListsLocal(lists: List[]): void { this.lists = lists.map((list) => ({ ...list, external: false })); }, - addListLocal(list: StoreInternalList) { - list.external = false; - this.lists.push(list); - }, - deleteListLocal(id: number) { - const index = this.lists.findIndex((value) => value.id === id); - if (index < 0) { - throw Error("invalid listId"); - } - this.lists.splice(index, 1); - }, - removeListItemLocal(payload: { listId: number; mediumId: number }) { + async deleteListItem(payload: { listId: number; mediumId: number }) { + await HttpClient.deleteListItem({ listId: payload.listId, mediumId: [payload.mediumId] }); + const list = this.lists.find((value) => value.id === payload.listId); if (!list) { throw Error("invalid listId"); @@ -45,7 +36,13 @@ export const useListStore = defineStore("lists", { } list.items.push(payload.mediumId); }, - updateListLocal(updateList: List) { + async updateList(updateList: List) { + const success = await HttpClient.updateList(updateList); + + if (!success) { + return false; + } + const list = this.lists.find((value: List) => value.id === updateList.id); if (list) { @@ -53,6 +50,7 @@ export const useListStore = defineStore("lists", { } else { console.error("Cannot find list to update for id:", updateList.id); } + return true; }, async loadLists() { try { @@ -65,23 +63,28 @@ export const useListStore = defineStore("lists", { }, async addList(data: { name: string; medium: number }) { - // TODO: check if list already exists + if (this.lists.find((value) => value.name === data.name)) { + throw Error("Duplicate List Name"); + } if (!data.name) { - // TODO: commit("addListModalError", "Missing name"); - } else if (!data.medium) { - // TODO: commit("addListModalError", "Missing type"); - } else { - return HttpClient.createList({ list: { name: data.name, medium: data.medium } }).then((list) => { - // @ts-expect-error - this.addListLocal(list); - // TODO: commit("resetModal", "addList"); - }); + throw Error("Missing name"); + } + if (!data.medium) { + throw Error("Missing Type"); } + + const list = await HttpClient.createList({ list: { name: data.name, medium: data.medium } }); + this.lists.push({ ...list, external: false }); }, async deleteList(id: number) { await HttpClient.deleteList(id); - this.deleteListLocal(id); + + const index = this.lists.findIndex((value) => value.id === id); + if (index < 0) { + throw Error("invalid listId"); + } + this.lists.splice(index, 1); }, }, }); diff --git a/packages/website/src/store/media.ts b/packages/website/src/store/media.ts index b4b766fe..197bcd5a 100644 --- a/packages/website/src/store/media.ts +++ b/packages/website/src/store/media.ts @@ -32,27 +32,13 @@ export const useMediaStore = defineStore("media", { }, }, actions: { - addMediumLocal(medium: Medium | Medium[]) { - if (Array.isArray(medium)) { - medium.forEach((item) => { - if (!item.id) { - throw Error("missing id on medium"); - } - this.media[item.id] = item; - }); - } else { - if (!medium.id) { - throw Error("missing id on medium"); - } - this.media[medium.id] = medium; - } - }, updateMediumLocal(medium: SimpleMedium) { if (!medium.id) { throw Error("missing id on medium"); } Object.assign(this.media[medium.id], medium); }, + deleteMediumLocal(id: number) { if (!(id in this.media)) { throw Error("invalid mediumId"); @@ -68,6 +54,7 @@ export const useMediaStore = defineStore("media", { } }); }, + deleteTocLocal(data: { mediumId: number; link: string }) { const medium = this.secondaryMedia[data.mediumId]; if (!medium) { @@ -75,6 +62,7 @@ export const useMediaStore = defineStore("media", { } medium.tocs = medium.tocs.filter((toc) => toc.link !== data.link); }, + async loadMedia() { try { const [data, secondaryData] = await Promise.all([HttpClient.getAllMedia(), HttpClient.getAllSecondaryMedia()]); @@ -101,23 +89,20 @@ export const useMediaStore = defineStore("media", { console.error(error); } }, - addMedium(data: AddMedium) { + + async addMedium(data: AddMedium) { if (!data.title) { - // TODO: commit("addMediumModalError", "Missing title"); + throw Error("Missing title"); } else if (!data.medium) { - // TODO: commit("addMediumModalError", "Missing type"); + throw Error("Missing Type"); } else { - HttpClient.createMedium(data) - .then((medium) => { - // @ts-expect-error - this.addMediumLocal(medium); - }) - .catch((error) => { - // TODO: notify error - console.error(error); - }); + const medium = await HttpClient.createMedium(data); + if (!medium.id) { + throw Error("missing id on medium"); + } + this.media[medium.id] = medium as Medium; + return medium; } - // TODO implement addMedium }, editMedium(data: { id: number; prop: string }) { diff --git a/packages/website/src/store/store.ts b/packages/website/src/store/store.ts index e566f966..bf795f92 100644 --- a/packages/website/src/store/store.ts +++ b/packages/website/src/store/store.ts @@ -14,6 +14,8 @@ function userClear(state: UserState) { state.name = ""; state.uuid = ""; state.session = ""; + state.user.readNotificationsCount = 0; + state.user.unreadNotificationsCount = 0; useListStore().lists = []; useMediaStore().media = {}; useExternalUserStore().externalUser = []; @@ -30,11 +32,6 @@ export const useUserStore = defineStore("user", { persist: true, state: () => ({ user: { - settings: {}, - columns: [ - { name: "Author", prop: "author", show: true }, - { name: "Artist", prop: "artist", show: true }, - ], unreadNotificationsCount: 0, readNotificationsCount: 0, }, @@ -61,44 +58,36 @@ export const useUserStore = defineStore("user", { hookStore.loadHooks(), ]); }, - async changeUser(user: User, modal?: string) { + + async changeUser(user: User) { const userChanged = user && this.uuid !== user.uuid; this.$patch((state) => setUser(state, user)); - if (modal) { - // TODO: commit("resetModal", modal); - } - if (userChanged) { - // TODO: load first and then change page, or reverse? - await this.load(); - - if (router.currentRoute.value.path === "/login") { + if (router.currentRoute.value.path === "/login" || router.currentRoute.value.path === "/register") { // automatically navigate to view under home if successfully logged in await router.push("/").catch(console.error); - console.log("pushed home"); } + + await this.load(); } }, + async login(data: { user: string; pw: string }) { if (!data.user) { - // TODO: commit("loginModalError", "Username is missing"); - return; - } else if (!data.pw) { - // TODO: commit("loginModalError", "Password is missing"); - return; + throw Error("Username is missing"); } - try { - // FIXME modal does not close after successful login - const newUser = await HttpClient.login(data.user, data.pw); - this.changeUser(newUser, "login"); - } catch (error) { - // TODO: commit("loginModalError", String(error)); + if (!data.pw) { + throw Error("Password is missing"); } + const newUser = await HttpClient.login(data.user, data.pw); + this.changeUser(newUser); }, + immediateLogout() { this.$patch(userClear); }, + async logout() { const loggedOut = await HttpClient.logout(); this.$patch(userClear); @@ -107,25 +96,28 @@ export const useUserStore = defineStore("user", { throw Error("An error occurred while logging out"); } }, + async register(data: { user: string; pw: string; pwRepeat: string }) { if (!data.user) { - // TODO: commit("registerModalError", "Username is missing"); - } else if (!data.pw) { - // TODO: commit("registerModalError", "Password is missing"); - } else if (!data.pwRepeat) { - // TODO: commit("registerModalError", "Password was not repeated"); - } else if (data.pwRepeat !== data.pw) { - // TODO: commit("registerModalError", "Repeated Password is not password"); - } else { - try { - const newUser = await HttpClient.register(data.user, data.pw, data.pwRepeat); - await this.changeUser(newUser, "register"); - } catch (error) { - // TODO: commit("registerModalError", String(error)); - } + throw Error("Username is missing"); + } + if (!data.pw) { + throw Error("Password is missing"); + } + if (!data.pwRepeat) { + throw Error("Password was not repeated"); + } + if (data.pwRepeat !== data.pw) { + throw Error("Repeated Password is not password"); } + const newUser = await HttpClient.register(data.user, data.pw, data.pwRepeat); + await this.changeUser(newUser); }, + async checkNotificationCounts() { + if (!this.loggedIn) { + return; + } const [unreadCount, readCount] = await Promise.all([ HttpClient.getNotificationsCount(false), HttpClient.getNotificationsCount(true), @@ -133,11 +125,19 @@ export const useUserStore = defineStore("user", { this.user.unreadNotificationsCount = unreadCount; this.user.readNotificationsCount = readCount; }, + async readNotification(data: UserNotification) { + if (!this.loggedIn) { + return; + } await HttpClient.readNotification(data.id); this.checkNotificationCounts(); }, + async readAllNotifications() { + if (!this.loggedIn) { + return; + } await HttpClient.readAllNotifications(); this.checkNotificationCounts(); }, diff --git a/packages/website/src/views/AddMedium.vue b/packages/website/src/views/AddMedium.vue index 6fe4f254..cf042d5a 100644 --- a/packages/website/src/views/AddMedium.vue +++ b/packages/website/src/views/AddMedium.vue @@ -209,7 +209,7 @@ diff --git a/packages/website/src/views/Lists.vue b/packages/website/src/views/Lists.vue index 766b66ff..cbc409f3 100644 --- a/packages/website/src/views/Lists.vue +++ b/packages/website/src/views/Lists.vue @@ -197,7 +197,7 @@ import { import typeIcon from "../components/type-icon.vue"; import releaseState from "../components/release-state.vue"; import MediaFilter from "../components/media-filter.vue"; -import { computed, ref, watch, watchEffect } from "vue"; +import { computed, ref, watchEffect } from "vue"; import { mergeMediaToc } from "../init"; import { FilterMatchMode, FilterService } from "primevue/api"; import { DataTableRowEditCancelEvent } from "primevue/datatable"; @@ -241,7 +241,6 @@ const selectedMedium = ref(null); const mediumSuggestions = ref([]); const editingList = ref(emptyMinList()); const editingRows = ref([]); -const loadingMedia = ref([]); const selectedList = ref(null); const displayEditDialog = ref(false); const filters = { @@ -333,31 +332,6 @@ watchEffect(() => { editingList.value = emptyMinList(); } }); - -watch(loadingMedia, (newValue: number[][], oldValue: number[][]) => { - // TODO: use function or remove it - console.log("New:", newValue, "Old", oldValue); - const missingBatches = newValue.filter((batch) => !oldValue.includes(batch)); - - for (const missingBatch of missingBatches) { - // load missing media - HttpClient.getMedia(missingBatch) - .then((media) => mediaStore.addMediumLocal(media)) - .catch((error) => { - // TODO: display error - console.log(error); - }) - .finally(() => { - // remove batch so it will be regarded as loaded, regardless whether it failed or not - const index = loadingMedia.value.indexOf(missingBatch); - - if (index > 0) { - loadingMedia.value.splice(index, 1); - } - }); - } -}); - // LIFECYCLE EVENTS // FUNCTIONS @@ -396,10 +370,10 @@ function saveList() { }; editListLoading.value = true; - HttpClient.updateList(currentList) + listsStore + .updateList(currentList) .then((success) => { if (success) { - listsStore.updateListLocal(currentList); toast.add({ severity: "success", summary: "Saved", detail: "List changes were saved", life: 3000 }); } else { toast.add({ @@ -435,11 +409,10 @@ function deleteList() { icon: "pi pi-exclamation-triangle", acceptClass: "p-button-danger", accept: () => { - // TODO: make this an action deleteListLoading.value = true; - HttpClient.deleteList(listId) + listsStore + .deleteList(listId) .then(() => { - listsStore.deleteListLocal(listId); // select the next list selectedList.value = displayedLists.value.find((list) => list.id !== listId) || null; toast.add({ severity: "info", summary: "Confirmed", detail: "Record deleted", life: 3000 }); @@ -471,9 +444,9 @@ function confirmDeleteListItem(data: Medium) { accept: () => { deleteItemLoading.value = true; - HttpClient.deleteListItem({ listId: list.id, mediumId: [mediumId] }) + listsStore + .deleteListItem({ listId: list.id, mediumId }) .then(() => { - listsStore.removeListItemLocal({ listId: list.id, mediumId }); toast.add({ severity: "info", summary: "Confirmed", detail: "Item removed from List", life: 3000 }); }) .catch((reason) => { diff --git a/packages/website/src/views/Login.vue b/packages/website/src/views/Login.vue index 2e0e8af6..f623a63b 100644 --- a/packages/website/src/views/Login.vue +++ b/packages/website/src/views/Login.vue @@ -2,29 +2,21 @@

Login

-
- - +
+ +
+ +
-
- - +
+ +
+ +
- +
Forgot your password?
-
+
{{ data.error }}
@@ -38,9 +30,28 @@ const data = reactive({ show: true, user: "", pw: "", + loading: false, }); function sendForm(): void { - useUserStore().login({ user: data.user, pw: data.pw }); + if (!data.user.trim() || !data.pw.trim()) { + return; + } + data.loading = true; + useUserStore() + .login({ user: data.user, pw: data.pw }) + .catch((reason) => { + if (reason instanceof Error) { + data.error = reason.message; + } else { + data.error = JSON.stringify(reason); + } + }) + .finally(() => (data.loading = false)); } + diff --git a/packages/website/src/views/Register.vue b/packages/website/src/views/Register.vue index 64c4953b..148767ac 100644 --- a/packages/website/src/views/Register.vue +++ b/packages/website/src/views/Register.vue @@ -1,62 +1,52 @@ +