API
authorAdam Dullage <redacted>
Sat, 27 Apr 2024 15:31:59 +0000 (16:31 +0100)
committerAdam Dullage <redacted>
Sat, 27 Apr 2024 15:31:59 +0000 (16:31 +0100)
client/App.vue
client/api.js [new file with mode: 0644]
client/constants.js [new file with mode: 0644]
client/globalStore.js [new file with mode: 0644]
client/index.js
client/tokenStorage.js [new file with mode: 0644]
client/views/LogIn.vue
package-lock.json
package.json
vite.config.js

index 507633d1a0d4af82d295ea5388df63f130133bf4..bdd0fa9883beecf80032b77a5733ae8424a54b0e 100644 (file)
@@ -5,5 +5,24 @@
 </template>\r
 \r
 <script setup>\r
+import { onBeforeMount } from "vue";\r
 import { RouterView } from "vue-router";\r
+\r
+import { getConfig } from "./api.js";\r
+import { useGlobalStore } from "./globalStore.js";\r
+\r
+const globalStore = useGlobalStore();\r
+\r
+onBeforeMount(() => {\r
+  getConfig()\r
+    .then((response) => {\r
+      globalStore.authType = response.data.authType;\r
+    })\r
+    .catch(function (error) {\r
+      if (!error.handled) {\r
+        // TODO: Trigger unknown error toast\r
+        console.error(error);\r
+      }\r
+    });\r
+});\r
 </script>\r
diff --git a/client/api.js b/client/api.js
new file mode 100644 (file)
index 0000000..d95884d
--- /dev/null
@@ -0,0 +1,45 @@
+import * as constants from "./constants.js";
+
+import axios from "axios";
+import { getToken } from "./tokenStorage.js";
+import router from "./router.js";
+
+const api = axios.create();
+
+api.interceptors.request.use(
+  // If the request is not for the token endpoint, add the token to the headers.
+  function (config) {
+    if (config.url !== "/api/token") {
+      const token = getToken();
+      config.headers.Authorization = `Bearer ${token}`;
+    }
+    return config;
+  },
+  function (error) {
+    return Promise.reject(error);
+  },
+);
+
+api.interceptors.response.use(
+  function (response) {
+    return response;
+  },
+  function (error) {
+    // If the response is a 401 Unauthorized, redirect to the login page.
+    if (
+      error.response?.status === 401 &&
+      router.currentRoute.value.name !== "login"
+    ) {
+      const redirectPath = router.currentRoute.value.fullPath;
+      router.push({
+        name: "login",
+        query: { [constants.params.redirect]: redirectPath },
+      });
+    }
+    return Promise.reject(error);
+  },
+);
+
+export function getConfig() {
+  return api.get("/api/config");
+}
diff --git a/client/constants.js b/client/constants.js
new file mode 100644 (file)
index 0000000..25f88f0
--- /dev/null
@@ -0,0 +1,46 @@
+// Params
+export const params = {
+  searchTerm: "term",
+  redirect: "redirect",
+  showHighlights: "showHighlights",
+  sortBy: "sortBy",
+};
+
+// Other
+export const alphabet = [
+  "A",
+  "B",
+  "C",
+  "D",
+  "E",
+  "F",
+  "G",
+  "H",
+  "I",
+  "J",
+  "K",
+  "L",
+  "M",
+  "N",
+  "O",
+  "P",
+  "Q",
+  "R",
+  "S",
+  "T",
+  "U",
+  "V",
+  "W",
+  "X",
+  "Y",
+  "Z",
+];
+
+export const searchSortOptions = { score: 0, title: 1, lastModified: 2 };
+
+export const authTypes = {
+  none: "none",
+  readOnly: "read_only",
+  password: "password",
+  totp: "totp",
+};
diff --git a/client/globalStore.js b/client/globalStore.js
new file mode 100644 (file)
index 0000000..ca9c6f5
--- /dev/null
@@ -0,0 +1,8 @@
+import { defineStore } from "pinia";
+import { ref } from "vue";
+
+export const useGlobalStore = defineStore("global", () => {
+  const authType = ref("authType");
+
+  return { authType };
+});
index f7f8f7f4f5c1a185fd6969526e1dc5012ebff865..e89ab7a991931a717ed7fbf93d883ca519889929 100644 (file)
@@ -1,7 +1,11 @@
 import App from "/App.vue";\r
 import { createApp } from "vue";\r
+import { createPinia } from 'pinia';\r
 import router from "/router.js";\r
 \r
 const app = createApp(App);\r
+const pinia = createPinia()\r
+\r
 app.use(router);\r
+app.use(pinia)\r
 app.mount("#app");\r
diff --git a/client/tokenStorage.js b/client/tokenStorage.js
new file mode 100644 (file)
index 0000000..dc0fc08
--- /dev/null
@@ -0,0 +1,31 @@
+const tokenStorageKey = "token";
+
+function getCookieString(token) {
+  return `${tokenStorageKey}=${token}; path=/attachments; SameSite=Strict`;
+}
+
+export function setToken(token, persist = false) {
+  document.cookie = getCookieString(token);
+  sessionStorage.setItem(tokenStorageKey, token);
+  if (persist === true) {
+    localStorage.setItem(tokenStorageKey, token);
+  }
+}
+
+export function getToken() {
+  return sessionStorage.getItem(tokenStorageKey);
+}
+
+export function loadToken() {
+  const token = localStorage.getItem(tokenStorageKey);
+  if (token != null) {
+    setToken(token, false);
+  }
+}
+
+export function clearToken() {
+  sessionStorage.removeItem(tokenStorageKey);
+  localStorage.removeItem(tokenStorageKey);
+  document.cookie =
+    getCookieString() + "; expires=Thu, 01 Jan 1970 00:00:00 GMT";
+}
index 70ef3c2edb3cc4d8da559a09b09321ce2bd40713..616e7c5e8e60678b846c6c612769cdaa3e8597b3 100644 (file)
@@ -4,7 +4,12 @@
     <form @submit.prevent="login" class="flex max-w-80 flex-col items-center">
       <TextInput placeholder="Username" class="mb-1" required />
       <TextInput placeholder="Password" type="password" class="mb-1" required />
