-
+
+
+
+
+
diff --git a/src/FiscalOS.Web/src/components/icons/RightArrowIcon.vue b/src/FiscalOS.Web/src/components/icons/RightArrowIcon.vue
new file mode 100644
index 0000000..dc7b5fa
--- /dev/null
+++ b/src/FiscalOS.Web/src/components/icons/RightArrowIcon.vue
@@ -0,0 +1,6 @@
+
+
+
diff --git a/src/FiscalOS.Web/src/composables/useInstitutionService.ts b/src/FiscalOS.Web/src/composables/useInstitutionService.ts
new file mode 100644
index 0000000..081138c
--- /dev/null
+++ b/src/FiscalOS.Web/src/composables/useInstitutionService.ts
@@ -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;
+}
diff --git a/src/FiscalOS.Web/src/main.ts b/src/FiscalOS.Web/src/main.ts
index b55ed9b..487d805 100644
--- a/src/FiscalOS.Web/src/main.ts
+++ b/src/FiscalOS.Web/src/main.ts
@@ -7,11 +7,13 @@ import App from "./App.vue";
import router from "./router";
import { ClientFactory, ClientFactoryKey } from "./services/client";
import { AuthServiceFactory, AuthServiceFactoryKey } from "./services/authService";
+import { InstituionServiceFactoryKey, InstitutionServiceFactory } from './services/institutionService';
const app = createApp(App);
app.provide(ClientFactoryKey, new ClientFactory());
app.provide(AuthServiceFactoryKey, new AuthServiceFactory());
+app.provide(InstituionServiceFactoryKey, new InstitutionServiceFactory());
app.use(createPinia());
app.use(router);
diff --git a/src/FiscalOS.Web/src/router/index.ts b/src/FiscalOS.Web/src/router/index.ts
index f306038..cb10e76 100644
--- a/src/FiscalOS.Web/src/router/index.ts
+++ b/src/FiscalOS.Web/src/router/index.ts
@@ -1,7 +1,7 @@
-import { AuthService } from "@/services/authService";
-import { Client, ClientConfig } from "@/services/client";
-import { useUserStore } from "@/stores/userStore";
-import { createRouter, createWebHistory } from "vue-router";
+import { AuthService } from '@/services/authService';
+import { Client, ClientConfig } from '@/services/client';
+import { useUserStore } from '@/stores/userStore';
+import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -22,24 +22,25 @@ const router = createRouter({
children: [
{
path: 'login',
- component: () => import('../views/LoginView.vue')
- }
+ component: () => import('../views/LoginView.vue'),
+ },
],
},
{
path: '/',
+ component: () => import('../components/ProtectedLayout.vue'),
beforeEnter: async () => {
const userStore = useUserStore();
if (userStore.user === null) {
- return { path: '/public/login' }
+ return { path: '/public/login' };
}
const isExpired = userStore.user.expiresAtInSeconds < Date.now() / 1000;
if (isExpired) {
const clientConfig = new ClientConfig(
- { Authorization: `Bearer ${userStore.user.token}`},
+ { Authorization: `Bearer ${userStore.user.token}` },
true
);
const client = new Client(clientConfig);
@@ -48,7 +49,7 @@ const router = createRouter({
if (refreshResult.err) {
userStore.logUserOut();
- return { path: '/public/login' }
+ return { path: '/public/login' };
}
userStore.logUserIn(refreshResult.val.accessToken);
@@ -56,8 +57,13 @@ const router = createRouter({
return true;
},
- component: () => import('../views/HomeView.vue'),
- }
+ children: [
+ {
+ path: '/',
+ component: () => import('../views/HomeView.vue'),
+ },
+ ],
+ },
],
});
diff --git a/src/FiscalOS.Web/src/services/institutionService.ts b/src/FiscalOS.Web/src/services/institutionService.ts
new file mode 100644
index 0000000..f898567
--- /dev/null
+++ b/src/FiscalOS.Web/src/services/institutionService.ts
@@ -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
;
+
+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>;
+ connect: (publicToken: string, plaidInstitutionId: string) => Promise>;
+}
+
+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;
+ 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;
+};
diff --git a/src/FiscalOS.Web/src/views/HomeView.vue b/src/FiscalOS.Web/src/views/HomeView.vue
index 07779bc..23a86c6 100644
--- a/src/FiscalOS.Web/src/views/HomeView.vue
+++ b/src/FiscalOS.Web/src/views/HomeView.vue
@@ -1,9 +1,35 @@
Home View
-
-
+
diff --git a/src/FiscalOS.Web/src/views/LoginView.vue b/src/FiscalOS.Web/src/views/LoginView.vue
index 46e3c18..9b554ed 100644
--- a/src/FiscalOS.Web/src/views/LoginView.vue
+++ b/src/FiscalOS.Web/src/views/LoginView.vue
@@ -15,7 +15,7 @@ async function handleSubmit(e: SubmitEvent) {
);
if (loginResult.err) {
- alert(loginResult.val.join('\n'));
+ alert(loginResult.val.map(e => e.message).join('\n'));
return;
}
@@ -25,7 +25,7 @@ async function handleSubmit(e: SubmitEvent) {
-