feat(web): make things work
- setup basic login form - setup basic add institution functionality
This commit is contained in:
Generated
+106
-24
@@ -8,6 +8,7 @@
|
|||||||
"name": "fiscalos-web",
|
"name": "fiscalos-web",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@jcss/vue-plaid-link": "^1.1.3",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"ts-results": "^3.3.0",
|
"ts-results": "^3.3.0",
|
||||||
@@ -1416,6 +1417,16 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@jcss/vue-plaid-link": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jcss/vue-plaid-link/-/vue-plaid-link-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-h6uV2Muh0MdVKeOp/fY2/OYMgy9kQw7JA4HoE/PkfNutVsrO/foXxczLlReUhtPq2erdOepri3Du5W1CKxFc/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vueuse/core": "^10.7.2",
|
||||||
|
"vue": "^3.4.19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
@@ -2295,6 +2306,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/web-bluetooth": {
|
||||||
|
"version": "0.0.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
|
||||||
|
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.56.0",
|
"version": "8.56.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz",
|
||||||
@@ -3161,6 +3178,42 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vueuse/core": {
|
||||||
|
"version": "10.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
|
||||||
|
"integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/web-bluetooth": "^0.0.20",
|
||||||
|
"@vueuse/metadata": "10.11.1",
|
||||||
|
"@vueuse/shared": "10.11.1",
|
||||||
|
"vue-demi": ">=0.14.8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/metadata": {
|
||||||
|
"version": "10.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz",
|
||||||
|
"integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/shared": {
|
||||||
|
"version": "10.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz",
|
||||||
|
"integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"vue-demi": ">=0.14.8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/abbrev": {
|
"node_modules/abbrev": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
|
||||||
@@ -3172,9 +3225,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.15.0",
|
"version": "8.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
@@ -3342,13 +3395,16 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.9.19",
|
"version": "2.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
|
||||||
"integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
|
"integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"baseline-browser-mapping": "dist/cli.js"
|
"baseline-browser-mapping": "dist/cli.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bidi-js": {
|
"node_modules/bidi-js": {
|
||||||
@@ -3870,9 +3926,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.286",
|
"version": "1.5.302",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
|
||||||
"integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==",
|
"integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@@ -4113,9 +4169,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-playwright": {
|
"node_modules/eslint-plugin-playwright": {
|
||||||
"version": "2.6.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-2.7.0.tgz",
|
||||||
"integrity": "sha512-Lo/na1ldsJdKA93OZC5lNdSyzTiJdaIpkxwPDx1PDnEc/Ogkt72il5TJ9J5MZSo4FlybM4h4rxPe/31sdD0MSA==",
|
"integrity": "sha512-kUgwDZL3knnuJF53WSf5xNnB1aLPnX8furoh0PSrmmFIfMfIMmY3sNd4gtZ2MUUnaIX1/A9ndYtD7bhV1dj+1g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5351,11 +5407,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minipass": {
|
"node_modules/minipass": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "BlueOak-1.0.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
@@ -7260,6 +7316,32 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-demi": {
|
||||||
|
"version": "0.14.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||||
|
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||||
|
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.0.0-rc.1",
|
||||||
|
"vue": "^3.0.0-0 || ^2.6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-eslint-parser": {
|
"node_modules/vue-eslint-parser": {
|
||||||
"version": "10.4.0",
|
"version": "10.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.0.tgz",
|
||||||
@@ -7311,14 +7393,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-router": {
|
"node_modules/vue-router": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.3.tgz",
|
||||||
"integrity": "sha512-YFhwaE5c5JcJpNB1arpkl4/GnO32wiUWRB+OEj1T0DlDxEZoOfbltl2xEwktNU/9o1sGcGburIXSpbLpPFe/6w==",
|
"integrity": "sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/generator": "^7.28.6",
|
"@babel/generator": "^7.28.6",
|
||||||
"@vue-macros/common": "^3.1.1",
|
"@vue-macros/common": "^3.1.1",
|
||||||
"@vue/devtools-api": "^8.0.0",
|
"@vue/devtools-api": "^8.0.6",
|
||||||
"ast-walker-scope": "^0.8.3",
|
"ast-walker-scope": "^0.8.3",
|
||||||
"chokidar": "^5.0.0",
|
"chokidar": "^5.0.0",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
@@ -7473,9 +7555,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/whatwg-url": {
|
"node_modules/whatwg-url": {
|
||||||
"version": "16.0.0",
|
"version": "16.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
|
||||||
"integrity": "sha512-9CcxtEKsf53UFwkSUZjG+9vydAsFO4lFHBpJUtjBcoJOCJpKnSJNwCw813zrYJHpCJ7sgfbtOe0V5Ku7Pa1XMQ==",
|
"integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"format": "prettier --write --experimental-cli src/"
|
"format": "prettier --write --experimental-cli src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@jcss/vue-plaid-link": "^1.1.3",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"ts-results": "^3.3.0",
|
"ts-results": "^3.3.0",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ button {
|
|||||||
input {
|
input {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import LeftArrowIcon from '@/components/icons/RightArrowIcon.vue';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
const isCollapsed = ref(false);
|
||||||
|
const asideClasses = computed(() => ({
|
||||||
|
collapsed: isCollapsed.value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
function handleToggleButtonClick() {
|
||||||
|
isCollapsed.value = !isCollapsed.value;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<aside :class="asideClasses">
|
||||||
|
<button
|
||||||
|
@click="handleToggleButtonClick"
|
||||||
|
type="button"
|
||||||
|
class="toggle-button"
|
||||||
|
>
|
||||||
|
<LeftArrowIcon />
|
||||||
|
</button>
|
||||||
|
</aside>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
aside {
|
||||||
|
--sidebar-width: 15.625rem;
|
||||||
|
--button-size: 1.5rem;
|
||||||
|
--transition-duration: 0.5s;
|
||||||
|
--transition-function: ease-in-out;
|
||||||
|
position: relative;
|
||||||
|
width: var(--sidebar-width);
|
||||||
|
height: 100%;
|
||||||
|
z-index: 999;
|
||||||
|
background: var(--bg-surface);
|
||||||
|
transition-property: width, transform;
|
||||||
|
transition-duration: var(--transition-duration);
|
||||||
|
transition-timing-function: var(--transition-function);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 48rem) {
|
||||||
|
aside {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: absolute;
|
||||||
|
top: 3rem;
|
||||||
|
left: calc(-1 * var(--button-size) / 2);
|
||||||
|
height: var(--button-size);
|
||||||
|
width: var(--button-size);
|
||||||
|
background: var(--bg-element);
|
||||||
|
border-radius: 50%;
|
||||||
|
transition-property: transform;
|
||||||
|
transition-duration: var(--transition-duration);
|
||||||
|
transition-timing-function: var(--transition-function);
|
||||||
|
}
|
||||||
|
|
||||||
|
aside.collapsed {
|
||||||
|
width: calc(0.125rem + var(--button-size) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
aside.collapsed .toggle-button {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button svg {
|
||||||
|
--size: 1rem;
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,13 +1,26 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { RouterView } from 'vue-router';
|
||||||
|
import NavSidebar from './NavSidebar.vue';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="layout">
|
||||||
|
<main>
|
||||||
|
<RouterView />
|
||||||
|
</main>
|
||||||
|
<NavSidebar />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.layout {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid blue;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640">
|
||||||
|
<path
|
||||||
|
d="M566.6 342.6C579.1 330.1 579.1 309.8 566.6 297.3L406.6 137.3C394.1 124.8 373.8 124.8 361.3 137.3C348.8 149.8 348.8 170.1 361.3 182.6L466.7 288L96 288C78.3 288 64 302.3 64 320C64 337.7 78.3 352 96 352L466.7 352L361.3 457.4C348.8 469.9 348.8 490.2 361.3 502.7C373.8 515.2 394.1 515.2 406.6 502.7L566.6 342.7z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { ClientConfig, ClientFactoryKey } from "@/services/client";
|
||||||
|
import { InstituionServiceFactoryKey } from "@/services/institutionService";
|
||||||
|
import type { UserStore } from "@/stores/userStore";
|
||||||
|
import { inject } from "vue";
|
||||||
|
|
||||||
|
export function useInstitutionService(store: UserStore) {
|
||||||
|
const clientFactory = inject(ClientFactoryKey);
|
||||||
|
const institutionServiceFactory = inject(InstituionServiceFactoryKey);
|
||||||
|
|
||||||
|
if (clientFactory === undefined) {
|
||||||
|
throw new Error("Failed to inject client factory.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (institutionServiceFactory === undefined) {
|
||||||
|
throw new Error("Failed to inject institution service factory.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientConfig = new ClientConfig(
|
||||||
|
{ Authorization: `Bearer ${store.user?.token}`},
|
||||||
|
true,
|
||||||
|
store.refreshAccessToken
|
||||||
|
);
|
||||||
|
const client = clientFactory.create(clientConfig);
|
||||||
|
const institutionService = institutionServiceFactory.create(client);
|
||||||
|
|
||||||
|
return institutionService;
|
||||||
|
}
|
||||||
@@ -7,11 +7,13 @@ import App from "./App.vue";
|
|||||||
import router from "./router";
|
import router from "./router";
|
||||||
import { ClientFactory, ClientFactoryKey } from "./services/client";
|
import { ClientFactory, ClientFactoryKey } from "./services/client";
|
||||||
import { AuthServiceFactory, AuthServiceFactoryKey } from "./services/authService";
|
import { AuthServiceFactory, AuthServiceFactoryKey } from "./services/authService";
|
||||||
|
import { InstituionServiceFactoryKey, InstitutionServiceFactory } from './services/institutionService';
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
app.provide(ClientFactoryKey, new ClientFactory());
|
app.provide(ClientFactoryKey, new ClientFactory());
|
||||||
app.provide(AuthServiceFactoryKey, new AuthServiceFactory());
|
app.provide(AuthServiceFactoryKey, new AuthServiceFactory());
|
||||||
|
app.provide(InstituionServiceFactoryKey, new InstitutionServiceFactory());
|
||||||
|
|
||||||
app.use(createPinia());
|
app.use(createPinia());
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { AuthService } from "@/services/authService";
|
import { AuthService } from '@/services/authService';
|
||||||
import { Client, ClientConfig } from "@/services/client";
|
import { Client, ClientConfig } from '@/services/client';
|
||||||
import { useUserStore } from "@/stores/userStore";
|
import { useUserStore } from '@/stores/userStore';
|
||||||
import { createRouter, createWebHistory } from "vue-router";
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
@@ -22,24 +22,25 @@ const router = createRouter({
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: 'login',
|
path: 'login',
|
||||||
component: () => import('../views/LoginView.vue')
|
component: () => import('../views/LoginView.vue'),
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
|
component: () => import('../components/ProtectedLayout.vue'),
|
||||||
beforeEnter: async () => {
|
beforeEnter: async () => {
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
if (userStore.user === null) {
|
if (userStore.user === null) {
|
||||||
return { path: '/public/login' }
|
return { path: '/public/login' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const isExpired = userStore.user.expiresAtInSeconds < Date.now() / 1000;
|
const isExpired = userStore.user.expiresAtInSeconds < Date.now() / 1000;
|
||||||
|
|
||||||
if (isExpired) {
|
if (isExpired) {
|
||||||
const clientConfig = new ClientConfig(
|
const clientConfig = new ClientConfig(
|
||||||
{ Authorization: `Bearer ${userStore.user.token}`},
|
{ Authorization: `Bearer ${userStore.user.token}` },
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
const client = new Client(clientConfig);
|
const client = new Client(clientConfig);
|
||||||
@@ -48,7 +49,7 @@ const router = createRouter({
|
|||||||
|
|
||||||
if (refreshResult.err) {
|
if (refreshResult.err) {
|
||||||
userStore.logUserOut();
|
userStore.logUserOut();
|
||||||
return { path: '/public/login' }
|
return { path: '/public/login' };
|
||||||
}
|
}
|
||||||
|
|
||||||
userStore.logUserIn(refreshResult.val.accessToken);
|
userStore.logUserIn(refreshResult.val.accessToken);
|
||||||
@@ -56,8 +57,13 @@ const router = createRouter({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
component: () => import('../views/HomeView.vue'),
|
children: [
|
||||||
}
|
{
|
||||||
|
path: '/',
|
||||||
|
component: () => import('../views/HomeView.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { Err, Ok, Result } from 'ts-results';
|
||||||
|
import { type InjectionKey } from 'vue';
|
||||||
|
import { ClientRequestWithBody, type IClient } from './client';
|
||||||
|
|
||||||
|
type InstitutionServiceFactoryKeyType = InjectionKey<IInstitutionServiceFactory>;
|
||||||
|
|
||||||
|
export const InstituionServiceFactoryKey: InstitutionServiceFactoryKeyType =
|
||||||
|
Symbol('AuthServiceFactory');
|
||||||
|
|
||||||
|
export interface IInstitutionServiceFactory {
|
||||||
|
create: (client: IClient) => IInstitutionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InstitutionServiceFactory implements InstitutionServiceFactory {
|
||||||
|
create(client: IClient): IInstitutionService {
|
||||||
|
return new InstitutionService(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IInstitutionService {
|
||||||
|
createLinkToken: () => Promise<Result<LinkTokenResponse, Error[]>>;
|
||||||
|
connect: (publicToken: string, plaidInstitutionId: string) => Promise<Result<boolean, Error[]>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InstitutionService implements IInstitutionService {
|
||||||
|
private readonly client: IClient;
|
||||||
|
private readonly endpoints = {
|
||||||
|
link: '/api/institutions/link',
|
||||||
|
connect: '/api/institutions/connect',
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(client: IClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createLinkToken() {
|
||||||
|
const request = new ClientRequestWithBody(this.endpoints.link);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await this.client.post(request);
|
||||||
|
|
||||||
|
if (res.ok === false) {
|
||||||
|
return Err([new Error('Failed to create link token')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await res.json();
|
||||||
|
return Ok(body as LinkTokenResponse);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return Err([new Error('Failed to create link token')]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect(publicToken: string, plaidInstitutionId: string) {
|
||||||
|
const request = new ClientRequestWithBody(this.endpoints.connect, undefined, {
|
||||||
|
publicToken,
|
||||||
|
plaidInstitutionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await this.client.post(request);
|
||||||
|
|
||||||
|
if (res.status === 400) {
|
||||||
|
const body = await res.json();
|
||||||
|
const validationErrors = body.errors as Record<string, string[]>;
|
||||||
|
const errors = Object.values(validationErrors)
|
||||||
|
.flat()
|
||||||
|
.map(e => new Error(e));
|
||||||
|
|
||||||
|
return Err(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status === 401) {
|
||||||
|
return Err([new Error('Please sign in and try again.')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.status === 409) {
|
||||||
|
return Err([new Error('You have already linked this institution.')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(true);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return Err([new Error('Failed to connect institution')]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type LinkTokenResponse = {
|
||||||
|
linkToken: string;
|
||||||
|
};
|
||||||
@@ -1,9 +1,35 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { useInstitutionService } from '@/composables/useInstitutionService';
|
||||||
import { useUserStore } from '@/stores/userStore';
|
import { useUserStore } from '@/stores/userStore';
|
||||||
|
import { usePlaidLink, type PlaidLinkOptions } from '@jcss/vue-plaid-link';
|
||||||
|
import { nextTick, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
const { logUserOut } = useUserStore();
|
const userStore = useUserStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const institutionService = useInstitutionService(userStore);
|
||||||
|
const plaidOptions = ref<PlaidLinkOptions>({
|
||||||
|
token: '',
|
||||||
|
onSuccess: async (publicToken, metadata) => {
|
||||||
|
if (metadata.institution == null) {
|
||||||
|
alert('Institution information is missing from Plaid response. Please try again.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectResult = await institutionService.connect(
|
||||||
|
publicToken,
|
||||||
|
metadata.institution.institution_id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (connectResult.err) {
|
||||||
|
alert(connectResult.val.map(e => e.message).join('\n'))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad: () => console.log('loaded'),
|
||||||
|
onExit: () => console.log('exit'),
|
||||||
|
});
|
||||||
|
const { open } = usePlaidLink(plaidOptions);
|
||||||
|
|
||||||
function handleLogout() {
|
function handleLogout() {
|
||||||
// TODO: Also need to log user
|
// TODO: Also need to log user
|
||||||
@@ -11,16 +37,46 @@ function handleLogout() {
|
|||||||
// basically just clearing
|
// basically just clearing
|
||||||
// refresh token cookie and revoking
|
// refresh token cookie and revoking
|
||||||
// it in the database
|
// it in the database
|
||||||
logUserOut();
|
userStore.logUserOut();
|
||||||
router.push({ path: '/public/login' });
|
router.push({ path: '/public/login' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleAddInstitutionClick() {
|
||||||
|
const linkTokenResult = await institutionService.createLinkToken();
|
||||||
|
|
||||||
|
if (linkTokenResult.err) {
|
||||||
|
alert(linkTokenResult.val.map(e => e.message).join('\n'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
plaidOptions.value = {
|
||||||
|
...plaidOptions.value,
|
||||||
|
token: linkTokenResult.val.linkToken,
|
||||||
|
};
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
open();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1>Home View</h1>
|
<h1>Home View</h1>
|
||||||
<button type="button" v-on:click="handleLogout">
|
<button class="logout-button" type="button" @click="handleLogout">
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
|
<div>
|
||||||
|
<button class="add-institution-button" type="button" @click="handleAddInstitutionClick">
|
||||||
|
Add Institution
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.logout-button,
|
||||||
|
.add-institution-button {
|
||||||
|
background: var(--bg-element);
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ async function handleSubmit(e: SubmitEvent) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (loginResult.err) {
|
if (loginResult.err) {
|
||||||
alert(loginResult.val.join('\n'));
|
alert(loginResult.val.map(e => e.message).join('\n'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ async function handleSubmit(e: SubmitEvent) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<form v-on:submit.prevent="handleSubmit">
|
<form @submit.prevent="handleSubmit">
|
||||||
<div>
|
<div>
|
||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
<input type="text" name="username" id="username" />
|
<input type="text" name="username" id="username" />
|
||||||
|
|||||||
Reference in New Issue
Block a user