-
Datenschutz
-
Impressum
+
{{ appCustom_login_message }}
+
-
{{ config.custom_login_message }}
FF Admin
entwickelt von
@@ -14,5 +15,21 @@
+
+
diff --git a/src/components/Header.vue b/src/components/Header.vue
index d2235fa..9872753 100644
--- a/src/components/Header.vue
+++ b/src/components/Header.vue
@@ -1,9 +1,9 @@
-
+
- {{ config.app_name_overwrite || "FF Admin" }}
+ {{ clubName }}
@@ -37,15 +37,17 @@ import { useAuthStore } from "@/stores/auth";
import { useNavigationStore } from "@/stores/admin/navigation";
import TopLevelLink from "./admin/TopLevelLink.vue";
import UserMenu from "./UserMenu.vue";
-import { config } from "@/config";
+
+
diff --git a/src/components/admin/management/setting/BackupSetting.vue b/src/components/admin/management/setting/BackupSetting.vue
new file mode 100644
index 0000000..f71a36a
--- /dev/null
+++ b/src/components/admin/management/setting/BackupSetting.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/admin/management/setting/BaseSetting.vue b/src/components/admin/management/setting/BaseSetting.vue
new file mode 100644
index 0000000..5091d89
--- /dev/null
+++ b/src/components/admin/management/setting/BaseSetting.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
diff --git a/src/components/admin/management/setting/ClubImageSetting.vue b/src/components/admin/management/setting/ClubImageSetting.vue
new file mode 100644
index 0000000..adc4630
--- /dev/null
+++ b/src/components/admin/management/setting/ClubImageSetting.vue
@@ -0,0 +1,152 @@
+
+
+
+
Vereins-Icon
+
+
+
+ Kein eigenes Icon ausgewählt
+
+
![]()
+
+
+
+
+
+
Vereins-Logo
+
+
+
+ Kein eigenes Logo ausgewählt
+
+
![]()
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/admin/management/setting/ClubSetting.vue b/src/components/admin/management/setting/ClubSetting.vue
new file mode 100644
index 0000000..8b6cbcc
--- /dev/null
+++ b/src/components/admin/management/setting/ClubSetting.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/admin/management/setting/MailSetting.vue b/src/components/admin/management/setting/MailSetting.vue
new file mode 100644
index 0000000..4b09057
--- /dev/null
+++ b/src/components/admin/management/setting/MailSetting.vue
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/admin/management/setting/SessionSetting.vue b/src/components/admin/management/setting/SessionSetting.vue
new file mode 100644
index 0000000..712ea75
--- /dev/null
+++ b/src/components/admin/management/setting/SessionSetting.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/setup/Account.vue b/src/components/setup/Account.vue
new file mode 100644
index 0000000..95b88bd
--- /dev/null
+++ b/src/components/setup/Account.vue
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
diff --git a/src/components/setup/App.vue b/src/components/setup/App.vue
new file mode 100644
index 0000000..fbc2079
--- /dev/null
+++ b/src/components/setup/App.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
diff --git a/src/components/setup/Club.vue b/src/components/setup/Club.vue
new file mode 100644
index 0000000..96fa4f9
--- /dev/null
+++ b/src/components/setup/Club.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
diff --git a/src/components/setup/Finished.vue b/src/components/setup/Finished.vue
new file mode 100644
index 0000000..c711b4e
--- /dev/null
+++ b/src/components/setup/Finished.vue
@@ -0,0 +1,3 @@
+
+ Sie haben einen Verifizierungslink per Mail erhalten.
+
diff --git a/src/components/setup/Images.vue b/src/components/setup/Images.vue
new file mode 100644
index 0000000..33f5c23
--- /dev/null
+++ b/src/components/setup/Images.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
diff --git a/src/components/setup/Mail.vue b/src/components/setup/Mail.vue
new file mode 100644
index 0000000..170c541
--- /dev/null
+++ b/src/components/setup/Mail.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
diff --git a/src/config.ts b/src/config.ts
index 1351e98..ccba337 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,15 +1,7 @@
export interface Config {
server_address: string;
- app_name_overwrite: string;
- imprint_link: string;
- privacy_link: string;
- custom_login_message: string;
}
export const config: Config = {
server_address: import.meta.env.VITE_SERVER_ADDRESS,
- app_name_overwrite: import.meta.env.VITE_APP_NAME_OVERWRITE,
- imprint_link: import.meta.env.VITE_IMPRINT_LINK,
- privacy_link: import.meta.env.VITE_PRIVACY_LINK,
- custom_login_message: import.meta.env.VITE_CUSTOM_LOGIN_MESSAGE,
};
diff --git a/src/main.css b/src/main.css
index e618f2f..9f95aa5 100644
--- a/src/main.css
+++ b/src/main.css
@@ -18,7 +18,7 @@
--error: #9a0d55;
--warning: #bb6210;
--info: #388994;
- --success: #73ad0f;
+ --success: #7ac142;
}
.dark {
--primary: #ff0d00;
@@ -27,7 +27,7 @@
--error: #9a0d55;
--warning: #bb6210;
--info: #4ccbda;
- --success: #73ad0f;
+ --success: #7ac142;
}
}
diff --git a/src/main.ts b/src/main.ts
index fdb7f03..8939925 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -9,6 +9,9 @@ import "../node_modules/nprogress/nprogress.css";
import { http } from "./serverCom";
import "./main.css";
+// auto generates splash screen for iOS
+import "pwacompat";
+
NProgress.configure({ showSpinner: false });
const app = createApp(App);
diff --git a/src/router/index.ts b/src/router/index.ts
index e2e484c..0be84bf 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -2,14 +2,12 @@ import { createRouter, createWebHistory } from "vue-router";
import Login from "@/views/Login.vue";
import { isAuthenticated } from "./authGuard";
-import { loadAccountData } from "./accountGuard";
import { isSetup } from "./setupGuard";
import { abilityAndNavUpdate } from "./adminGuard";
import type { PermissionType, PermissionSection, PermissionModule } from "@/types/permissionTypes";
import { resetMemberStores, setMemberId } from "./memberGuard";
import { resetProtocolStores, setProtocolId } from "./protocolGuard";
import { resetNewsletterStores, setNewsletterId } from "./newsletterGuard";
-import { config } from "../config";
import { setBackupPage } from "./backupGuard";
const router = createRouter({
@@ -642,6 +640,13 @@ const router = createRouter({
},
],
},
+ {
+ path: "settings",
+ name: "admin-management-setting",
+ component: () => import("@/views/admin/management/setting/Setting.vue"),
+ meta: { type: "read", section: "management", module: "setting" },
+ beforeEnter: [abilityAndNavUpdate],
+ },
{
path: "backup",
name: "admin-management-backup-route",
@@ -777,10 +782,6 @@ const router = createRouter({
],
});
-router.afterEach((to, from) => {
- document.title = config.app_name_overwrite || "FF Admin";
-});
-
export default router;
declare module "vue-router" {
diff --git a/src/serverCom.ts b/src/serverCom.ts
index 5249bf1..ad4d39f 100644
--- a/src/serverCom.ts
+++ b/src/serverCom.ts
@@ -135,4 +135,4 @@ async function* streamingFetch(path: string, abort?: AbortController) {
}
}
-export { http, newEventSource, streamingFetch, host };
+export { http, newEventSource, streamingFetch, host, url };
diff --git a/src/stores/admin/management/setting.ts b/src/stores/admin/management/setting.ts
new file mode 100644
index 0000000..da8b455
--- /dev/null
+++ b/src/stores/admin/management/setting.ts
@@ -0,0 +1,103 @@
+import { defineStore } from "pinia";
+import { http } from "@/serverCom";
+import type { SettingString, SettingTopic, SettingValueMapping } from "@/types/settingTypes";
+import type { AxiosResponse } from "axios";
+
+export const useSettingStore = defineStore("setting", {
+ state: () => {
+ return {
+ settings: {} as { [key in SettingString]: SettingValueMapping[key] },
+ loading: "loading" as "loading" | "fetched" | "failed",
+ };
+ },
+ getters: {
+ readSetting:
+ (state) =>
+
(key: K): SettingValueMapping[K] => {
+ return state.settings[key];
+ },
+ readByTopic:
+ (state) =>
+ (
+ topic: T
+ ): { [K in SettingString as K extends `${T}.${string}` ? K : never]: SettingValueMapping[K] } => {
+ return Object.entries(state.settings).reduce((acc, [key, value]) => {
+ const typedKey = key as SettingString;
+ if (key.startsWith(topic)) {
+ acc[typedKey] = value;
+ }
+ return acc;
+ }, {} as any);
+ },
+ },
+ actions: {
+ fetchSettings() {
+ this.loading = "loading";
+ http
+ .get("/admin/setting")
+ .then((result) => {
+ this.settings = result.data;
+ this.loading = "fetched";
+ })
+ .catch((err) => {
+ this.loading = "failed";
+ });
+ },
+ async getSetting(key: SettingString): Promise> {
+ return await http.get(`/admin/setting/${key}`).then((res) => {
+ //@ts-expect-error
+ this.settings[key] = res.data;
+ return res;
+ });
+ },
+ async updateSetting(
+ key: K,
+ value: SettingValueMapping[K]
+ ): Promise> {
+ return await http
+ .put("/admin/setting", {
+ setting: key,
+ value: value,
+ })
+ .then((res) => {
+ this.settings[key] = value;
+ return res;
+ });
+ },
+ async updateSettings(
+ data: { key: K; value: SettingValueMapping[K] }[]
+ ): Promise> {
+ return await http.put("/admin/setting/multi", data).then((res) => {
+ for (const element of data) {
+ this.settings[element.key] = element.value;
+ }
+ return res;
+ });
+ },
+ async uploadImage(
+ data: { key: "club.logo" | "club.icon"; value?: File | "keep" }[]
+ ): Promise> {
+ const formData = new FormData();
+ for (let entry of data) {
+ if (entry.value) {
+ formData.append(typeof entry.value == "string" ? entry.key : entry.key.split(".")[1], entry.value);
+ }
+ }
+ return await http
+ .put("/admin/setting/images", formData, {
+ headers: {
+ "Content-Type": "multipart/form-data",
+ },
+ })
+ .then((res) => {
+ for (const element of data) {
+ this.settings[element.key] = element.value ? "configured" : "";
+ }
+ return res;
+ });
+ },
+ async resetSetting(key: SettingString): Promise> {
+ return await http.delete(`/admin/setting/${key}`);
+ },
+ },
+});
diff --git a/src/stores/admin/navigation.ts b/src/stores/admin/navigation.ts
index cc9024a..116bbd6 100644
--- a/src/stores/admin/navigation.ts
+++ b/src/stores/admin/navigation.ts
@@ -137,6 +137,7 @@ export const useNavigationStore = defineStore("navigation", {
...(abilityStore.can("read", "management", "user") ? [{ key: "user", title: "Benutzer" }] : []),
...(abilityStore.can("read", "management", "role") ? [{ key: "role", title: "Rollen" }] : []),
...(abilityStore.can("read", "management", "webapi") ? [{ key: "webapi", title: "Webapi-Token" }] : []),
+ ...(abilityStore.can("read", "management", "setting") ? [{ key: "setting", title: "Einstellungen" }] : []),
...(abilityStore.can("read", "management", "backup") ? [{ key: "backup", title: "Backups" }] : []),
...(abilityStore.isAdmin() ? [{ key: "version", title: "Version" }] : []),
],
diff --git a/src/stores/configuration.ts b/src/stores/configuration.ts
new file mode 100644
index 0000000..1e8bfd8
--- /dev/null
+++ b/src/stores/configuration.ts
@@ -0,0 +1,34 @@
+import { defineStore } from "pinia";
+import { http } from "../serverCom";
+
+export const useConfigurationStore = defineStore("configuration", {
+ state: () => {
+ return {
+ clubName: "",
+ clubImprint: "",
+ clubPrivacy: "",
+ clubWebsite: "",
+ appCustom_login_message: "",
+ appShow_link_to_calendar: false as boolean,
+
+ serverOffline: false as boolean,
+ };
+ },
+ actions: {
+ configure() {
+ http
+ .get("/public/configuration")
+ .then((res) => {
+ this.clubName = res.data["club.name"];
+ this.clubImprint = res.data["club.imprint"];
+ this.clubPrivacy = res.data["club.privacy"];
+ this.clubWebsite = res.data["club.website"];
+ this.appCustom_login_message = res.data["app.custom_login_message"];
+ this.appShow_link_to_calendar = res.data["app.show_link_to_calendar"];
+ })
+ .catch(() => {
+ this.serverOffline = true;
+ });
+ },
+ },
+});
diff --git a/src/stores/setup.ts b/src/stores/setup.ts
new file mode 100644
index 0000000..3958eee
--- /dev/null
+++ b/src/stores/setup.ts
@@ -0,0 +1,130 @@
+import { defineStore } from "pinia";
+import { http } from "../serverCom";
+import type { AxiosResponse } from "axios";
+import { useConfigurationStore } from "./configuration";
+
+export const useSetupStore = defineStore("setup", {
+ state: () => {
+ return {
+ dictionary: ["club", "clubImages", "app", "mail", "account", "finished"],
+ step: 0 as number,
+ successfull: 0 as number,
+ };
+ },
+ getters: {
+ stepIndex: (state) => (dict: string) => state.dictionary.findIndex((d) => d == dict),
+ },
+ actions: {
+ skip(dict: string) {
+ let myIndex = this.stepIndex(dict);
+ this.step += 1;
+ if (this.successfull <= myIndex) {
+ this.successfull = myIndex + 1;
+ }
+ },
+ async setClub(data: {
+ name?: string;
+ imprint?: string;
+ privacy?: string;
+ website?: string;
+ }): Promise> {
+ let configStore = useConfigurationStore();
+
+ let myIndex = this.stepIndex("club");
+ const res = await http.post(`/setup/club`, {
+ name: data.name,
+ imprint: data.imprint,
+ privacy: data.privacy,
+ website: data.website,
+ });
+ configStore.configure();
+
+ this.step += 1;
+ if (this.successfull <= myIndex) {
+ this.successfull = myIndex;
+ }
+ return res;
+ },
+ async setClubImages(data: { icon?: File; logo?: File }): Promise> {
+ let configStore = useConfigurationStore();
+
+ let myIndex = this.stepIndex("clubImages");
+ const formData = new FormData();
+
+ if (data.icon) {
+ formData.append("icon", data.icon);
+ }
+
+ if (data.logo) {
+ formData.append("logo", data.logo);
+ }
+
+ const res = await http.post(`/setup/club/images`, formData, {
+ headers: {
+ "Content-Type": "multipart/form-data",
+ },
+ });
+ configStore.configure();
+
+ this.step += 1;
+ if (this.successfull <= myIndex) {
+ this.successfull = myIndex;
+ }
+ return res;
+ },
+ async setApp(data: { login_message: string; show_cal_link: boolean }): Promise> {
+ let myIndex = this.stepIndex("app");
+ const res = await http.post(`/setup/app`, {
+ custom_login_message: data.login_message,
+ show_link_to_calendar: data.show_cal_link,
+ });
+ this.step += 1;
+ if (this.successfull <= myIndex) {
+ this.successfull = myIndex;
+ }
+ return res;
+ },
+ async setMailConfig(data: {
+ host: string;
+ port: number;
+ secure: boolean;
+ mail: string;
+ username: string;
+ password: string;
+ }): Promise> {
+ let myIndex = this.stepIndex("mail");
+ const res = await http.post(`/setup/mail`, {
+ host: data.host,
+ port: data.port,
+ secure: data.secure,
+ mail: data.mail,
+ username: data.username,
+ password: data.password,
+ });
+ this.step += 1;
+ if (this.successfull <= myIndex) {
+ this.successfull = myIndex;
+ }
+ return res;
+ },
+ async createAdmin(credentials: {
+ username: string;
+ mail: string;
+ firstname: string;
+ lastname: string;
+ }): Promise> {
+ let myIndex = this.stepIndex("account");
+ const res = await http.post(`/setup/me`, {
+ username: credentials.username,
+ mail: credentials.mail,
+ firstname: credentials.firstname,
+ lastname: credentials.lastname,
+ });
+ this.step += 1;
+ if (this.successfull < myIndex) {
+ this.successfull = myIndex;
+ }
+ return res;
+ },
+ },
+});
diff --git a/src/types/permissionTypes.ts b/src/types/permissionTypes.ts
index e9ed138..72f15c7 100644
--- a/src/types/permissionTypes.ts
+++ b/src/types/permissionTypes.ts
@@ -21,7 +21,8 @@ export type PermissionModule =
| "query_store"
| "template"
| "template_usage"
- | "backup";
+ | "backup"
+ | "setting";
export type PermissionType = "read" | "create" | "update" | "delete";
@@ -67,6 +68,7 @@ export const permissionModules: Array = [
"template",
"template_usage",
"backup",
+ "setting",
];
export const permissionTypes: Array = ["read", "create", "update", "delete"];
export const sectionsAndModules: SectionsAndModulesObject = {
@@ -84,5 +86,5 @@ export const sectionsAndModules: SectionsAndModulesObject = {
"template_usage",
"newsletter_config",
],
- management: ["user", "role", "webapi", "backup"],
+ management: ["user", "role", "webapi", "backup", "setting"],
};
diff --git a/src/types/settingTypes.ts b/src/types/settingTypes.ts
new file mode 100644
index 0000000..43d77a3
--- /dev/null
+++ b/src/types/settingTypes.ts
@@ -0,0 +1,80 @@
+export type SettingTopic = "club" | "app" | "session" | "mail" | "backup" | "security";
+export type SettingString =
+ | "club.icon"
+ | "club.logo"
+ | "club.name"
+ | "club.imprint"
+ | "club.privacy"
+ | "club.website"
+ | "app.custom_login_message"
+ | "app.show_link_to_calendar"
+ | "session.jwt_expiration"
+ | "session.refresh_expiration"
+ | "session.pwa_refresh_expiration"
+ | "mail.email"
+ | "mail.username"
+ | "mail.password"
+ | "mail.host"
+ | "mail.port"
+ | "mail.secure"
+ | "backup.interval"
+ | "backup.copies";
+
+export type SettingTypeAtom = "longstring" | "string" | "ms" | "number" | "boolean" | "url" | "email";
+export type SettingType = SettingTypeAtom | `${SettingTypeAtom}/crypt` | `${SettingTypeAtom}/rand`;
+
+export type SettingValueMapping = {
+ "club.icon": string;
+ "club.logo": string;
+ "club.name": string;
+ "club.imprint": string;
+ "club.privacy": string;
+ "club.website": string;
+ "app.custom_login_message": string;
+ "app.show_link_to_calendar": boolean;
+ "session.jwt_expiration": string;
+ "session.refresh_expiration": string;
+ "session.pwa_refresh_expiration": string;
+ "mail.email": string;
+ "mail.username": string;
+ "mail.password": string;
+ "mail.host": string;
+ "mail.port": number;
+ "mail.secure": boolean;
+ "backup.interval": number;
+ "backup.copies": number;
+};
+
+// Typsicherer Zugriff auf Settings
+export type SettingDefinition = {
+ type: T;
+ default?: string | number | boolean;
+ optional?: boolean;
+ min?: T extends "number" | `number/crypt` | `number/rand` ? number : never;
+};
+
+export type SettingsSchema = {
+ [key in SettingString]: SettingDefinition;
+};
+
+export const settingsType: SettingsSchema = {
+ "club.icon": { type: "string", optional: true },
+ "club.logo": { type: "string", optional: true },
+ "club.name": { type: "string", default: "FF Admin" },
+ "club.imprint": { type: "url", optional: true },
+ "club.privacy": { type: "url", optional: true },
+ "club.website": { type: "url", optional: true },
+ "app.custom_login_message": { type: "string", optional: true },
+ "app.show_link_to_calendar": { type: "boolean", default: true },
+ "session.jwt_expiration": { type: "ms", default: "15m" },
+ "session.refresh_expiration": { type: "ms", default: "1d" },
+ "session.pwa_refresh_expiration": { type: "ms", default: "5d" },
+ "mail.email": { type: "email", optional: false },
+ "mail.username": { type: "string", optional: false },
+ "mail.password": { type: "string/crypt", optional: false },
+ "mail.host": { type: "url", optional: false },
+ "mail.port": { type: "number", default: 587 },
+ "mail.secure": { type: "boolean", default: false },
+ "backup.interval": { type: "number", default: 1, min: 1 },
+ "backup.copies": { type: "number", default: 7, min: 1 },
+};
diff --git a/src/views/Login.vue b/src/views/Login.vue
index 5a8b9f4..2ad96f7 100644
--- a/src/views/Login.vue
+++ b/src/views/Login.vue
@@ -2,9 +2,9 @@
-

+
@@ -50,7 +50,9 @@ 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";
+
+
diff --git a/src/views/invite/Verify.vue b/src/views/invite/Verify.vue
index 8fa1b2c..f2cbed2 100644
--- a/src/views/invite/Verify.vue
+++ b/src/views/invite/Verify.vue
@@ -2,7 +2,7 @@
-

+
@@ -49,6 +49,7 @@ 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";
diff --git a/src/views/setup/Verify.vue b/src/views/setup/Verify.vue
index 6e6c666..06a6bf0 100644
--- a/src/views/setup/Verify.vue
+++ b/src/views/setup/Verify.vue
@@ -2,7 +2,7 @@
-

+
@@ -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";