Merge branch 'milestone/ff-admin-unit' into unit/#70-build-ui-demo
# Conflicts: # package-lock.json # package.json # src/router/club/newsletterGuard.ts # src/router/club/protocolGuard.ts # src/router/index.ts # src/types/permissionTypes.ts # src/views/admin/club/newsletter/NewsletterRecipients.vue
This commit is contained in:
commit
bdc139f37f
107 changed files with 4984 additions and 1742 deletions
|
@ -2,19 +2,34 @@
|
|||
<div class="grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8 pb-20">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<img src="/Logo.png" alt="LOGO" class="h-auto w-full" />
|
||||
<AppLogo />
|
||||
<h2 class="text-center text-4xl font-extrabold text-gray-900">
|
||||
{{ config.app_name_overwrite || "FF Admin" }}
|
||||
{{ clubName }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<form class="flex flex-col gap-2" @submit.prevent="login">
|
||||
<form class="flex flex-col gap-2" @submit.prevent="submit">
|
||||
<div class="-space-y-px">
|
||||
<div>
|
||||
<input id="username" name="username" type="text" required placeholder="Benutzer" class="rounded-b-none!" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
required
|
||||
placeholder="Benutzer"
|
||||
:class="routine == '' ? '' : 'rounded-b-none!'"
|
||||
:value="username"
|
||||
:disabled="username != ''"
|
||||
/>
|
||||
<div v-if="usernameStatus" class="h-full flex items-center justify-center w-5 absolute top-0 right-2">
|
||||
<Spinner v-if="usernameStatus == 'loading'" class="my-auto" />
|
||||
<SuccessCheckmark v-else-if="usernameStatus == 'success'" />
|
||||
<FailureXMark v-else-if="usernameStatus == 'failed'" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="routine != ''">
|
||||
<input
|
||||
v-if="routine == 'totp'"
|
||||
id="totp"
|
||||
name="totp"
|
||||
type="text"
|
||||
|
@ -23,13 +38,26 @@
|
|||
class="rounded-t-none!"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
placeholder="Passwort"
|
||||
class="rounded-t-none!"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<RouterLink :to="{ name: 'reset-start' }" class="w-fit self-end text-primary">TOTP verloren</RouterLink>
|
||||
<p v-if="username != ''" class="w-fit self-end text-primary cursor-pointer" @click="resetRoutine">
|
||||
Benutzer wechseln
|
||||
</p>
|
||||
<RouterLink :to="{ name: 'reset-start' }" class="w-fit self-end text-primary">Zugang verloren</RouterLink>
|
||||
|
||||
<div class="flex flex-row gap-2">
|
||||
<button type="submit" primary :disabled="loginStatus == 'loading' || loginStatus == 'success'">
|
||||
anmelden
|
||||
{{ routine == "" ? "Benutzer prüfen" : "anmelden" }}
|
||||
</button>
|
||||
<Spinner v-if="loginStatus == 'loading'" class="my-auto" />
|
||||
<SuccessCheckmark v-else-if="loginStatus == 'success'" />
|
||||
|
@ -50,7 +78,10 @@ import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
|||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import { resetAllPiniaStores } from "@/helpers/piniaReset";
|
||||
import FormBottomBar from "@/components/FormBottomBar.vue";
|
||||
import { config } from "@/config";
|
||||
import AppLogo from "@/components/AppLogo.vue";
|
||||
import { mapState } from "pinia";
|
||||
import { useConfigurationStore } from "@/stores/configuration";
|
||||
import { hashString } from "@/helpers/crypto";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -58,21 +89,90 @@ export default defineComponent({
|
|||
data() {
|
||||
return {
|
||||
loginStatus: undefined as undefined | "loading" | "success" | "failed",
|
||||
usernameStatus: undefined as undefined | "loading" | "success" | "failed",
|
||||
loginError: "" as string,
|
||||
username: "" as string,
|
||||
routine: "" as string,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useConfigurationStore, ["clubName"]),
|
||||
},
|
||||
mounted() {
|
||||
resetAllPiniaStores();
|
||||
this.username = localStorage.getItem("username") ?? "";
|
||||
this.routine = localStorage.getItem("routine") ?? "";
|
||||
|
||||
if (this.username != "") {
|
||||
this.$http
|
||||
.post(`/auth/kickof`, {
|
||||
username: this.username,
|
||||
})
|
||||
.then((result) => {
|
||||
this.usernameStatus = "success";
|
||||
this.routine = result.data.routine;
|
||||
localStorage.setItem("routine", result.data.routine);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.usernameStatus = "failed";
|
||||
this.loginError = err.response?.data;
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
login(e: any) {
|
||||
resetRoutine() {
|
||||
this.routine = "";
|
||||
this.username = "";
|
||||
localStorage.removeItem("routine");
|
||||
localStorage.removeItem("username");
|
||||
},
|
||||
submit(e: any) {
|
||||
if (this.routine == "") this.kickof(e);
|
||||
else this.login(e);
|
||||
},
|
||||
kickof(e: any) {
|
||||
let formData = e.target.elements;
|
||||
let username = formData.username.value;
|
||||
this.usernameStatus = "loading";
|
||||
this.loginError = "";
|
||||
this.$http
|
||||
.post(`/auth/kickof`, {
|
||||
username: username,
|
||||
})
|
||||
.then((result) => {
|
||||
this.usernameStatus = "success";
|
||||
this.routine = result.data.routine;
|
||||
this.username = username;
|
||||
localStorage.setItem("routine", result.data.routine);
|
||||
localStorage.setItem("username", username);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.usernameStatus = "failed";
|
||||
this.loginError = err.response?.data;
|
||||
})
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
this.usernameStatus = undefined;
|
||||
this.loginError = "";
|
||||
}, 2000);
|
||||
});
|
||||
},
|
||||
async login(e: any) {
|
||||
let formData = e.target.elements;
|
||||
this.loginStatus = "loading";
|
||||
this.loginError = "";
|
||||
|
||||
let secret = "";
|
||||
if (this.routine == "totp") {
|
||||
secret = formData.totp.value;
|
||||
} else {
|
||||
secret = await hashString(formData.password.value);
|
||||
}
|
||||
|
||||
this.$http
|
||||
.post(`/auth/login`, {
|
||||
username: formData.username.value,
|
||||
totp: formData.totp.value,
|
||||
secret: secret,
|
||||
})
|
||||
.then((result) => {
|
||||
this.loginStatus = "success";
|
||||
|
|
|
@ -6,29 +6,41 @@
|
|||
</div>
|
||||
</template>
|
||||
<template #diffMain>
|
||||
<div class="flex flex-col w-full h-full gap-2 justify-between px-7 overflow-hidden">
|
||||
<div class="flex flex-col gap-2">
|
||||
<img :src="image" alt="totp" class="w-56 h-56 self-center" />
|
||||
|
||||
<TextCopy :copyText="otp" />
|
||||
<Spinner v-if="loading" class="mx-auto" />
|
||||
<div v-else class="flex flex-col w-full h-full gap-2 px-7 overflow-hidden">
|
||||
<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>
|
||||
<form class="flex flex-col gap-2" @submit.prevent="verify">
|
||||
<div class="-space-y-px">
|
||||
<div>
|
||||
<input id="totp" name="totp" type="text" required placeholder="TOTP prüfen" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-2">
|
||||
<button type="submit" primary :disabled="verifyStatus == 'loading' || verifyStatus == 'success'">
|
||||
TOTP prüfen
|
||||
</button>
|
||||
<Spinner v-if="verifyStatus == 'loading'" class="my-auto" />
|
||||
<SuccessCheckmark v-else-if="verifyStatus == 'success'" />
|
||||
<FailureXMark v-else-if="verifyStatus == 'failed'" />
|
||||
</div>
|
||||
<p v-if="verifyError" class="text-center">{{ verifyError }}</p>
|
||||
</form>
|
||||
<ChangeToTOTP
|
||||
v-if="currentRoutine == 'password' && tab == 'totp'"
|
||||
:currentRoutine="currentRoutine"
|
||||
@updateCurrent="currentRoutine = 'totp'"
|
||||
/>
|
||||
<ChangeToPassword
|
||||
v-else-if="currentRoutine == 'totp' && tab == 'password'"
|
||||
:currentRoutine="currentRoutine"
|
||||
@updateCurrent="currentRoutine = 'password'"
|
||||
/>
|
||||
<TotpCheckAndScan v-else-if="tab == 'totp'" />
|
||||
<PasswordChange v-else-if="tab == 'password'" />
|
||||
<p v-else>etwas ist schief gelaufen</p>
|
||||
</div>
|
||||
</template>
|
||||
</MainTemplate>
|
||||
|
@ -42,53 +54,34 @@ import Spinner from "@/components/Spinner.vue";
|
|||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import TextCopy from "@/components/TextCopy.vue";
|
||||
import TotpCheckAndScan from "@/components/account/TotpCheckAndScan.vue";
|
||||
import PasswordChange from "@/components/account/PasswordChange.vue";
|
||||
import ChangeToPassword from "@/components/account/ChangeToPassword.vue";
|
||||
import ChangeToTOTP from "@/components/account/ChangeToTOTP.vue";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
verification: "loading" as "success" | "loading" | "failed",
|
||||
image: undefined as undefined | string,
|
||||
otp: undefined as undefined | string,
|
||||
verifyStatus: undefined as undefined | "loading" | "success" | "failed",
|
||||
verifyError: "" as string,
|
||||
loading: false,
|
||||
tab: "",
|
||||
currentRoutine: "",
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.loading = true;
|
||||
this.$http
|
||||
.get(`/user/totp`)
|
||||
.get(`/user/routine`)
|
||||
.then((result) => {
|
||||
this.verification = "success";
|
||||
this.image = result.data.dataUrl;
|
||||
this.otp = result.data.otp;
|
||||
this.tab = result.data.routine;
|
||||
this.currentRoutine = result.data.routine;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.verification = "failed";
|
||||
.catch((err) => {})
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
verify(e: any) {
|
||||
let formData = e.target.elements;
|
||||
this.verifyStatus = "loading";
|
||||
this.verifyError = "";
|
||||
this.$http
|
||||
.post(`/user/verify`, {
|
||||
totp: formData.totp.value,
|
||||
})
|
||||
.then((result) => {
|
||||
this.verifyStatus = "success";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.verifyStatus = "failed";
|
||||
this.verifyError = err.response.data;
|
||||
})
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
this.verifyStatus = undefined;
|
||||
}, 2000);
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
<template>
|
||||
<SidebarLayout>
|
||||
<template #sidebar>
|
||||
<SidebarTemplate
|
||||
mainTitle="Mein Account"
|
||||
:topTitle="config.app_name_overwrite || 'FF Admin'"
|
||||
:showTopList="isOwner"
|
||||
>
|
||||
<SidebarTemplate mainTitle="Mein Account" :topTitle="clubName" :showTopList="isOwner">
|
||||
<template v-if="isOwner" #topList>
|
||||
<RoutingLink
|
||||
title="Administration"
|
||||
|
@ -42,13 +38,14 @@ import SidebarTemplate from "@/templates/Sidebar.vue";
|
|||
import RoutingLink from "@/components/admin/RoutingLink.vue";
|
||||
import { RouterView } from "vue-router";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
import { config } from "@/config";
|
||||
import { useConfigurationStore } from "@/stores/configuration";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
computed: {
|
||||
...mapState(useAbilityStore, ["isOwner"]),
|
||||
...mapState(useConfigurationStore, ["clubName"]),
|
||||
activeRouteName() {
|
||||
return this.$route.name;
|
||||
},
|
||||
|
|
|
@ -4,14 +4,17 @@
|
|||
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
||||
<h1 class="font-bold text-xl h-8">Kalender</h1>
|
||||
<div class="flex flex-row gap-2">
|
||||
<PlusIcon class="text-gray-500 h-5 w-5 cursor-pointer" @click="select" />
|
||||
<PlusIcon
|
||||
class="text-gray-500 h-5 w-5 cursor-pointer"
|
||||
@click="select({ start: '', end: '', allDay: false })"
|
||||
/>
|
||||
<LinkIcon class="text-gray-500 h-5 w-5 cursor-pointer" @click="openLinkModal" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #diffMain>
|
||||
<div class="flex flex-col w-full h-full gap-2 justify-between px-7 overflow-hidden">
|
||||
<FullCalendar :options="calendarOptions" class="max-h-full h-full" />
|
||||
<CustomCalendar :items="formattedItems" @date-select="select" @event-select="eventClick" />
|
||||
</div>
|
||||
</template>
|
||||
</MainTemplate>
|
||||
|
@ -22,14 +25,10 @@ import { defineComponent, markRaw, defineAsyncComponent } from "vue";
|
|||
import { mapActions, mapState } from "pinia";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import MainTemplate from "@/templates/Main.vue";
|
||||
import FullCalendar from "@fullcalendar/vue3";
|
||||
import deLocale from "@fullcalendar/core/locales/de";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import { useCalendarStore } from "@/stores/admin/club/calendar";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
import { LinkIcon, PlusIcon } from "@heroicons/vue/24/outline";
|
||||
import CustomCalendar from "@/components/CustomCalendar.vue";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -40,33 +39,6 @@ export default defineComponent({
|
|||
computed: {
|
||||
...mapState(useCalendarStore, ["formattedItems"]),
|
||||
...mapState(useAbilityStore, ["can"]),
|
||||
calendarOptions() {
|
||||
return {
|
||||
timeZone: "local",
|
||||
locale: deLocale,
|
||||
plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin],
|
||||
initialView: "dayGridMonth",
|
||||
headerToolbar: {
|
||||
left: "dayGridMonth,timeGridWeek",
|
||||
center: "title",
|
||||
right: "prev,today,next",
|
||||
},
|
||||
eventDisplay: "block",
|
||||
weekends: true,
|
||||
editable: true,
|
||||
selectable: true,
|
||||
selectMirror: false,
|
||||
dayMaxEvents: true,
|
||||
weekNumbers: true,
|
||||
displayEventTime: true,
|
||||
nowIndicator: true,
|
||||
weekText: "KW",
|
||||
allDaySlot: false,
|
||||
events: this.formattedItems,
|
||||
select: this.select,
|
||||
eventClick: this.eventClick,
|
||||
};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchCalendars();
|
||||
|
@ -74,22 +46,22 @@ export default defineComponent({
|
|||
methods: {
|
||||
...mapActions(useModalStore, ["openModal"]),
|
||||
...mapActions(useCalendarStore, ["fetchCalendars"]),
|
||||
select(e: any) {
|
||||
select({ start, end, allDay }: { start: string; end: string; allDay: boolean }) {
|
||||
if (!this.can("create", "club", "calendar")) return;
|
||||
this.openModal(
|
||||
markRaw(defineAsyncComponent(() => import("@/components/admin/club/calendar/CreateCalendarModal.vue"))),
|
||||
{
|
||||
start: e?.startStr ?? new Date().toISOString(),
|
||||
end: e?.endStr ?? new Date().toISOString(),
|
||||
allDay: e?.allDay ?? false,
|
||||
start,
|
||||
end,
|
||||
allDay,
|
||||
}
|
||||
);
|
||||
},
|
||||
eventClick(e: any) {
|
||||
eventClick(id: string) {
|
||||
if (!this.can("update", "club", "calendar")) return;
|
||||
this.openModal(
|
||||
markRaw(defineAsyncComponent(() => import("@/components/admin/club/calendar/UpdateCalendarModal.vue"))),
|
||||
e.event.id
|
||||
id
|
||||
);
|
||||
},
|
||||
openLinkModal(e: any) {
|
||||
|
@ -99,55 +71,4 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
locale: deLocale,
|
||||
events: this.absencesList.map((x) => ({
|
||||
id: x.absenceId,
|
||||
start: x.startDate,
|
||||
end: x.endDate,
|
||||
allday: true,
|
||||
backgroundColor: this.getColorForAbsenceType(x.absenceType),
|
||||
borderColor: '#ffffff',
|
||||
title: this.getAbsenceType(x.absenceType) + ' ' + x.fullName,
|
||||
})),
|
||||
plugins: [
|
||||
interactionPlugin,
|
||||
dayGridPlugin,
|
||||
timeGridPlugin,
|
||||
listPlugin,
|
||||
multiMonthPlugin,
|
||||
],
|
||||
initialView: 'dayGridMonth',
|
||||
eventDisplay: 'block',
|
||||
weekends: false,
|
||||
editable: true,
|
||||
selectable: true,
|
||||
selectMirror: true,
|
||||
dayMaxEvents: true,
|
||||
weekNumbers: true,
|
||||
displayEventTime: false,
|
||||
weekText: 'KW',
|
||||
validRange: { start: '2023-01-01', end: '' },
|
||||
headerToolbar: {
|
||||
left: 'today prev,next',
|
||||
center: 'title',
|
||||
right: 'listMonth,dayGridMonth,multiMonthYear,customview',
|
||||
},
|
||||
views: {
|
||||
customview: {
|
||||
type: 'multiMonth',
|
||||
multiMonthMaxColumns: 1,
|
||||
duration: { month: 12 },
|
||||
buttonText: 'grid',
|
||||
},
|
||||
},
|
||||
dateClick: this.handleDateSelect.bind(this),
|
||||
datesSet: this.handleMonthChange.bind(this),
|
||||
select: this.handleDateSelect.bind(this),
|
||||
eventClick: this.handleEventClick.bind(this),
|
||||
eventsSet: this.handleEvents.bind(this),
|
||||
};
|
||||
|
||||
*/
|
||||
</script>
|
||||
|
|
|
@ -103,7 +103,7 @@ import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } f
|
|||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import isEqual from "lodash.isequal";
|
||||
import { useSalutationStore } from "../../../../stores/admin/configuration/salutation";
|
||||
import { useSalutationStore } from "@/stores/admin/configuration/salutation";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -38,7 +38,7 @@ import Pagination from "@/components/Pagination.vue";
|
|||
import { useAbilityStore } from "@/stores/ability";
|
||||
import { useNewsletterStore } from "@/stores/admin/club/newsletter/newsletter";
|
||||
import type { NewsletterViewModel } from "@/viewmodels/admin/club/newsletter/newsletter.models";
|
||||
import NewsletterListItem from "../../../../components/admin/club/newsletter/NewsletterListItem.vue";
|
||||
import NewsletterListItem from "@/components/admin/club/newsletter/NewsletterListItem.vue";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -81,7 +81,7 @@ import FailureXMark from "@/components/FailureXMark.vue";
|
|||
import { ArrowDownTrayIcon, ViewfinderCircleIcon } from "@heroicons/vue/24/outline";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
import { useNewsletterPrintoutStore } from "../../../../stores/admin/club/newsletter/newsletterPrintout";
|
||||
import { useNewsletterPrintoutStore } from "@/stores/admin/club/newsletter/newsletterPrintout";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -4,52 +4,56 @@
|
|||
<p v-else-if="loading == 'failed'" @click="fetchNewsletterRecipients" class="cursor-pointer">
|
||||
↺ laden fehlgeschlagen
|
||||
</p>
|
||||
<div class="flex flex-col gap-2 h-1/2">
|
||||
|
||||
<div v-if="!showMemberSelect" class="flex flex-row gap-2 items-center">
|
||||
<select v-model="recipientsByQueryId">
|
||||
<option value="def">Optional</option>
|
||||
<option v-for="query in queries" :key="query.id" :value="query.id">{{ query.title }}</option>
|
||||
</select>
|
||||
<p>Empfänger durch gespeicherte Abfrage</p>
|
||||
<div class="flex flex-col gap-2 grow overflow-y-auto">
|
||||
<div
|
||||
v-for="member in queried"
|
||||
:key="member.id"
|
||||
class="flex flex-row h-fit w-full border border-primary rounded-md bg-primary p-2 text-white justify-between items-center"
|
||||
>
|
||||
<div>
|
||||
<p>{{ member.lastname }}, {{ member.firstname }} {{ member.nameaffix ? `- ${member.nameaffix}` : "" }}</p>
|
||||
<p>Newsletter senden an Typ: {{ member.sendNewsletter?.type.type }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div title="Empfänger manuell hinzufügen" @click="showMemberSelect = true">
|
||||
<UserPlusIcon class="w-7 h-7 cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 h-1/2">
|
||||
<div v-else class="flex flex-row gap-2 items-center">
|
||||
<MemberSearchSelectMultiple
|
||||
title="weitere Empfänger suchen"
|
||||
showTitleAsPlaceholder
|
||||
v-model="recipients"
|
||||
:disabled="!can('create', 'club', 'newsletter')"
|
||||
/>
|
||||
|
||||
<p>Ausgewählte Empfänger</p>
|
||||
<div class="flex flex-col gap-2 grow overflow-y-auto">
|
||||
<div
|
||||
v-for="member in selected"
|
||||
:key="member.id"
|
||||
class="flex flex-row h-fit w-full border border-primary rounded-md bg-primary p-2 text-white justify-between items-center"
|
||||
>
|
||||
<div>
|
||||
<p>{{ member.lastname }}, {{ member.firstname }} {{ member.nameaffix ? `- ${member.nameaffix}` : "" }}</p>
|
||||
<p>Newsletter senden an Typ: {{ member.sendNewsletter?.type.type }}</p>
|
||||
</div>
|
||||
|
||||
<TrashIcon
|
||||
v-if="can('create', 'club', 'newsletter')"
|
||||
class="w-5 h-5 p-1 box-content cursor-pointer"
|
||||
@click="removeSelected(member.id)"
|
||||
/>
|
||||
</div>
|
||||
<div title="Empfänger über Query hinzufügen" @click="showMemberSelect = false">
|
||||
<ArchiveBoxIcon class="w-7 h-7 cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="!showMemberSelect">Empfänger durch gespeicherte Abfrage</p>
|
||||
<p v-else>Ausgewählte Empfänger</p>
|
||||
|
||||
<div class="flex flex-col gap-2 grow overflow-y-auto">
|
||||
<div
|
||||
v-for="member in showRecipientsByMode"
|
||||
:key="member.id"
|
||||
class="flex flex-row gap-2 h-fit w-full border border-primary rounded-md bg-primary p-2 text-white items-center"
|
||||
>
|
||||
<ExclamationTriangleIcon v-if="member.sendNewsletter == null" class="w-7 h-7" />
|
||||
|
||||
<div class="grow">
|
||||
<p>{{ member.lastname }}, {{ member.firstname }} {{ member.nameaffix ? `- ${member.nameaffix}` : "" }}</p>
|
||||
<p>Newsletter senden an Typ: {{ member.sendNewsletter?.type.type ?? "---" }}</p>
|
||||
</div>
|
||||
|
||||
<TrashIcon
|
||||
v-if="can('create', 'club', 'newsletter') && showMemberSelect"
|
||||
class="w-5 h-5 p-1 box-content cursor-pointer"
|
||||
@click="removeSelected(member.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="countOfNoConfig != 0" class="flex flex-row items-center gap-2 pt-3">
|
||||
<ExclamationTriangleIcon class="text-red-500 w-5 h-5" />
|
||||
<p>{{ countOfNoConfig }} Mitglieder der Auswahl haben keinen Newsletter-Versand konfiguriert!</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -67,7 +71,7 @@ import {
|
|||
TransitionRoot,
|
||||
} from "@headlessui/vue";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||
import { TrashIcon } from "@heroicons/vue/24/outline";
|
||||
import { ArchiveBoxIcon, ExclamationTriangleIcon, TrashIcon, UserPlusIcon } from "@heroicons/vue/24/outline";
|
||||
import { useMemberStore } from "@/stores/admin/club/member/member";
|
||||
import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
||||
import { useNewsletterStore } from "@/stores/admin/club/newsletter/newsletter";
|
||||
|
@ -77,6 +81,8 @@ import { useQueryStoreStore } from "@/stores/admin/configuration/queryStore";
|
|||
import { useQueryBuilderStore } from "@/stores/admin/club/queryBuilder";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import MemberSearchSelectMultiple from "@/components/search/MemberSearchSelectMultiple.vue";
|
||||
import MemberSearchSelect from "@/components/search/MemberSearchSelect.vue";
|
||||
import type { FieldType } from "@/types/dynamicQueries";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -84,22 +90,17 @@ export default defineComponent({
|
|||
props: {
|
||||
newsletterId: String,
|
||||
},
|
||||
watch: {
|
||||
recipientsByQuery() {
|
||||
this.loadQuery();
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
query: "" as String,
|
||||
queryResult: [] as Array<{ id: FieldType; [key: string]: FieldType }>,
|
||||
members: [] as Array<MemberViewModel>,
|
||||
showMemberSelect: false as boolean,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapWritableState(useNewsletterRecipientsStore, ["recipients", "loading"]),
|
||||
...mapWritableState(useNewsletterStore, ["activeNewsletterObj"]),
|
||||
...mapState(useQueryStoreStore, ["queries"]),
|
||||
...mapState(useQueryBuilderStore, ["data"]),
|
||||
...mapState(useAbilityStore, ["can"]),
|
||||
selected(): Array<MemberViewModel> {
|
||||
return this.members
|
||||
|
@ -114,10 +115,10 @@ export default defineComponent({
|
|||
},
|
||||
queried(): Array<MemberViewModel> {
|
||||
if (this.recipientsByQueryId == "def") return [];
|
||||
let keys = Object.keys(this.data?.[0] ?? {});
|
||||
let keys = Object.keys(this.queryResult?.[0] ?? {});
|
||||
let memberKey = keys.find((k) => k.includes("member_id"));
|
||||
return this.members.filter((m) =>
|
||||
this.data
|
||||
this.queryResult
|
||||
.map((t) => ({
|
||||
id: t.id,
|
||||
...(memberKey ? { memberId: t[memberKey] } : {}),
|
||||
|
@ -125,6 +126,17 @@ export default defineComponent({
|
|||
.some((d) => (d.memberId ?? d.id) == m.id)
|
||||
);
|
||||
},
|
||||
showRecipientsByMode() {
|
||||
return (this.showMemberSelect ? this.selected : this.queried).sort((a, b) => {
|
||||
const aHasConfig = a.sendNewsletter != null;
|
||||
const bHasConfig = b.sendNewsletter != null;
|
||||
if (aHasConfig === bHasConfig) return 0;
|
||||
return aHasConfig ? -1 : 1;
|
||||
});
|
||||
},
|
||||
countOfNoConfig() {
|
||||
return this.showRecipientsByMode.filter((member) => member.sendNewsletter == null).length;
|
||||
},
|
||||
recipientsByQueryId: {
|
||||
get() {
|
||||
return this.activeNewsletterObj?.recipientsByQueryId ?? "def";
|
||||
|
@ -133,17 +145,12 @@ export default defineComponent({
|
|||
if (this.activeNewsletterObj == undefined) return;
|
||||
if (val == "def") {
|
||||
this.activeNewsletterObj.recipientsByQueryId = null;
|
||||
this.activeNewsletterObj.recipientsByQuery = null;
|
||||
} else if (this.queries.find((q) => q.id == val)) {
|
||||
this.activeNewsletterObj.recipientsByQueryId = val;
|
||||
this.activeNewsletterObj.recipientsByQuery = cloneDeep(this.queries.find((q) => q.id == val));
|
||||
this.sendQuery(0, 0, this.recipientsByQuery?.query, true);
|
||||
this.loadQuery();
|
||||
}
|
||||
},
|
||||
},
|
||||
recipientsByQuery() {
|
||||
return this.activeNewsletterObj?.recipientsByQuery;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// this.fetchNewsletterRecipients();
|
||||
|
@ -155,7 +162,7 @@ export default defineComponent({
|
|||
...mapActions(useMemberStore, ["getAllMembers"]),
|
||||
...mapActions(useNewsletterRecipientsStore, ["fetchNewsletterRecipients"]),
|
||||
...mapActions(useQueryStoreStore, ["fetchQueries"]),
|
||||
...mapActions(useQueryBuilderStore, ["sendQuery"]),
|
||||
...mapActions(useQueryBuilderStore, ["sendQueryByStoreId"]),
|
||||
removeSelected(id: string) {
|
||||
let index = this.recipients.findIndex((s) => s == id);
|
||||
if (index != -1) {
|
||||
|
@ -170,8 +177,12 @@ export default defineComponent({
|
|||
.catch(() => {});
|
||||
},
|
||||
loadQuery() {
|
||||
if (this.recipientsByQuery) {
|
||||
this.sendQuery(0, 0, this.recipientsByQuery.query, true);
|
||||
if (this.recipientsByQueryId != "def") {
|
||||
this.sendQueryByStoreId(this.recipientsByQueryId, 0, 0, true)
|
||||
.then((result) => {
|
||||
this.queryResult = result.data.rows;
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -53,8 +53,8 @@ import { useNewsletterStore } from "@/stores/admin/club/newsletter/newsletter";
|
|||
import { useModalStore } from "@/stores/modal";
|
||||
import NewsletterSyncing from "@/components/admin/club/newsletter/NewsletterSyncing.vue";
|
||||
import { PrinterIcon } from "@heroicons/vue/24/outline";
|
||||
import { useNewsletterDatesStore } from "../../../../stores/admin/club/newsletter/newsletterDates";
|
||||
import { useNewsletterRecipientsStore } from "../../../../stores/admin/club/newsletter/newsletterRecipients";
|
||||
import { useNewsletterDatesStore } from "@/stores/admin/club/newsletter/newsletterDates";
|
||||
import { useNewsletterRecipientsStore } from "@/stores/admin/club/newsletter/newsletterRecipients";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -70,14 +70,11 @@ import { useQueryStoreStore } from "@/stores/admin/configuration/queryStore";
|
|||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
computed: {
|
||||
...mapState(useQueryBuilderStore, ["loading", "loadingData", "tableMetas", "data", "totalLength", "queryError"]),
|
||||
...mapState(useQueryBuilderStore, ["loading", "loadingData", "data", "totalLength", "queryError"]),
|
||||
...mapWritableState(useQueryBuilderStore, ["query"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchTableMetas();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useQueryBuilderStore, ["fetchTableMetas", "sendQuery", "clearResults", "exportData"]),
|
||||
...mapActions(useQueryBuilderStore, ["sendQuery", "clearResults", "exportData"]),
|
||||
...mapActions(useQueryStoreStore, ["triggerSave"]),
|
||||
},
|
||||
});
|
||||
|
|
46
src/views/admin/management/setting/Setting.vue
Normal file
46
src/views/admin/management/setting/Setting.vue
Normal file
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<MainTemplate>
|
||||
<template #topBar>
|
||||
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
||||
<h1 class="font-bold text-xl h-8">Einstellungen</h1>
|
||||
</div>
|
||||
</template>
|
||||
<template #main>
|
||||
<p>Hinweis: Optionale Felder können leer gelassen werden und nutzen dann einen Fallback-Werte.</p>
|
||||
<ClubImageSetting />
|
||||
<ClubSetting />
|
||||
<AppSetting />
|
||||
<MailSetting />
|
||||
<SessionSetting />
|
||||
<BackupSetting />
|
||||
</template>
|
||||
</MainTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import MainTemplate from "@/templates/Main.vue";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
import { useSettingStore } from "@/stores/admin/management/setting";
|
||||
import ClubSetting from "@/components/admin/management/setting/ClubSetting.vue";
|
||||
import AppSetting from "@/components/admin/management/setting/AppSetting.vue";
|
||||
import MailSetting from "@/components/admin/management/setting/MailSetting.vue";
|
||||
import SessionSetting from "@/components/admin/management/setting/SessionSetting.vue";
|
||||
import BackupSetting from "@/components/admin/management/setting/BackupSetting.vue";
|
||||
import ClubImageSetting from "@/components/admin/management/setting/ClubImageSetting.vue";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
computed: {
|
||||
...mapState(useAbilityStore, ["can"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchSettings();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useSettingStore, ["fetchSettings"]),
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -2,7 +2,7 @@
|
|||
<div class="grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8 pb-20">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<img src="/Logo.png" alt="LOGO" class="h-auto w-full" />
|
||||
<AppLogo />
|
||||
<h2 class="text-center text-4xl font-extrabold text-gray-900">Einrichtung</h2>
|
||||
</div>
|
||||
|
||||
|
@ -14,17 +14,62 @@
|
|||
<p class="w-fit">Einladungslink nicht gültig - Melde dich bei einem Admin.</p>
|
||||
</div>
|
||||
<form v-else class="flex flex-col gap-2" @submit.prevent="invite">
|
||||
<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>
|
||||
|
||||
<img :src="image" alt="totp" class="w-56 h-56 self-center" />
|
||||
<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="inviteStatus == 'loading' || inviteStatus == 'success'">
|
||||
|
@ -49,6 +94,8 @@ import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
|||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
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">
|
||||
|
@ -59,12 +106,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,
|
||||
inviteStatus: undefined as undefined | "loading" | "success" | "failed",
|
||||
inviteError: "" as string,
|
||||
notMatching: false as boolean,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
|
@ -88,15 +137,21 @@ export default defineComponent({
|
|||
});
|
||||
},
|
||||
methods: {
|
||||
invite(e: any) {
|
||||
let formData = e.target.elements;
|
||||
async invite(e: any) {
|
||||
let secret = "";
|
||||
if (this.tab == "totp") secret = this.totp(e);
|
||||
else secret = await this.password(e);
|
||||
|
||||
if (secret == "") return;
|
||||
|
||||
this.inviteStatus = "loading";
|
||||
this.inviteError = "";
|
||||
this.$http
|
||||
.put(`/invite`, {
|
||||
token: this.token,
|
||||
mail: this.mail,
|
||||
totp: formData.totp.value,
|
||||
secret: secret,
|
||||
routine: this.tab,
|
||||
})
|
||||
.then((result) => {
|
||||
this.inviteStatus = "success";
|
||||
|
@ -111,6 +166,23 @@ export default defineComponent({
|
|||
this.inviteError = 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>
|
||||
|
|
|
@ -12,7 +12,12 @@
|
|||
</template>
|
||||
<template #diffMain>
|
||||
<div class="flex flex-col w-full h-full gap-2 justify-between px-7 overflow-hidden">
|
||||
<FullCalendar :options="calendarOptions" class="max-h-full h-full" />
|
||||
<CustomCalendar
|
||||
:items="formattedItems"
|
||||
:allow-interaction="false"
|
||||
:small-styling="true"
|
||||
@event-select="eventClick"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</MainTemplate>
|
||||
|
@ -23,14 +28,10 @@ import { defineComponent, markRaw, defineAsyncComponent } from "vue";
|
|||
import { mapActions, mapState } from "pinia";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import MainTemplate from "@/templates/Main.vue";
|
||||
import FullCalendar from "@fullcalendar/vue3";
|
||||
import deLocale from "@fullcalendar/core/locales/de";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import { InformationCircleIcon, LinkIcon } from "@heroicons/vue/24/outline";
|
||||
import type { CalendarViewModel } from "@/viewmodels/admin/club/calendar.models";
|
||||
import { RouterLink } from "vue-router";
|
||||
import CustomCalendar from "@/components/CustomCalendar.vue";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -50,32 +51,6 @@ export default defineComponent({
|
|||
backgroundColor: c.type.color,
|
||||
}));
|
||||
},
|
||||
calendarOptions() {
|
||||
return {
|
||||
timeZone: "local",
|
||||
locale: deLocale,
|
||||
plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin],
|
||||
initialView: "dayGridMonth",
|
||||
headerToolbar: {
|
||||
left: "dayGridMonth,timeGridWeek",
|
||||
center: "title",
|
||||
right: "prev,today,next",
|
||||
},
|
||||
eventDisplay: "block",
|
||||
weekends: true,
|
||||
editable: false,
|
||||
selectable: false,
|
||||
selectMirror: false,
|
||||
dayMaxEvents: true,
|
||||
weekNumbers: true,
|
||||
displayEventTime: true,
|
||||
nowIndicator: true,
|
||||
weekText: "KW",
|
||||
allDaySlot: false,
|
||||
events: this.formattedItems,
|
||||
eventClick: this.eventClick,
|
||||
};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchCalendars();
|
||||
|
@ -93,10 +68,10 @@ export default defineComponent({
|
|||
openLinkModal(e: any) {
|
||||
this.openModal(markRaw(defineAsyncComponent(() => import("@/components/public/calendar/CalendarLinkModal.vue"))));
|
||||
},
|
||||
eventClick(e: any) {
|
||||
eventClick(id: string) {
|
||||
this.openModal(
|
||||
markRaw(defineAsyncComponent(() => import("@/components/public/calendar/ShowCalendarEntryModal.vue"))),
|
||||
this.calendars.find((c) => c.id == e.event.id)
|
||||
this.calendars.find((c) => c.id == id)
|
||||
);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<div class="grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8 pb-20">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<img src="/Logo.png" alt="LOGO" class="h-auto w-full" />
|
||||
<h2 class="text-center text-4xl font-extrabold text-gray-900">TOTP zurücksetzen</h2>
|
||||
<AppLogo />
|
||||
<h2 class="text-center text-4xl font-extrabold text-gray-900">Zugang zurücksetzen</h2>
|
||||
</div>
|
||||
|
||||
<div v-if="verification == 'loading'" class="flex flex-col gap-2 items-center">
|
||||
|
@ -15,15 +15,61 @@
|
|||
<RouterLink to="/reset" class="text-primary">Zum zurücksetzen Start</RouterLink>
|
||||
</div>
|
||||
<form v-else class="flex flex-col gap-2" @submit.prevent="reset">
|
||||
<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>
|
||||
|
||||
<TextCopy :copyText="otp" />
|
||||
<div v-if="tab == 'totp'" class="flex flex-col gap-2">
|
||||
<img :src="image" alt="totp" class="w-56 h-56 self-center" />
|
||||
|
||||
<div class="-space-y-px">
|
||||
<div>
|
||||
<input id="totp" name="totp" type="text" required placeholder="TOTP" />
|
||||
<TextCopy :copyText="otp" />
|
||||
|
||||
<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="resetStatus == 'loading' || resetStatus == 'success'">
|
||||
|
@ -49,6 +95,8 @@ import FailureXMark from "@/components/FailureXMark.vue";
|
|||
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">
|
||||
|
@ -59,11 +107,13 @@ export default defineComponent({
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
tab: "totp",
|
||||
verification: "loading" as "success" | "loading" | "failed",
|
||||
image: undefined as undefined | string,
|
||||
otp: undefined as undefined | string,
|
||||
resetStatus: undefined as undefined | "loading" | "success" | "failed",
|
||||
resetError: "" as string,
|
||||
notMatching: false as boolean,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
|
@ -86,15 +136,21 @@ export default defineComponent({
|
|||
});
|
||||
},
|
||||
methods: {
|
||||
reset(e: any) {
|
||||
let formData = e.target.elements;
|
||||
async reset(e: any) {
|
||||
let secret = "";
|
||||
if (this.tab == "totp") secret = this.totp(e);
|
||||
else secret = await this.password(e);
|
||||
|
||||
if (secret == "") return;
|
||||
|
||||
this.resetStatus = "loading";
|
||||
this.resetError = "";
|
||||
this.$http
|
||||
.put(`/reset`, {
|
||||
token: this.token,
|
||||
mail: this.mail,
|
||||
totp: formData.totp.value,
|
||||
secret: secret,
|
||||
routine: this.tab,
|
||||
})
|
||||
.then((result) => {
|
||||
this.resetStatus = "success";
|
||||
|
@ -109,6 +165,23 @@ export default defineComponent({
|
|||
this.resetError = 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>
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
<div class="grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8 pb-20">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<img src="/Logo.png" alt="LOGO" class="h-auto w-full" />
|
||||
<h2 class="text-center text-4xl font-extrabold text-gray-900">TOTP zurücksetzen</h2>
|
||||
<AppLogo />
|
||||
<h2 class="text-center text-4xl font-extrabold text-gray-900">Zugang zurücksetzen</h2>
|
||||
</div>
|
||||
|
||||
<form class="flex flex-col gap-2" @submit.prevent="reset">
|
||||
<div class="-space-y-px">
|
||||
<div>
|
||||
<input id="username" name="username" type="text" required placeholder="Benutzer" />
|
||||
<input id="username" name="username" type="text" required placeholder="Benutzer" :value="username" />
|
||||
</div>
|
||||
</div>
|
||||
<RouterLink to="/" class="w-fit self-end text-primary">zum Login</RouterLink>
|
||||
|
@ -36,6 +36,7 @@ import Spinner from "@/components/Spinner.vue";
|
|||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import FormBottomBar from "@/components/FormBottomBar.vue";
|
||||
import AppLogo from "@/components/AppLogo.vue";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -44,8 +45,12 @@ export default defineComponent({
|
|||
return {
|
||||
resetStatus: undefined as undefined | "loading" | "success" | "failed",
|
||||
resetMessage: "" as string,
|
||||
username: "" as string,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.username = localStorage.getItem("username") ?? "";
|
||||
},
|
||||
methods: {
|
||||
reset(e: any) {
|
||||
let formData = e.target.elements;
|
||||
|
|
|
@ -2,36 +2,18 @@
|
|||
<div class="grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8 pb-20">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<img src="/Logo.png" alt="LOGO" class="h-auto w-full" />
|
||||
<AppLogo />
|
||||
<h2 class="text-center text-4xl font-extrabold text-gray-900">Einrichtung</h2>
|
||||
</div>
|
||||
<CheckProgressBar :total="dictionary.length" :step="step" :successfull="successfull" />
|
||||
|
||||
<form class="flex flex-col gap-2" @submit.prevent="setup">
|
||||
<div class="-space-y-px">
|
||||
<div>
|
||||
<input id="username" name="username" type="text" required placeholder="Benutzer" class="rounded-b-none!" />
|
||||
</div>
|
||||
<div>
|
||||
<input id="mail" name="mail" type="email" required placeholder="Mailadresse" class="rounded-none!" />
|
||||
</div>
|
||||
<div>
|
||||
<input id="firstname" name="firstname" type="text" required placeholder="Vorname" class="rounded-none!" />
|
||||
</div>
|
||||
<div>
|
||||
<input id="lastname" name="lastname" type="text" required placeholder="Nachname" class="rounded-t-none!" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row gap-2">
|
||||
<button type="submit" primary :disabled="setupStatus == 'loading' || setupStatus == 'success'">
|
||||
Admin-Account anlegen
|
||||
</button>
|
||||
<Spinner v-if="setupStatus == 'loading'" class="my-auto" />
|
||||
<SuccessCheckmark v-else-if="setupStatus == 'success'" />
|
||||
<FailureXMark v-else-if="setupStatus == 'failed'" />
|
||||
</div>
|
||||
<p v-if="setupMessage" class="text-center">{{ setupMessage }}</p>
|
||||
</form>
|
||||
<Club v-if="step == stepIndex('club')" />
|
||||
<Images v-else-if="step == stepIndex('clubImages')" />
|
||||
<App v-else-if="step == stepIndex('app')" />
|
||||
<Mail v-else-if="step == stepIndex('mail')" />
|
||||
<Account v-else-if="step == stepIndex('account')" />
|
||||
<Finished v-else-if="step == stepIndex('finished')" />
|
||||
<p v-else class="text-center">UI-Fehler - versuchen Sie einen Reload der Seite</p>
|
||||
|
||||
<FormBottomBar />
|
||||
</div>
|
||||
|
@ -40,41 +22,23 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import FormBottomBar from "@/components/FormBottomBar.vue";
|
||||
import AppLogo from "@/components/AppLogo.vue";
|
||||
import App from "@/components/setup/App.vue";
|
||||
import Account from "@/components/setup/Account.vue";
|
||||
import CheckProgressBar from "@/components/CheckProgressBar.vue";
|
||||
import { mapState } from "pinia";
|
||||
import { useSetupStore } from "@/stores/setup";
|
||||
import Club from "@/components/setup/Club.vue";
|
||||
import Images from "@/components/setup/Images.vue";
|
||||
import Finished from "@/components/setup/Finished.vue";
|
||||
import Mail from "@/components/setup/Mail.vue";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
setupStatus: undefined as undefined | "loading" | "success" | "failed",
|
||||
setupMessage: "" as string,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
setup(e: any) {
|
||||
let formData = e.target.elements;
|
||||
this.setupStatus = "loading";
|
||||
this.setupMessage = "";
|
||||
this.$http
|
||||
.post(`/setup`, {
|
||||
username: formData.username.value,
|
||||
mail: formData.mail.value,
|
||||
firstname: formData.firstname.value,
|
||||
lastname: formData.lastname.value,
|
||||
})
|
||||
.then((result) => {
|
||||
this.setupStatus = "success";
|
||||
this.setupMessage = "Sie haben einen Verifizierungslink per Mail erhalten.";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.setupStatus = "failed";
|
||||
this.setupMessage = err.response.data;
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
...mapState(useSetupStore, ["step", "stepIndex", "successfull", "dictionary"]),
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8 pb-20">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<img src="/Logo.png" alt="LOGO" class="h-auto w-full" />
|
||||
<AppLogo />
|
||||
<h2 class="text-center text-4xl font-extrabold text-gray-900">Einrichtung</h2>
|
||||
</div>
|
||||
|
||||
|
@ -50,6 +50,7 @@ import FailureXMark from "@/components/FailureXMark.vue";
|
|||
import { RouterLink } from "vue-router";
|
||||
import FormBottomBar from "@/components/FormBottomBar.vue";
|
||||
import TextCopy from "@/components/TextCopy.vue";
|
||||
import AppLogo from "@/components/AppLogo.vue";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -92,7 +93,7 @@ export default defineComponent({
|
|||
this.setupStatus = "loading";
|
||||
this.setupError = "";
|
||||
this.$http
|
||||
.put(`/setup`, {
|
||||
.post(`/setup/finish`, {
|
||||
token: this.token,
|
||||
mail: this.mail,
|
||||
totp: formData.totp.value,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue