aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/routes
diff options
context:
space:
mode:
authorivar <i@oiee.no>2025-12-11 00:44:59 +0100
committerivar <i@oiee.no>2025-12-11 00:44:59 +0100
commit008862f8a2431c8f755a38a0ef242b8faf125057 (patch)
tree64087987855e95940110b3c65fc877921574dc8f /app/src/routes
parent3dfc7b11ca5b243c63c90bb3c2fafeb8e14dc7f0 (diff)
downloadsparebank1-actualbudget-008862f8a2431c8f755a38a0ef242b8faf125057.tar.xz
sparebank1-actualbudget-008862f8a2431c8f755a38a0ef242b8faf125057.zip
WIP! Restructure
Diffstat (limited to 'app/src/routes')
-rw-r--r--app/src/routes/+page.svelte69
-rw-r--r--app/src/routes/actual.remote.ts9
-rw-r--r--app/src/routes/sb1.remote.ts192
3 files changed, 57 insertions, 213 deletions
diff --git a/app/src/routes/+page.svelte b/app/src/routes/+page.svelte
index 005eb34..e89df9b 100644
--- a/app/src/routes/+page.svelte
+++ b/app/src/routes/+page.svelte
@@ -1,68 +1,51 @@
<script lang="ts">
import Button from "$lib/ui/button.svelte";
- import {
- clearTokens,
- createSb1SyncSessionAndReturnLoginUrl,
- getAccounts,
- getTokenExpires,
- getTransactions,
- refreshSB1Token,
- } from "./sb1.remote";
+ import { onMount } from "svelte";
+ import { clear_auth_session, get_accounts, get_transactions, init_auth_session, is_ready } from "./sb1.remote";
+ import { get_actual_meta } from "./actual.remote";
let navigating = $state(false);
async function authorize() {
navigating = true;
- const url = await createSb1SyncSessionAndReturnLoginUrl();
+ const url = await init_auth_session();
location.href = url;
navigating = false;
}
- async function clearAuth() {
- await clearTokens();
- getTokenExpires().refresh();
+ async function logout() {
+ await clear_auth_session();
}
- async function refreshAuth() {
- await refreshSB1Token();
- getTokenExpires().refresh();
- }
-
- async function initActual() {
- await initActual()
- }
+ onMount(async () => {
+ await get_actual_meta();
+ });
</script>
<main>
- {#if await getTokenExpires()}
- {@const tokens = await getTokenExpires()}
- {@const accounts = await getAccounts()}
- {#if tokens}
- <pre>accessToken: {tokens.accessToken.created.add({ seconds: tokens.accessToken.expires }).toLocaleString()}
-refreshToken: {tokens.refreshToken.created.add({ seconds: tokens.refreshToken.expires }).toLocaleString()}</pre>
- <ul>
- {#each accounts?.accounts as account}
- {@const transactions = await getTransactions(account.key)}
- <li>{account.name}</li>
+ {#if await is_ready()}
+ {@const accounts = await get_accounts()}
+ {@const actual_meta = await get_actual_meta()}
+ {#if accounts}
+ {#each accounts?.accounts as account}
+ {@const transactions = await get_transactions(account.key)}
+ <li>{account.name}</li>
+ {#if transactions?.length}
<ul>
- {#each transactions?.transactions as transaction}
+ {#each transactions as transaction}
<li>{JSON.stringify(transaction)}</li>
{/each}
</ul>
- {/each}
- </ul>
- <Button onclick={clearAuth}>Slett autorisasjon</Button>
- <Button onclick={refreshAuth}>Oppdater autorisasjon</Button>
- <Button></Button>
+ {:else}
+ <small>Ingen transaksjoner</small>
+ {/if}
+ {/each}
{/if}
+ {#if actual_meta}
+ <pre>{JSON.stringify(actual_meta, null, 2)}</pre>
+ {/if}
+ <Button onclick={logout}>Logg ut</Button>
{:else}
<Button onclick={authorize} loading={navigating}>Autentisér hos Sparebanken 1</Button>
{/if}
</main>
-
-<style>
- pre {
- max-width: 50vw;
- overflow: auto;
- }
-</style>
diff --git a/app/src/routes/actual.remote.ts b/app/src/routes/actual.remote.ts
index 535e387..4bd70b4 100644
--- a/app/src/routes/actual.remote.ts
+++ b/app/src/routes/actual.remote.ts
@@ -4,7 +4,7 @@ import * as actual from "@actual-app/api"
import { existsSync, mkdirSync } from "node:fs";
import path from "node:path";
-async function initActual() {
+async function init_actual() {
const dataDir = path.resolve(__dirname, "actualDataDir");
if (!existsSync(dataDir)) mkdirSync(dataDir);
@@ -16,8 +16,7 @@ async function initActual() {
})
}
-export const getActualMeta = query(async () => {
- await initActual()
- const accounts = await actual.getAccounts()
- return
+export const get_actual_meta = query(async () => {
+ await init_actual()
+ return await actual.getBudgets()
}) \ No newline at end of file
diff --git a/app/src/routes/sb1.remote.ts b/app/src/routes/sb1.remote.ts
index 17e1ead..d2eb0cc 100644
--- a/app/src/routes/sb1.remote.ts
+++ b/app/src/routes/sb1.remote.ts
@@ -1,181 +1,43 @@
-import { SB1_FIN_INST, SB1_ID, SB1_REDIRECT_URI, SB1_SECRET } from "$env/static/private";
-import { randomUUID } from "node:crypto";
-import { db } from "../lib/server/db";
-import { syncSession } from "../lib/server/db/schema";
+import { db } from "$lib/server/db";
+import { syncSession } from "$lib/server/db/schema";
import * as v from "valibot"
import { command, query } from "$app/server";
-import { eq } from "drizzle-orm";
-import { Temporal } from "temporal-polyfill";
+import sb1 from "$lib/server/sb1";
-export const createSb1SyncSessionAndReturnLoginUrl = command(async () => {
- return createSb1Auth()
+const init_auth_session = command(async () => {
+ return await sb1.auth.init_auth_session()
})
-async function createSb1Auth() {
- const state = randomUUID()
-
- await db.insert(syncSession).values({
- authzState: state
- })
-
- const authorizeUrl = new URL("https://api.sparebank1.no/oauth/authorize");
-
- authorizeUrl.searchParams.set("client_id", SB1_ID);
- authorizeUrl.searchParams.set("state", state);
- authorizeUrl.searchParams.set("redirect_uri", SB1_REDIRECT_URI);
- authorizeUrl.searchParams.set("finInst", SB1_FIN_INST);
- authorizeUrl.searchParams.set("response_type", "code");
-
- return authorizeUrl.toString()
-}
-
-export const getAccounts = query(async () => {
- const token = await getSb1AccessToken()
- if (!token) return undefined
- const url = new URL(
- "https://api.sparebank1.no/personal/banking/accounts",
- );
- const response = await fetch(url, {
- headers: {
- Authorization: `Bearer ${token}`
- },
- });
- if (response.ok) {
- return await response.json() as { accounts: Array<any> }
- }
- else console.error(await response.text())
+const is_ready = query(async () => {
+ return await sb1.auth.is_ready()
})
-export const getTransactions = query(v.string(), async (accountKey: string) => {
- const token = await getSb1AccessToken()
- if (token) return undefined
- const url = new URL(
- "https://api.sparebank1.no/personal/banking/transactions",
- );
- url.searchParams.set("accountKey", accountKey);
- const response = await fetch(url, {
- headers: {
- Authorization: `Bearer ${token}`,
- },
- });
- return (await response.json()) as TransactionsResponse;
+const get_accounts = query(async () => {
+ return await sb1.data.get_accounts()
})
-async function getSb1AccessToken() {
- const entity = await db.select({
- tokens: syncSession.tokens
- }).from(syncSession)
- const { tokens } = entity[0]
- if (!tokens) return null
- const parsed = JSON.parse(tokens)
- return parsed.access_token as string
-}
+const get_transactions = query(v.string(), async (accountKey: string) => {
+ return await sb1.data.get_transactions(accountKey)
+})
-export const clearTokens = query(async () => {
+const clear_auth_session = query(async () => {
await db.delete(syncSession)
})
-export const getTokenExpires = query(async () => {
- const entity = await db.select({
- refreshTokenCreated: syncSession.refreshTokenCreated,
- accessTokenCreated: syncSession.accessTokenCreated,
- tokens: syncSession.tokens
- }).from(syncSession)
- if (!entity[0]) return undefined
- const { tokens, accessTokenCreated, refreshTokenCreated } = entity[0]
- const tokensParsed = JSON.parse(tokens ?? "")
- return {
- accessToken: {
- expires: tokensParsed?.expires_in ?? 0,
- created: Temporal.Instant.fromEpochMilliseconds(Number(accessTokenCreated))
- },
- refreshToken: {
- expires: tokensParsed?.refresh_token_expires_in ?? 0,
- created: Temporal.Instant.fromEpochMilliseconds(Number(refreshTokenCreated))
- },
- }
+const get_auth_info = query(async () => {
+ return await sb1.auth.get_auth_info()
})
-type Sb1Tokens = {
- access_token: string,
- expires_in: number,
- refresh_token_expires_in: number,
- refresh_token_absolute_expires_in: number,
- token_type: string,
- refresh_token: string
-}
-
-type TokenAction = "Empty"
-
-const auth = {
- async ready(): Promise<boolean> {
- const token = await this.tokenOrAction()
- const ping = await fetch("https://developer.sparebank1.no/helloworld/ping", {
- headers: {
- "Authorization": "Bearer " + token
- }
- })
- return ping.ok
- },
- async tokenOrAction(): Promise<TokenAction | string> {
- const entity = await db.select({
- refreshTokenCreated: syncSession.refreshTokenCreated,
- accessTokenCreated: syncSession.accessTokenCreated,
- tokens: syncSession.tokens
- }).from(syncSession)
- const { tokens, accessTokenCreated, refreshTokenCreated } = entity[0]
- if (!tokens) return "Empty"
- const json = JSON.parse(tokens) as Sb1Tokens
- if (!Object.hasOwn(json, "access_token")) return TokenAction.Empty
- return json.access_token
- },
- getAccessToken() { },
- async getRefreshToken() {
- const entity = await db.select({
- tokens: syncSession.tokens,
- id: syncSession.id
- }).from(syncSession)
-
- const { tokens, id } = entity[0]
-
- if (!tokens) return null
-
- const parsed = JSON.parse(tokens)
-
- if (!parsed.refresh_token) throw new Error("No refresh token");
-
- const fd = new URLSearchParams();
-
- fd.set("client_id", SB1_ID);
- fd.set("client_secret", SB1_SECRET);
- fd.set("refresh_token", parsed.refresh_token);
- fd.set("grant_type", "refresh_token");
-
- const res = await fetch("https://api.sparebank1.no/oauth/token", {
- headers: {
- "Content-Type": "application/x-www-form-urlencoded",
- },
- method: "post",
- body: fd,
- });
-
- const text = await res.text();
- const epoch = Temporal.Now.instant().epochMilliseconds
- await db.update(syncSession).set({ tokens: text, accessTokenCreated: epoch, refreshTokenCreated: epoch }).where(eq(syncSession.id, id))
- }
-}
-
-export const refreshSB1Token = command(async () => {
- auth.getRefreshToken()
-});
-
-export type Transaction = {
- description: string;
- amount: number;
- date: string;
- mcc: string;
-}
+const refresh_tokem = command(async () => {
+ await sb1.auth.refresh_tokem()
+})
-export type TransactionsResponse = {
- transactions: Array<Transaction>;
-}
+export {
+ refresh_tokem,
+ init_auth_session,
+ is_ready,
+ get_accounts,
+ get_transactions,
+ clear_auth_session,
+ get_auth_info,
+} \ No newline at end of file