-      <TextInput placeholder="2FA Code" class="mb-1" required />
+      <TextInput
+        v-if="globalStore.authType == authTypes.totp"
+        placeholder="2FA Code"
+        class="mb-1"
+        required
+      />
       <div class="mb-4 flex">
         <input
           type="checkbox"
@@ -27,8 +32,11 @@ import { useRouter } from "vue-router";
 import CustomButton from "../components/CustomButton.vue";
 import Logo from "../components/Logo.vue";
 import TextInput from "../components/TextInput.vue";
+import { authTypes } from "../constants.js";
+import { useGlobalStore } from "../globalStore.js";
 
 const router = useRouter();
+const globalStore = useGlobalStore();
 const rememberMe = ref(false);
 
 function login() {
index dc9030368dedc91c40d707d6be04ef2b45579b5e..abc53abc285385a5e34bb52a6539d538f1df2bff 100644 (file)
@@ -14,6 +14,7 @@
         "@mdi/light-js": "0.2.63",\r
         "axios": "1.6.8",\r
         "mousetrap": "1.6.5",\r
+        "pinia": "2.1.7",\r
         "vue": "3.4.24",\r
         "vue-router": "4.3.2"\r
       },\r
         "node": ">=0.10.0"\r
       }\r
     },\r
+    "node_modules/pinia": {\r
+      "version": "2.1.7",\r
+      "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.7.tgz",\r
+      "integrity": "sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==",\r
+      "dependencies": {\r
+        "@vue/devtools-api": "^6.5.0",\r
+        "vue-demi": ">=0.14.5"\r
+      },\r
+      "funding": {\r
+        "url": "https://github.com/sponsors/posva"\r
+      },\r
+      "peerDependencies": {\r
+        "@vue/composition-api": "^1.4.0",\r
+        "typescript": ">=4.4.4",\r
+        "vue": "^2.6.14 || ^3.3.0"\r
+      },\r
+      "peerDependenciesMeta": {\r
+        "@vue/composition-api": {\r
+          "optional": true\r
+        },\r
+        "typescript": {\r
+          "optional": true\r
+        }\r
+      }\r
+    },\r
+    "node_modules/pinia/node_modules/vue-demi": {\r
+      "version": "0.14.7",\r
+      "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz",\r
+      "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==",\r
+      "hasInstallScript": true,\r
+      "bin": {\r
+        "vue-demi-fix": "bin/vue-demi-fix.js",\r
+        "vue-demi-switch": "bin/vue-demi-switch.js"\r
+      },\r
+      "engines": {\r
+        "node": ">=12"\r
+      },\r
+      "funding": {\r
+        "url": "https://github.com/sponsors/antfu"\r
+      },\r
+      "peerDependencies": {\r
+        "@vue/composition-api": "^1.0.0-rc.1",\r
+        "vue": "^3.0.0-0 || ^2.6.0"\r
+      },\r
+      "peerDependenciesMeta": {\r
+        "@vue/composition-api": {\r
+          "optional": true\r
+        }\r
+      }\r
+    },\r
     "node_modules/pirates": {\r
       "version": "4.0.6",\r
       "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",\r
index 40e72e95dc98e01f5f4c0f8e0ecca4436dba0cea..8b122ad8077170cfa7b63712ce8caa0beb3cf664 100644 (file)
@@ -16,6 +16,7 @@
     "@mdi/light-js": "0.2.63",\r
     "axios": "1.6.8",\r
     "mousetrap": "1.6.5",\r
+    "pinia": "2.1.7",\r
     "vue": "3.4.24",\r
     "vue-router": "4.3.2"\r
   },\r
index 6ec45fe68b6f7bb474cf9ae4917e9e544eb9f952..99645b28553dde127f0bcb63bd1d9629e6404f03 100644 (file)
@@ -1,10 +1,30 @@
 import { defineConfig } from "vite";\r
 import vue from "@vitejs/plugin-vue";\r
 \r
+const devApiUrl = "http://127.0.0.1:8000";\r
+\r
 export default defineConfig({\r
   plugins: [vue()],\r
   root: "client",\r
   server: {\r
     port: 8080,\r
+    proxy: {\r
+      "/api/": {\r
+        target: devApiUrl,\r
+        changeOrigin: true,\r
+      },\r
+      "/docs": {\r
+        target: devApiUrl,\r
+        changeOrigin: true,\r
+      },\r
+      "/openapi.json": {\r
+        target: devApiUrl,\r
+        changeOrigin: true,\r
+      },\r
+      "/health": {\r
+        target: devApiUrl,\r
+        changeOrigin: true,\r
+      },\r
+    },\r
   },\r
 });\r
git clone https://git.99rst.org/PROJECT