Merge branch 'develop' into milestone/ff-admin-unit

# Conflicts:
#	package-lock.json
#	package.json
This commit is contained in:
Julian Krauser 2025-07-25 09:29:01 +02:00
commit 0b16599d2a
13 changed files with 198 additions and 107 deletions

View file

@ -6,7 +6,7 @@
<div class="w-full flex flex-row gap-2 h-full align-middle">
<TopLevelLink
v-if="routeName == 'admin' || routeName.includes('admin-')"
v-for="item in topLevel"
v-for="item in topLevelObject"
:key="item.key"
:link="item"
:disableSubLink="true"
@ -34,7 +34,7 @@ import TopLevelLink from "./admin/TopLevelLink.vue";
export default defineComponent({
computed: {
...mapState(useAuthStore, ["authCheck"]),
...mapState(useNavigationStore, ["topLevel"]),
...mapState(useNavigationStore, ["topLevelObject"]),
routeName() {
return typeof this.$route.name == "string" ? this.$route.name : "";
},

View file

@ -10,7 +10,7 @@
<div v-if="authCheck" class="hidden md:flex flex-row gap-2 h-full align-middle">
<TopLevelLink
v-if="routeName == 'admin' || routeName.includes('admin-')"
v-for="item in topLevel"
v-for="item in topLevelObject"
:key="item.key"
:link="item"
/>
@ -46,7 +46,7 @@ import { useConfigurationStore } from "@/stores/configuration";
export default defineComponent({
computed: {
...mapState(useAuthStore, ["authCheck"]),
...mapState(useNavigationStore, ["topLevel"]),
...mapState(useNavigationStore, ["topLevelObject"]),
...mapState(useConfigurationStore, ["clubName"]),
routeName() {
return typeof this.$route.name == "string" ? this.$route.name : "";

View file

@ -19,11 +19,16 @@ export async function abilityAndNavUpdate(to: any, from: any, next: any) {
navigation.updateNavigation();
NProgress.done();
next();
} else if ((admin && ability.isAdmin()) || ability.can(type, section, module)) {
} else if (module && ((admin && ability.isAdmin()) || ability.can(type, section, module))) {
NProgress.done();
navigation.activeNavigation = to.name.split("-")[1];
navigation.activeLink = to.name.split("-")[2];
next();
} else if (!module && ((admin && ability.isAdmin()) || ability.canSection(type, section))) {
NProgress.done();
navigation.activeNavigation = to.name.split("-")[1];
navigation.activeLink = null;
next();
} else {
NProgress.done();
next({ name: "admin-default" });

View file

@ -33,7 +33,8 @@ export const useAbilityStore = defineStore("ability", {
if (type == "admin") return permissions?.admin ?? permissions?.adminByOwner ?? false;
if (permissions?.admin || permissions?.adminByOwner) return true;
if (
(permissions[section]?.all == "*" || permissions[section]?.all?.includes(type)) &&
permissions[section]?.all == "*" ||
permissions[section]?.all?.includes(type) ||
permissions[section] != undefined
)
return true;

View file

@ -49,7 +49,8 @@ export const useMembershipStore = defineStore("membership", {
http
.get(`/admin/member/${memberId}/memberships/totalstatistics`)
.then((result) => {
this.totalMembershipStatistics = result.data;
if (result.status == 200) this.totalMembershipStatistics = result.data;
else this.totalMembershipStatistics = undefined;
})
.catch((err) => {});
},

View file

@ -37,6 +37,11 @@ export const useNavigationStore = defineStore("navigation", {
};
},
getters: {
topLevelObject: (state) =>
state.topLevel.map((tl) => ({
...tl,
levelDefault: state.navigation[tl.key].main.filter((m) => !m.key.includes("divider"))[0]?.key ?? "",
})),
activeNavigationObject: (state) => (state.navigation[state.activeNavigation] ?? {}) as navigationSplitModel,
activeTopLevelObject: (state) =>
(state.topLevel.find((elem) => elem.key == state.activeNavigation) ?? {}) as topLevelNavigationModel,

View file

@ -1,5 +1,5 @@
<template>
<div class="flex flex-col items-center">
<div class="flex flex-col gap-2 items-center">
<br />
<h1 class="w-full p-4 text-center font-bold text-3xl">Kein Zugriff</h1>
<br />

View file

@ -24,7 +24,7 @@
type="text"
name="title"
id="title"
placeholder="Entscheidung"
placeholder="Beschluss"
autocomplete="off"
v-model="item.topic"
@keyup.prevent
@ -57,7 +57,7 @@
<QuillEditor
id="top"
theme="snow"
placeholder="Entscheidung Inhalt..."
placeholder="Beschluss Inhalt..."
style="height: 250px; max-height: 250px; min-height: 250px"
contentType="html"
:toolbar="toolbarOptions"

View file

@ -57,8 +57,8 @@
<QuillEditor
id="top"
theme="snow"
placeholder="Entscheidung Inhalt..."
style="height: 100px; max-height: 100px; min-height: 100px"
placeholder="Abstimmung Inhalt..."
style="height: 150px; max-height: 150px; min-height: 150px"
contentType="html"
:toolbar="toolbarOptions"
v-model:content="item.context"

View file

@ -157,6 +157,8 @@ export default defineComponent({
this.inviteStatus = "success";
localStorage.setItem("accessToken", result.data.accessToken);
localStorage.setItem("refreshToken", result.data.refreshToken);
localStorage.setItem("routine", this.tab);
localStorage.setItem("username", this.username);
setTimeout(() => {
this.$router.push(`/admin`);
}, 1000);

View file

@ -15,15 +15,61 @@
<RouterLink to="/setup" class="text-primary">Zum Einrichtungsstart</RouterLink>
</div>
<form v-else class="flex flex-col gap-2" @submit.prevent="setup">
<img :src="image" alt="totp" class="w-56 h-56 self-center" />
<div class="w-full flex flex-row gap-2 justify-center">
<p
class="w-1/2 p-0.5 pl-0 rounded-lg py-2.5 text-sm text-center font-medium leading-5 outline-hidden cursor-pointer"
:class="
tab == 'totp' ? 'bg-red-200 shadow-sm border-b-2 border-primary rounded-b-none' : ' hover:bg-red-200'
"
@click="tab = 'totp'"
>
TOTP
</p>
<p
class="w-1/2 p-0.5 rounded-lg py-2.5 text-sm text-center font-medium leading-5 outline-hidden cursor-pointer"
:class="
tab == 'password' ? 'bg-red-200 shadow-sm border-b-2 border-primary rounded-b-none' : 'hover:bg-red-200'
"
@click="tab = 'password'"
>
Passwort
</p>
</div>
<p class="text-center">Dein Nutzername: {{ username }}</p>
<div v-if="tab == 'totp'" class="flex flex-col gap-2">
<img :src="image" alt="totp" class="w-56 h-56 self-center" />
<TextCopy :copyText="otp" />
<TextCopy :copyText="otp" />
<div class="-space-y-px">
<div>
<input id="totp" name="totp" type="text" required placeholder="TOTP" />
<div class="-space-y-px">
<div>
<input id="totp" name="totp" type="text" required placeholder="TOTP" />
</div>
</div>
</div>
<div v-else>
<input
id="password"
name="password"
type="password"
required
placeholder="Passwort"
class="rounded-b-none!"
autocomplete="new-password"
:class="notMatching ? 'border-red-600!' : ''"
/>
<input
id="password_rep"
name="password_rep"
type="password"
required
placeholder="Passwort wiederholen"
class="rounded-t-none!"
autocomplete="new-password"
:class="notMatching ? 'border-red-600!' : ''"
/>
<p v-if="notMatching">Passwörter stimmen nicht überein</p>
</div>
<div class="flex flex-row gap-2">
<button type="submit" primary :disabled="setupStatus == 'loading' || setupStatus == 'success'">
@ -51,6 +97,7 @@ import { RouterLink } from "vue-router";
import FormBottomBar from "@/components/FormBottomBar.vue";
import TextCopy from "@/components/TextCopy.vue";
import AppLogo from "@/components/AppLogo.vue";
import { hashString } from "@/helpers/crypto";
</script>
<script lang="ts">
@ -61,11 +108,14 @@ export default defineComponent({
},
data() {
return {
tab: "totp",
verification: "loading" as "success" | "loading" | "failed",
image: undefined as undefined | string,
otp: undefined as undefined | string,
username: "" as string,
setupStatus: undefined as undefined | "loading" | "success" | "failed",
setupError: "" as string,
notMatching: false as boolean,
};
},
mounted() {
@ -79,6 +129,7 @@ export default defineComponent({
this.verification = "success";
this.image = result.data.dataUrl;
this.otp = result.data.otp;
this.username = result.data.username;
}, 1000);
})
.catch((err) => {
@ -88,20 +139,28 @@ export default defineComponent({
});
},
methods: {
setup(e: any) {
let formData = e.target.elements;
async setup(e: any) {
let secret = "";
if (this.tab == "totp") secret = this.totp(e);
else secret = await this.password(e);
if (secret == "") return;
this.setupStatus = "loading";
this.setupError = "";
this.$http
.post(`/setup/finish`, {
token: this.token,
mail: this.mail,
totp: formData.totp.value,
secret: secret,
routine: this.tab,
})
.then((result) => {
this.setupStatus = "success";
localStorage.setItem("accessToken", result.data.accessToken);
localStorage.setItem("refreshToken", result.data.refreshToken);
localStorage.setItem("routine", this.tab);
localStorage.setItem("username", this.username);
setTimeout(() => {
this.$router.push(`/admin`);
}, 1000);
@ -111,6 +170,24 @@ export default defineComponent({
this.setupError = err.response.data;
});
},
totp(e: any) {
let formData = e.target.elements;
return formData.totp.value;
},
async password(e: any) {
let formData = e.target.elements;
let new_pw = await hashString(formData.password.value);
let new_rep = await hashString(formData.password_rep.value);
if (new_pw != new_rep) {
this.notMatching = true;
return "";
}
this.notMatching = false;
return await hashString(formData.password.value);
},
},
});
</script>