patches v1.3.1 #58

Merged
jkeffects merged 8 commits from develop into main 2025-02-07 12:33:39 +00:00
15 changed files with 174 additions and 119 deletions

View file

@ -32,9 +32,9 @@ services:
#environment: #environment:
# - SERVERADDRESS=<backend_url (https://... | http://...)> # wichtig: ohne Pfad # - SERVERADDRESS=<backend_url (https://... | http://...)> # wichtig: ohne Pfad
# - APPNAMEOVERWRITE=Mitgliederverwaltung # ersetzt den Namen FF-Admin auf der Login-Seite und sonstigen Positionen in der Oberfläche # - APPNAMEOVERWRITE=<appname> # ersetzt den Namen FF-Admin auf der Login-Seite und sonstigen Positionen in der Oberfläche
# - IMPRINTLINK=https://mywebsite-imprint-url # - IMPRINTLINK=<imprint link>
# - PRIVACYLINK=https://mywebsite-privacy-url # - PRIVACYLINK=<privacy link>
# - CUSTOMLOGINMESSAGE=betrieben von xy # - CUSTOMLOGINMESSAGE=betrieben von xy
#volumes: #volumes:
# - <volume|local path>/favicon.ico:/usr/share/nginx/html/favicon.ico # 48x48 px Auflösung # - <volume|local path>/favicon.ico:/usr/share/nginx/html/favicon.ico # 48x48 px Auflösung
@ -68,7 +68,7 @@ Ein eigenes Favicon und Logo kann über das verwenden Volume ausgetauscht werden
## Einrichtung ## Einrichtung
1. **Admin Benutzer erstellen**: Erstellen Sie einen Admin Benutzer unter dem Pfad /setup, um auf die Migliederverwaltung Zugriff zu erhalten. Nach der Erstellung des ersten Benutzers wird der Pfad automatisch geblockt. 1. **Admin Benutzer erstellen**: Erstellen Sie einen Admin Benutzer unter dem Pfad /setup, um auf die Mitgliederverwaltung Zugriff zu erhalten. Nach der Erstellung des ersten Benutzers wird der Pfad automatisch geblockt.
2. **Rollen und Berechtigungen**: Unter `Benutzer > Rollen` können die Rollen und Berechtigungen für die Benutzer erstellt und angepasst werden. 2. **Rollen und Berechtigungen**: Unter `Benutzer > Rollen` können die Rollen und Berechtigungen für die Benutzer erstellt und angepasst werden.

View file

