diff --git a/index.html b/index.html
index 87432ad..d832af0 100644
--- a/index.html
+++ b/index.html
@@ -2,9 +2,9 @@
-
+
- Fireportal
+ Mitgliederverwaltung
diff --git a/package-lock.json b/package-lock.json
index 8a81714..520c856 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,9 @@
"license": "GPL-3.0-only",
"dependencies": {
"@headlessui/vue": "^1.7.13",
+ "@heroicons/vue": "^1.0.6",
"axios": "^0.26.1",
+ "jwt-decode": "^4.0.0",
"nprogress": "^0.2.0",
"pdf-dist": "^1.0.0",
"pinia": "^2.1.7",
@@ -2417,6 +2419,15 @@
"vue": "^3.2.0"
}
},
+ "node_modules/@heroicons/vue": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@heroicons/vue/-/vue-1.0.6.tgz",
+ "integrity": "sha512-ng2YcCQrdoQWEFpw+ipFl2rZo8mZ56v0T5+MyfQQvNqfKChwgP6DMloZLW+rl17GEcHkE3H82UTAMKBKZr4+WA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "vue": ">= 3"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -6500,6 +6511,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/jwt-decode": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
+ "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
diff --git a/package.json b/package.json
index 177bc25..e94b600 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,9 @@
"license": "GPL-3.0-only",
"dependencies": {
"@headlessui/vue": "^1.7.13",
+ "@heroicons/vue": "^1.0.6",
"axios": "^0.26.1",
+ "jwt-decode": "^4.0.0",
"nprogress": "^0.2.0",
"pdf-dist": "^1.0.0",
"pinia": "^2.1.7",
diff --git a/public/FFW-Logo.svg b/public/FFW-Logo.svg
new file mode 100644
index 0000000..97abd6f
--- /dev/null
+++ b/public/FFW-Logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/FW-Wappen.ico b/public/FW-Wappen.ico
new file mode 100644
index 0000000..8019175
Binary files /dev/null and b/public/FW-Wappen.ico differ
diff --git a/public/FW-Wappen.png b/public/FW-Wappen.png
new file mode 100644
index 0000000..5e07171
Binary files /dev/null and b/public/FW-Wappen.png differ
diff --git a/public/FW-Wappen.svg b/public/FW-Wappen.svg
new file mode 100644
index 0000000..65094dd
--- /dev/null
+++ b/public/FW-Wappen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/App.vue b/src/App.vue
index 2446940..bd8a4af 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,9 +1,32 @@
+
+
+
+
+
+
+
+
-
-
-
-
-
+
diff --git a/src/components/FailureXMark.vue b/src/components/FailureXMark.vue
new file mode 100644
index 0000000..3c1a1ff
--- /dev/null
+++ b/src/components/FailureXMark.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
diff --git a/src/components/Footer.vue b/src/components/Footer.vue
new file mode 100644
index 0000000..ffd7fe5
--- /dev/null
+++ b/src/components/Footer.vue
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
diff --git a/src/components/Header.vue b/src/components/Header.vue
new file mode 100644
index 0000000..c47cbdc
--- /dev/null
+++ b/src/components/Header.vue
@@ -0,0 +1,36 @@
+
+
+
+
+ Mitgliederverwaltung
+
+
+
+
+
+
+
+
diff --git a/src/components/Spinner.vue b/src/components/Spinner.vue
new file mode 100644
index 0000000..acb32d6
--- /dev/null
+++ b/src/components/Spinner.vue
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/components/SuccessCheckmark.vue b/src/components/SuccessCheckmark.vue
new file mode 100644
index 0000000..ae5516f
--- /dev/null
+++ b/src/components/SuccessCheckmark.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
diff --git a/src/components/UserMenu.vue b/src/components/UserMenu.vue
new file mode 100644
index 0000000..8be4d08
--- /dev/null
+++ b/src/components/UserMenu.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
diff --git a/src/components/admin/RoutingLink.vue b/src/components/admin/RoutingLink.vue
new file mode 100644
index 0000000..a8c38da
--- /dev/null
+++ b/src/components/admin/RoutingLink.vue
@@ -0,0 +1,38 @@
+
+
+ {{ link.title }}
+
+
+
+
+
+
+@/stores/contest/viewManager
diff --git a/src/components/admin/TopLevelLink.vue b/src/components/admin/TopLevelLink.vue
new file mode 100644
index 0000000..df0afe8
--- /dev/null
+++ b/src/components/admin/TopLevelLink.vue
@@ -0,0 +1,41 @@
+
+
+ {{ link.title }}
+
+
+
+
+
+
diff --git a/src/globalProperties.config.ts b/src/globalProperties.config.ts
new file mode 100644
index 0000000..8bd0344
--- /dev/null
+++ b/src/globalProperties.config.ts
@@ -0,0 +1,15 @@
+import type { AxiosInstance } from "axios";
+import type { NProgress } from "nprogress";
+import type { Router } from "vue-router";
+
+declare module "@vue/runtime-core" {
+ interface ComponentCustomProperties {
+ $dev: boolean;
+ $http: AxiosInstance;
+ $progress: NProgress;
+ $router: Router;
+ $route: any;
+ }
+}
+
+export {}; // Important! See note.
diff --git a/src/layouts/FullContent.vue b/src/layouts/FullContent.vue
new file mode 100644
index 0000000..ae0f638
--- /dev/null
+++ b/src/layouts/FullContent.vue
@@ -0,0 +1,7 @@
+
+
+
diff --git a/src/layouts/Sidebar.vue b/src/layouts/Sidebar.vue
new file mode 100644
index 0000000..2bffff5
--- /dev/null
+++ b/src/layouts/Sidebar.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
diff --git a/src/main.css b/src/main.css
index 33230c6..6d54f3e 100644
--- a/src/main.css
+++ b/src/main.css
@@ -2,6 +2,27 @@
@tailwind components;
@tailwind utilities;
+@layer base {
+ :root {
+ --primary: #990b00;
+ --secondary: #0c6672;
+ --accent: #bb1e10;
+ --error: #9a0d55;
+ --warning: #bb6210;
+ --info: #388994;
+ --success: #73ad0f;
+ }
+ .dark {
+ --primary: #ff0d00;
+ --secondary: #0f9aa9;
+ --accent: #bb1e10;
+ --error: #9a0d55;
+ --warning: #bb6210;
+ --info: #4ccbda;
+ --success: #73ad0f;
+ }
+}
+
/* ===== Scrollbar CSS ===== */
/* Firefox */
* {
@@ -27,10 +48,34 @@
html,
body {
- @apply h-full w-screen m-0 p-0 overflow-hidden bg-white;
+ @apply h-full w-screen m-0 p-0 overflow-hidden bg-gray-100;
height: 100svh;
}
#app {
@apply w-full h-full overflow-hidden flex flex-col;
}
+
+button:not([headlessui]),
+a[button]:not([headlessui]) {
+ @apply relative box-border h-10 w-full flex justify-center py-2 px-4 text-sm font-medium rounded-md focus:outline-none focus:ring-0;
+}
+
+button[primary]:not([primary="false"]),
+a[button][primary]:not([primary="false"]) {
+ @apply border border-transparent text-white bg-primary hover:bg-primary;
+}
+
+button[primary-outline]:not([primary-outline="false"]),
+a[button][primary-outline]:not([primary-outline="false"]) {
+ @apply border-2 border-primary text-black hover:bg-primary;
+}
+
+button:disabled {
+ @apply opacity-75 pointer-events-none;
+}
+
+input:not([type="checkbox"]),
+textarea {
+ @apply rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none;
+}
diff --git a/src/main.ts b/src/main.ts
index 341b931..fdb7f03 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -3,11 +3,19 @@ import { createPinia } from "pinia";
import App from "./App.vue";
import router from "./router";
+import NProgress from "nprogress";
+import "../node_modules/nprogress/nprogress.css";
+
+import { http } from "./serverCom";
import "./main.css";
+NProgress.configure({ showSpinner: false });
+
const app = createApp(App);
app.use(createPinia());
app.use(router);
+app.config.globalProperties.$http = http;
+app.config.globalProperties.$progress = NProgress;
app.mount("#app");
diff --git a/src/router/accountGuard.ts b/src/router/accountGuard.ts
new file mode 100644
index 0000000..0ab0074
--- /dev/null
+++ b/src/router/accountGuard.ts
@@ -0,0 +1,9 @@
+import { useAccountStore } from "@/stores/account";
+
+export async function loadAccountData(to: any, from: any, next: any) {
+ const account = useAccountStore();
+ account.fetchAccountContests();
+ account.fetchAccountInvites();
+ account.fetchAccountLicense();
+ next();
+}
diff --git a/src/router/authGuards.ts b/src/router/authGuards.ts
new file mode 100644
index 0000000..07bc5df
--- /dev/null
+++ b/src/router/authGuards.ts
@@ -0,0 +1,41 @@
+import NProgress from "nprogress";
+import { useAuthStore } from "@/stores/auth";
+import { useAccountStore } from "@/stores/account";
+import { jwtDecode, type JwtPayload } from "jwt-decode";
+
+type Payload = JwtPayload & { userId: number; username: string; firstname: string; lastname: string; mail: string };
+
+export async function isAuthenticated(to: any, from: any, next: any) {
+ const auth = useAuthStore();
+ NProgress.start();
+ if (auth.authCheck && localStorage.getItem("access_token")) {
+ NProgress.done();
+ next();
+ return;
+ }
+ await isAuthenticatedPromise()
+ .then(async (result: any) => {
+ NProgress.done();
+ next();
+ })
+ .catch((err: Error) => {
+ NProgress.done();
+ next({ name: "login" });
+ });
+}
+
+export async function isAuthenticatedPromise(): Promise {
+ return new Promise((resolve, reject) => {
+ const auth = useAuthStore();
+ const account = useAccountStore();
+ let decoded = jwtDecode(localStorage.getItem("accessToken") ?? "");
+
+ auth.setSuccess();
+ if (typeof decoded == "string" || !decoded) {
+ reject("jwt failed");
+ }
+ var { firstname, lastname, mail, username } = decoded;
+ account.setAccountData(firstname, lastname, mail, username);
+ resolve(decoded);
+ });
+}
diff --git a/src/router/index.ts b/src/router/index.ts
index c843786..0ade275 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -1,13 +1,31 @@
-import { createRouter, createWebHistory } from "vue-router";
-import HomeView from "../views/HomeView.vue";
+import { createRouter, createWebHistory, createWebHashHistory } from "vue-router";
+import Login from "../views/Login.vue";
+
+import { isAuthenticated } from "./authGuards";
+import { loadAccountData } from "./accountGuard";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
- name: "home",
- component: HomeView,
+ redirect: { name: "admin" },
+ },
+ {
+ path: "/login",
+ name: "login",
+ component: Login,
+ },
+ {
+ path: "/admin",
+ name: "admin",
+ component: () => import("../views/admin/View.vue"),
+ beforeEnter: [isAuthenticated],
+ },
+ {
+ path: "/:pathMatch(.*)*",
+ name: "404",
+ component: () => import("../views/notFound.vue"),
},
],
});
diff --git a/src/serverCom.ts b/src/serverCom.ts
index 43e37c8..a58b0a3 100644
--- a/src/serverCom.ts
+++ b/src/serverCom.ts
@@ -1,7 +1,7 @@
import axios from "axios";
const http = axios.create({
- baseURL: "https://localhost:5000",
+ baseURL: "http://localhost:5000",
headers: {
"Cache-Control": "no-cache",
Pragma: "no-cache",
@@ -30,6 +30,10 @@ http.interceptors.response.use(
return response;
},
async (error) => {
+ if (error.config.url.includes("/auth")) {
+ return Promise.reject(error);
+ }
+
const originalRequest = error.config;
// Handle token expiration and retry the request with a refreshed token
diff --git a/src/shims-vue.d.ts b/src/shims-vue.d.ts
new file mode 100644
index 0000000..d77b62b
--- /dev/null
+++ b/src/shims-vue.d.ts
@@ -0,0 +1,5 @@
+declare module "*.vue" {
+ import type { DefineComponent } from "vue";
+ const component: DefineComponent<{}, {}, any>;
+ export default component;
+}
diff --git a/src/stores/account.ts b/src/stores/account.ts
new file mode 100644
index 0000000..8c11fd9
--- /dev/null
+++ b/src/stores/account.ts
@@ -0,0 +1,25 @@
+import { defineStore } from "pinia";
+
+export const useAccountStore = defineStore("account", {
+ state: () => {
+ return {
+ firstname: "" as string,
+ lastname: "" as string,
+ mail: "" as string,
+ alias: "" as string,
+ };
+ },
+ actions: {
+ logoutAccount() {
+ localStorage.removeItem("accessToken");
+ localStorage.removeItem("refreshToken");
+ window.open("/login", "_self");
+ },
+ setAccountData(firstname: string, lastname: string, mail: string, alias: string) {
+ this.firstname = firstname;
+ this.lastname = lastname;
+ this.mail = mail;
+ this.alias = alias;
+ },
+ },
+});
diff --git a/src/stores/admin/navigation.ts b/src/stores/admin/navigation.ts
new file mode 100644
index 0000000..4275d90
--- /dev/null
+++ b/src/stores/admin/navigation.ts
@@ -0,0 +1,151 @@
+import { defineStore } from "pinia";
+import { shallowRef, defineAsyncComponent } from "vue";
+
+export interface navigationModel {
+ club: navigationSplitModel;
+ settings: navigationSplitModel;
+ user: navigationSplitModel;
+}
+
+export interface navigationSplitModel {
+ topTitle?: string;
+ top?: Array;
+ mainTitle: string;
+ main: Array;
+}
+
+export type topLevelNavigationType = "club" | "settings" | "user";
+
+export interface topLevelNavigationModel {
+ key: topLevelNavigationType;
+ title: string;
+ levelDefault: string;
+}
+
+export interface navigationLinkModel {
+ key: string;
+ title: string;
+ component: any;
+}
+
+export const useNavigationStore = defineStore("navigation", {
+ state: () => {
+ return {
+ activeNavigation: "club" as topLevelNavigationType,
+ activeLink: null as null | navigationLinkModel,
+ topLevel: [
+ {
+ key: "club",
+ title: "Verein",
+ levelDefault: "#members",
+ },
+ {
+ key: "settings",
+ title: "Einstellungen",
+ levelDefault: "#qualification",
+ },
+ ] as Array,
+ navigation: {
+ club: {
+ mainTitle: "Verein",
+ main: [
+ {
+ key: "#members",
+ title: "Mitglieder",
+ component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))),
+ },
+ {
+ key: "#calendar",
+ title: "Termine",
+ component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))),
+ },
+ {
+ key: "#newsletter",
+ title: "Newsletter",
+ component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))),
+ },
+ {
+ key: "#protocol",
+ title: "Prookolle",
+ component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))),
+ },
+ ],
+ },
+ settings: {
+ mainTitle: "Einstellungen",
+ main: [
+ {
+ key: "#qualification",
+ title: "Qualifikationen",
+ component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))),
+ },
+ {
+ key: "#award",
+ title: "Auszeichnungen",
+ component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))),
+ },
+ {
+ key: "#executive_position",
+ title: "Vereinsämter",
+ component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))),
+ },
+ {
+ key: "#communication",
+ title: "Mitgliederdaten",
+ component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))),
+ },
+ ],
+ },
+ user: {
+ mainTitle: "Benutzer",
+ main: [
+ {
+ key: "#user",
+ title: "Benutzer",
+ component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))),
+ },
+ {
+ key: "#roles",
+ title: "Rollen",
+ component: shallowRef(defineAsyncComponent(() => import("@/views/admin/members/Overview.vue"))),
+ },
+ ],
+ },
+ } as navigationModel,
+ };
+ },
+ getters: {
+ activeNavigationObject: (state) => (state.navigation[state.activeNavigation] ?? {}) as navigationSplitModel,
+ activeTopLevelObject: (state) =>
+ (state.topLevel.find((elem) => elem.key == state.activeNavigation) ?? {}) as topLevelNavigationModel,
+ },
+ actions: {
+ setTopLevel(key: topLevelNavigationType, disableSubLink: boolean = true) {
+ let level = this.topLevel.find((e) => e.key == key) ?? null;
+ if (!level) {
+ this.activeNavigation = "club";
+ if (!disableSubLink) this.setLink(this.topLevel.find((e) => e.key == "club")?.levelDefault ?? null);
+ else this.setLink(null);
+ } else {
+ this.activeNavigation = level.key;
+ if (!disableSubLink) this.setLink(level.levelDefault);
+ else this.setLink(null);
+ }
+ },
+ setLink(key: string | null) {
+ let nav = this.navigation[this.activeNavigation];
+ if (!nav) {
+ this.activeLink = null;
+ return;
+ }
+ let links = [...Object.values(nav.main), ...Object.values(nav.top ?? {})];
+ this.activeLink = links.find((e) => e.key == key) ?? null;
+ },
+ setTopLevelNav(topLeveLinks: Array) {
+ this.topLevel = topLeveLinks;
+ },
+ resetNavigation() {
+ this.$reset();
+ },
+ },
+});
diff --git a/src/stores/auth.ts b/src/stores/auth.ts
new file mode 100644
index 0000000..fdaf4d2
--- /dev/null
+++ b/src/stores/auth.ts
@@ -0,0 +1,14 @@
+import { defineStore } from "pinia";
+
+export const useAuthStore = defineStore("auth", {
+ state: () => {
+ return {
+ authCheck: false,
+ };
+ },
+ actions: {
+ setSuccess() {
+ this.authCheck = true;
+ },
+ },
+});
diff --git a/src/templates/Main.vue b/src/templates/Main.vue
new file mode 100644
index 0000000..540ff88
--- /dev/null
+++ b/src/templates/Main.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/templates/Sidebar.vue b/src/templates/Sidebar.vue
new file mode 100644
index 0000000..a5f6a16
--- /dev/null
+++ b/src/templates/Sidebar.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
{{ mainTitle }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue
deleted file mode 100644
index 142ab8f..0000000
--- a/src/views/HomeView.vue
+++ /dev/null
@@ -1,5 +0,0 @@
-
- Home
-
-
-
diff --git a/src/views/Login.vue b/src/views/Login.vue
new file mode 100644
index 0000000..e3decc3
--- /dev/null
+++ b/src/views/Login.vue
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/RouterView.vue b/src/views/RouterView.vue
new file mode 100644
index 0000000..543cfdb
--- /dev/null
+++ b/src/views/RouterView.vue
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/src/views/admin/View.vue b/src/views/admin/View.vue
new file mode 100644
index 0000000..795aa52
--- /dev/null
+++ b/src/views/admin/View.vue
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/admin/members/Overview.vue b/src/views/admin/members/Overview.vue
new file mode 100644
index 0000000..1dd4ee8
--- /dev/null
+++ b/src/views/admin/members/Overview.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
Übersicht
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/notFound.vue b/src/views/notFound.vue
new file mode 100644
index 0000000..1a4e66c
--- /dev/null
+++ b/src/views/notFound.vue
@@ -0,0 +1,282 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tailwind.config.js b/tailwind.config.js
index 45ce112..59560d6 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -2,7 +2,17 @@
export default {
content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
theme: {
- extend: {},
+ extend: {
+ colors: {
+ primary: "var(--primary)",
+ secondary: "var(--secondary)",
+ accent: "var(--accent)",
+ error: "var(--error)",
+ warning: "var(--warning)",
+ info: "var(--info)",
+ success: "var(--success)",
+ },
+ },
},
plugins: [],
};