@ -79,11 +79,30 @@
<div v-if="!allDay" class="flex flex-row gap-2"> <div v-if="!allDay" class="flex flex-row gap-2">
<div class="w-full"> <div class="w-full">
<label for="starttime">Startzeit</label> <label for="starttime">Startzeit</label>
<input type="datetime-local" id="starttime" required :value="data.start.split(':00+')[0]" /> <input
type="datetime-local"
id="starttime"
required
:value="formatForDateTimeLocalInput(data.start)"
@change="
($event) => {
($refs.endtime as HTMLInputElement).min = formatForDateTimeLocalInput(
($event.target as HTMLInputElement).value
);
}
"
/>
</div> </div>
<div class="w-full"> <div class="w-full">
<label for="endtime">Endzeit</label> <label for="endtime">Endzeit</label>
<input type="datetime-local" id="endtime" required :value="data.end.split(':00+')[0]" /> <input
ref="endtime"
type="datetime-local"
id="endtime"
required
:value="formatForDateTimeLocalInput(data.end)"
:min="formatForDateTimeLocalInput(data.start)"
/>
</div> </div>
</div> </div>
<div v-else class="flex flex-row gap-2"> <div v-else class="flex flex-row gap-2">
@ -93,8 +112,12 @@
type="date" type="date"
id="startdate" id="startdate"
required required
:value="data.start" :value="formatForDateInput(data.start)"
@change="($event) => (($refs.enddate as HTMLInputElement).max = ($event.target as HTMLInputElement).value)" @change="
($event) => {
($refs.enddate as HTMLInputElement).min = formatForDateInput(($event.target as HTMLInputElement).value);
}
"
/> />
</div> </div>
<div class="w-full"> <div class="w-full">
@ -105,7 +128,7 @@
id="enddate" id="enddate"
required required
:value="decrementEndDate(data.end)" :value="decrementEndDate(data.end)"
:min="data.start" :min="formatForDateInput(data.start)"
/> />
</div> </div>
</div> </div>
@ -180,11 +203,11 @@ export default defineComponent({
let createCalendar: CreateCalendarViewModel = { let createCalendar: CreateCalendarViewModel = {
typeId: this.selectedType.id, typeId: this.selectedType.id,
starttime: this.allDay starttime: this.allDay
? new Date(new Date(formData.startdate.value).setHours(0, 0, 0, 0)) ? new Date(new Date(formData.startdate.value).setHours(0, 0, 0, 0)).toISOString()
: formData.starttime.value, : new Date(formData.starttime.value).toISOString(),
endtime: this.allDay endtime: this.allDay
? new Date(new Date(formData.enddate.value).setHours(23, 59, 59, 999)) ? new Date(new Date(formData.enddate.value).setHours(23, 59, 59, 999)).toISOString()
: formData.endtime.value, : new Date(formData.endtime.value).toISOString(),
title: formData.title.value, title: formData.title.value,
content: formData.content.value, content: formData.content.value,
location: formData.location.value, location: formData.location.value,
@ -209,6 +232,26 @@ export default defineComponent({
const month = String(localDate.getMonth() + 1).padStart(2, "0"); const month = String(localDate.getMonth() + 1).padStart(2, "0");
const day = String(localDate.getDate() - 1).padStart(2, "0"); const day = String(localDate.getDate() - 1).padStart(2, "0");
return `${year}-${month}-${day}`;
},
formatForDateTimeLocalInput(utcDateString: string) {
const localDate = new Date(utcDateString);
const year = localDate.getFullYear();
const month = String(localDate.getMonth() + 1).padStart(2, "0");
const day = String(localDate.getDate()).padStart(2, "0");
const hours = String(localDate.getHours()).padStart(2, "0");
const minutes = String(localDate.getMinutes()).padStart(2, "0");
return `${year}-${month}-${day}T${hours}:${minutes}`;
},
formatForDateInput(utcDateString: string) {
const localDate = new Date(utcDateString);
const year = localDate.getFullYear();
const month = String(localDate.getMonth() + 1).padStart(2, "0");
const day = String(localDate.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
}, },
}, },

View file

@ -78,7 +78,7 @@
<input type="checkbox" id="allDay" v-model="calendar.allDay" /> <input type="checkbox" id="allDay" v-model="calendar.allDay" />
<label for="allDay">ganztägig</label> <label for="allDay">ganztägig</label>
</div> </div>
<div v-if="calendar.allDay == false" class="flex flex-row gap-2"> <div v-if="!calendar.allDay" class="flex flex-row gap-2">
<div class="w-full"> <div class="w-full">
<label for="starttime">Startzeit</label> <label for="starttime">Startzeit</label>
<input <input

View file

@ -7,7 +7,7 @@ import type { PermissionObject } from "@/types/permissionTypes";
import { useAbilityStore } from "@/stores/ability"; import { useAbilityStore } from "@/stores/ability";
export type Payload = JwtPayload & { export type Payload = JwtPayload & {
userId: number; userId: string;
username: string; username: string;
firstname: string; firstname: string;
lastname: string; lastname: string;
@ -67,7 +67,7 @@ export async function isAuthenticatedPromise(forceRefresh: boolean = false): Pro
}); });
} }
var { firstname, lastname, mail, username, permissions, isOwner } = decoded; var { userId, firstname, lastname, mail, username, permissions, isOwner } = decoded;
if (Object.keys(permissions ?? {}).length === 0 && !isOwner) { if (Object.keys(permissions ?? {}).length === 0 && !isOwner) {
auth.setFailed(); auth.setFailed();
@ -75,7 +75,7 @@ export async function isAuthenticatedPromise(forceRefresh: boolean = false): Pro
} }
auth.setSuccess(); auth.setSuccess();
account.setAccountData(firstname, lastname, mail, username); account.setAccountData(userId, firstname, lastname, mail, username);
ability.setAbility(permissions, isOwner); ability.setAbility(permissions, isOwner);
resolve(decoded); resolve(decoded);
} }

View file

@ -661,6 +661,11 @@ const router = createRouter({
}, },
], ],
}, },
{
path: "version",
name: "admin-user-version",
component: () => import("@/views/admin/user/version/VersionDisplay.vue"),
},
], ],
}, },
{ {
@ -701,11 +706,6 @@ const router = createRouter({
name: "account-administration", name: "account-administration",
component: () => import("@/views/account/Administration.vue"), component: () => import("@/views/account/Administration.vue"),
}, },
{
path: "version",
name: "account-version",
component: () => import("@/views/account/VersionDisplay.vue"),
},
{ {
path: ":pathMatch(.*)*", path: ":pathMatch(.*)*",
name: "account-404", name: "account-404",

View file

@ -43,6 +43,11 @@ export const useAbilityStore = defineStore("ability", {
return true; return true;
return false; return false;
}, },
isAdmin: (state) => (): boolean => {
const permissions = state.permissions;
if (state.isOwner) return true;
return permissions?.admin ?? false;
},
_can: _can:
() => () =>
( (

View file

@ -5,6 +5,7 @@ import { useAbilityStore } from "./ability";
export const useAccountStore = defineStore("account", { export const useAccountStore = defineStore("account", {
state: () => { state: () => {
return { return {
id: "" as string,
firstname: "" as string, firstname: "" as string,
lastname: "" as string, lastname: "" as string,
mail: "" as string, mail: "" as string,
@ -17,7 +18,8 @@ export const useAccountStore = defineStore("account", {
localStorage.removeItem("refreshToken"); localStorage.removeItem("refreshToken");
window.open("/login", "_self"); window.open("/login", "_self");
}, },
setAccountData(firstname: string, lastname: string, mail: string, alias: string) { setAccountData(id: string, firstname: string, lastname: string, mail: string, alias: string) {
this.id = id;
this.firstname = firstname; this.firstname = firstname;
this.lastname = lastname; this.lastname = lastname;
this.mail = mail; this.mail = mail;

View file

@ -20,7 +20,10 @@ export const useNewsletterRecipientsStore = defineStore("newsletterRecipients",
}, },
getters: { getters: {
detectedChangeNewsletterRecipients: (state) => detectedChangeNewsletterRecipients: (state) =>
!isEqual(state.origin, state.recipients) && state.syncingNewsletterRecipients != "syncing", !isEqual(
state.origin.sort((a: string, b: string) => a.localeCompare(b)),
state.recipients.sort((a: string, b: string) => a.localeCompare(b))
) && state.syncingNewsletterRecipients != "syncing",
}, },
actions: { actions: {
setNewsletterRecipientsSyncingState(state: "synced" | "syncing" | "detectedChanges" | "failed") { setNewsletterRecipientsSyncingState(state: "synced" | "syncing" | "detectedChanges" | "failed") {

View file

@ -30,7 +30,7 @@ export const useQueryBuilderStore = defineStore("queryBuilder", {
this.loading = "failed"; this.loading = "failed";
}); });
}, },
sendQuery(offset = 0, count = 25, query?: DynamicQueryStructure | string) { async sendQuery(offset = 0, count = 25, query?: DynamicQueryStructure | string) {
this.queryError = ""; this.queryError = "";
if (offset == 0) { if (offset == 0) {
this.data = []; this.data = [];
@ -40,7 +40,7 @@ export const useQueryBuilderStore = defineStore("queryBuilder", {
if (queryToSend == undefined || queryToSend == "" || (typeof queryToSend != "string" && queryToSend.table == "")) if (queryToSend == undefined || queryToSend == "" || (typeof queryToSend != "string" && queryToSend.table == ""))
return; return;
this.loadingData = "loading"; this.loadingData = "loading";
http await http
.post(`/admin/querybuilder/query?offset=${offset}&count=${count}`, { .post(`/admin/querybuilder/query?offset=${offset}&count=${count}`, {
query: queryToSend, query: queryToSend,
}) })
@ -65,7 +65,8 @@ export const useQueryBuilderStore = defineStore("queryBuilder", {
this.queryError = ""; this.queryError = "";
this.loadingData = "fetched"; this.loadingData = "fetched";
}, },
exportData() { async exportData() {
await this.sendQuery(0, this.totalLength);
if (this.data.length == 0) return; if (this.data.length == 0) return;
const csvString = [Object.keys(this.data[0]), ...this.data.map((d) => Object.values(d))] const csvString = [Object.keys(this.data[0]), ...this.data.map((d) => Object.values(d))]
.map((e) => e.join(";")) .map((e) => e.join(";"))

View file

@ -134,6 +134,7 @@ export const useNavigationStore = defineStore("navigation", {
...(abilityStore.can("read", "user", "role") ? [{ key: "role", title: "Rollen" }] : []), ...(abilityStore.can("read", "user", "role") ? [{ key: "role", title: "Rollen" }] : []),
...(abilityStore.can("read", "user", "webapi") ? [{ key: "webapi", title: "Webapi-Token" }] : []), ...(abilityStore.can("read", "user", "webapi") ? [{ key: "webapi", title: "Webapi-Token" }] : []),
...(abilityStore.can("read", "user", "backup") ? [{ key: "backup", title: "Backups" }] : []), ...(abilityStore.can("read", "user", "backup") ? [{ key: "backup", title: "Backups" }] : []),
...(abilityStore.isAdmin() ? [{ key: "version", title: "Version" }] : []),
], ],
}, },
} as navigationModel; } as navigationModel;

View file

@ -103,6 +103,7 @@ import {
} from "@headlessui/vue"; } from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid"; import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import type { UserViewModel } from "@/viewmodels/admin/user/user.models"; import type { UserViewModel } from "@/viewmodels/admin/user/user.models";
import { useAccountStore } from "@/stores/account";
</script> </script>
<script lang="ts"> <script lang="ts">
@ -116,15 +117,18 @@ export default defineComponent({
}, },
computed: { computed: {
...mapState(useUserStore, ["users", "loading"]), ...mapState(useUserStore, ["users", "loading"]),
...mapState(useAccountStore, ["id"]),
filtered(): Array<UserViewModel> { filtered(): Array<UserViewModel> {
return this.query === "" return (
this.query === ""
? this.users ? this.users
: this.users.filter((user) => : this.users.filter((user) =>
(user.firstname + " " + user.lastname) (user.firstname + " " + user.lastname)
.toLowerCase() .toLowerCase()
.replace(/\s+/g, "") .replace(/\s+/g, "")
.includes(this.query.toLowerCase().replace(/\s+/g, "")) .includes(this.query.toLowerCase().replace(/\s+/g, ""))
); )
).filter((u) => u.id != this.id);
}, },
}, },
mounted() { mounted() {

View file

@ -12,11 +12,6 @@
:link="{ name: 'account-administration' }" :link="{ name: 'account-administration' }"
:active="activeRouteName == 'account-administration'" :active="activeRouteName == 'account-administration'"
/> />
<RoutingLink
title="Versions-Verwaltung"
:link="{ name: 'account-version' }"
:active="activeRouteName == 'account-version'"
/>
</template> </template>
<template #list> <template #list>
<RoutingLink title="Mein Account" :link="{ name: 'account-me' }" :active="activeRouteName == 'account-me'" /> <RoutingLink title="Mein Account" :link="{ name: 'account-me' }" :active="activeRouteName == 'account-me'" />

View file

@ -1,10 +1,10 @@
<template> <template>
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto"> <div class="flex flex-col h-full w-full overflow-hidden">
<Spinner v-if="loading == 'loading'" class="mx-auto" /> <Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'" @click="fetchNewsletterRecipients" class="cursor-pointer"> <p v-else-if="loading == 'failed'" @click="fetchNewsletterRecipients" class="cursor-pointer">
&#8634; laden fehlgeschlagen &#8634; laden fehlgeschlagen
</p> </p>
<div class="flex flex-col gap-2 h-1/2">
<select v-model="recipientsByQueryId"> <select v-model="recipientsByQueryId">
<option value="def">Optional</option> <option value="def">Optional</option>
<option v-for="query in queries" :key="query.id" :value="query.id">{{ query.title }}</option> <option v-for="query in queries" :key="query.id" :value="query.id">{{ query.title }}</option>
@ -22,7 +22,8 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="flex flex-col gap-2 h-1/2">
<MemberSearchSelect <MemberSearchSelect
title="weitere Empfänger suchen" title="weitere Empfänger suchen"
v-model="recipients" v-model="recipients"
@ -49,6 +50,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View file

@ -10,7 +10,17 @@
<div class="h-1/2 flex flex-col gap-2 p-2 border border-gray-300 rounded-t-md"> <div class="h-1/2 flex flex-col gap-2 p-2 border border-gray-300 rounded-t-md">
<div class="flex flex-row justify-between border-b-2 border-gray-300"> <div class="flex flex-row justify-between border-b-2 border-gray-300">
<h1 class="text-xl font-semibold">Client</h1> <h1 class="text-xl font-semibold">Client</h1>
<p>V{{ clientVersion }}</p> <p>
V{{ clientVersion }} ({{
new Date(clientVersionRelease).toLocaleDateString("de", {
month: "2-digit",
day: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
})
}})
</p>
</div> </div>
<div class="grow flex flex-col gap-4 overflow-y-scroll"> <div class="grow flex flex-col gap-4 overflow-y-scroll">
<div v-for="version in newerClientVersions"> <div v-for="version in newerClientVersions">
@ -18,7 +28,7 @@
<span class="font-semibold text-lg">V{{ version.title }}</span> vom <span class="font-semibold text-lg">V{{ version.title }}</span> vom
{{ {{
new Date(version.isoDate).toLocaleDateString("de", { new Date(version.isoDate).toLocaleDateString("de", {
month: "long", month: "2-digit",
day: "2-digit", day: "2-digit",
year: "numeric", year: "numeric",
}) })
@ -34,7 +44,17 @@
<div class="h-1/2 flex flex-col gap-2 p-2 border border-gray-300 rounded-b-md"> <div class="h-1/2 flex flex-col gap-2 p-2 border border-gray-300 rounded-b-md">
<div class="flex flex-row justify-between border-b-2 border-gray-300"> <div class="flex flex-row justify-between border-b-2 border-gray-300">
<h1 class="text-xl font-semibold">Server</h1> <h1 class="text-xl font-semibold">Server</h1>
<p>V{{ serverVersion }}</p> <p>
V{{ serverVersion }} ({{
new Date(serverVersionRelease).toLocaleDateString("de", {
month: "2-digit",
day: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
})
}})
</p>
</div> </div>
<div class="grow flex flex-col gap-2 overflow-y-scroll"> <div class="grow flex flex-col gap-2 overflow-y-scroll">
<div v-for="version in newerServerVersions"> <div v-for="version in newerServerVersions">
@ -42,7 +62,7 @@
<span class="font-semibold text-lg">V{{ version.title }}</span> vom <span class="font-semibold text-lg">V{{ version.title }}</span> vom
{{ {{
new Date(version.isoDate).toLocaleDateString("de", { new Date(version.isoDate).toLocaleDateString("de", {
month: "long", month: "2-digit",
day: "2-digit", day: "2-digit",
year: "numeric", year: "numeric",
}) })
@ -63,8 +83,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import MainTemplate from "@/templates/Main.vue"; import MainTemplate from "@/templates/Main.vue";
import clientPackage from "../../../package.json"; import clientPackage from "../../../../../package.json";
import type { Releases } from "../../viewmodels/version.models"; import type { Releases } from "@/viewmodels/version.models";
</script> </script>
<script lang="ts"> <script lang="ts">
@ -80,11 +100,19 @@ export default defineComponent({
computed: { computed: {
newerServerVersions() { newerServerVersions() {
if (!this.serverRss) return []; if (!this.serverRss) return [];
return this.serverRss.items.filter((i) => this.compareVersionStrings(this.serverVersion, i.title) < 0); return this.serverRss.items.filter((i) => new Date(i.isoDate) > new Date(this.serverVersionRelease));
}, },
newerClientVersions() { newerClientVersions() {
if (!this.clientRss) return []; if (!this.clientRss) return [];
return this.clientRss.items.filter((i) => this.compareVersionStrings(this.clientVersion, i.title) < 0); return this.clientRss.items.filter((i) => new Date(i.isoDate) > new Date(this.clientVersionRelease));
},
serverVersionRelease() {
if (!this.serverRss) return "";
return this.serverRss.items.find((i) => i.title == this.serverVersion)?.isoDate ?? "";
},
clientVersionRelease() {
if (!this.clientRss) return "";
return this.clientRss.items.find((i) => i.title == this.clientVersion)?.isoDate ?? "";
}, },
}, },
mounted() { mounted() {
@ -102,7 +130,7 @@ export default defineComponent({
}) })
.catch(() => {}); .catch(() => {});
}, },
async getServerFeed() { getServerFeed() {
this.$http this.$http
.get("/server/serverrss") .get("/server/serverrss")
.then((res) => { .then((res) => {
@ -110,7 +138,7 @@ export default defineComponent({
}) })
.catch(() => {}); .catch(() => {});
}, },
async getClientFeed() { getClientFeed() {
this.$http this.$http
.get("/server/clientrss") .get("/server/clientrss")
.then((res) => { .then((res) => {
@ -118,37 +146,6 @@ export default defineComponent({
}) })
.catch(() => {}); .catch(() => {});
}, },
compareVersionStrings(activeVersion: string, compareVersion: string) {
const parseVersion = (version: string) => {
const [main, tag] = version.split("-");
const [major, minor, patch] = main.split(".").map(Number);
return { major, minor, patch, tag };
};
if (!activeVersion || !compareVersion) return 0;
const versionA = parseVersion(activeVersion);
const versionB = parseVersion(compareVersion);
if (versionA.major !== versionB.major) {
return versionA.major - versionB.major;
}
if (versionA.minor !== versionB.minor) {
return versionA.minor - versionB.minor;
}
if (versionA.patch !== versionB.patch) {
return versionA.patch - versionB.patch;
}
if (versionA.tag && !versionB.tag) return -1;
if (!versionA.tag && versionB.tag) return 1;
if (versionA.tag && versionB.tag) {
const tags = ["alpha", "beta", ""];
return tags.indexOf(versionA.tag) - tags.indexOf(versionB.tag);
}
return 0;
},
}, },
}); });
</script> </script>

View file

@ -43,11 +43,13 @@ export default defineConfig({
name: "__APPNAMEOVERWRITE__", name: "__APPNAMEOVERWRITE__",
short_name: "__APPNAMEOVERWRITE__", short_name: "__APPNAMEOVERWRITE__",
theme_color: "#990b00", theme_color: "#990b00",
display: "standalone",
start_url: "/",
icons: [ icons: [
{ {
src: "favicon.ico", src: "favicon.ico",
sizes: "48x48", sizes: "48x48",
type: "image/png", type: "image/ico",
}, },
{ {
src: "favicon.png", src: "favicon.png",