diff options
| author | ivar <i@oiee.no> | 2025-12-15 00:40:21 +0100 |
|---|---|---|
| committer | ivar <i@oiee.no> | 2025-12-15 00:40:21 +0100 |
| commit | 2b5e4c4aa372632b87a404027cf53d5a7bfc3808 (patch) | |
| tree | 4782fec17e6c3745fb43e209dfe80e33340d37c0 /app | |
| parent | 008862f8a2431c8f755a38a0ef242b8faf125057 (diff) | |
| download | sparebank1-actualbudget-2b5e4c4aa372632b87a404027cf53d5a7bfc3808.tar.xz sparebank1-actualbudget-2b5e4c4aa372632b87a404027cf53d5a7bfc3808.zip | |
Progress
Diffstat (limited to 'app')
| -rw-r--r-- | app/bun.lock | 7 | ||||
| -rw-r--r-- | app/bunfig.toml | 2 | ||||
| -rw-r--r-- | app/package.json | 5 | ||||
| -rw-r--r-- | app/src/lib/server/actual.ts | 21 | ||||
| -rw-r--r-- | app/src/lib/server/sb1.ts | 31 | ||||
| -rw-r--r-- | app/src/lib/ui/button.svelte | 1 | ||||
| -rw-r--r-- | app/src/routes/+page.server.ts | 14 | ||||
| -rw-r--r-- | app/src/routes/+page.svelte | 41 | ||||
| -rw-r--r-- | app/src/routes/actual.remote.ts | 4 | ||||
| -rw-r--r-- | app/src/routes/sb1.remote.ts | 16 |
10 files changed, 93 insertions, 49 deletions
diff --git a/app/bun.lock b/app/bun.lock index a71dfd7..cceac74 100644 --- a/app/bun.lock +++ b/app/bun.lock @@ -4,6 +4,7 @@ "": { "name": "app", "dependencies": { + "@ivars/ueb": "latest", "better-sqlite3": "latest", }, "devDependencies": { @@ -89,6 +90,8 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "@ivars/ueb": ["@ivars/ueb@0.1.0", "https://npm.ivar.systems/@ivars/ueb/-/ueb-0.1.0.tgz", { "peerDependencies": { "typescript": "^5" } }, "sha512-4m9rRl4HgjmghCxu++1udLbpD/bkWnRlrbJ4mWx7NnT/kWDdARcX10+IBcPn2yLue3LQomAMw17dMufCPll0qA=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -171,7 +174,7 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "@types/node": ["@types/node@25.0.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-rl78HwuZlaDIUSeUKkmogkhebA+8K1Hy7tddZuJ3D0xV8pZSfsYGTsliGUol1JPzu9EKnTxPC4L1fiWouStRew=="], + "@types/node": ["@types/node@25.0.2", "https://npm.ivar.systems/@types/node/-/node-25.0.2.tgz", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA=="], "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], @@ -379,7 +382,7 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "svelte": ["svelte@5.45.8", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.5.0", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-1Jh7FwVh/2Uxg0T7SeE1qFKMhwYH45b2v53bcZpW7qHa6O8iU1ByEj56PF0IQ6dU4HE5gRkic6h+vx+tclHeiw=="], + "svelte": ["svelte@5.46.0", "https://npm.ivar.systems/svelte/-/svelte-5.46.0.tgz", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.5.0", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-ZhLtvroYxUxr+HQJfMZEDRsGsmU46x12RvAv/zi9584f5KOX7bUrEbhPJ7cKFmUvZTJXi/CFZUYwDC6M1FigPw=="], "svelte-check": ["svelte-check@4.3.4", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-DVWvxhBrDsd+0hHWKfjP99lsSXASeOhHJYyuKOFYJcP7ThfSCKgjVarE8XfuMWpS5JV3AlDf+iK1YGGo2TACdw=="], diff --git a/app/bunfig.toml b/app/bunfig.toml new file mode 100644 index 0000000..7243f16 --- /dev/null +++ b/app/bunfig.toml @@ -0,0 +1,2 @@ +[install.scopes] +"@ivars" = "https://npm.ivar.systems/" diff --git a/app/package.json b/app/package.json index 3def5e4..2cbd931 100644 --- a/app/package.json +++ b/app/package.json @@ -21,12 +21,12 @@ "@sveltejs/kit": "^2.49.2", "@sveltejs/vite-plugin-svelte": "^6.2.1", "@types/better-sqlite3": "^7.6.13", - "@types/node": "^25.0.0", + "@types/node": "^25.0.2", "drizzle-kit": "^0.31.8", "drizzle-orm": "^0.45.1", "lightningcss": "^1.30.2", "phosphor-svelte": "^3.0.1", - "svelte": "^5.45.8", + "svelte": "^5.46.0", "svelte-check": "^4.3.4", "temporal-polyfill": "^0.3.0", "typescript": "^5.9.3", @@ -34,6 +34,7 @@ "vite": "^7.2.7" }, "dependencies": { + "@ivars/ueb": "^0.1.0", "better-sqlite3": "^12.5.0" } }
\ No newline at end of file diff --git a/app/src/lib/server/actual.ts b/app/src/lib/server/actual.ts new file mode 100644 index 0000000..7291aad --- /dev/null +++ b/app/src/lib/server/actual.ts @@ -0,0 +1,21 @@ +import { ACTUAL_HOST, ACTUAL_PASS } from "$env/static/private"; +import * as actual from "@actual-app/api" +import { existsSync, mkdirSync } from "node:fs"; +import path from "node:path"; + +async function init_actual() { + const dataDir = path.resolve(import.meta.dirname, "actualDataDir"); + + if (!existsSync(dataDir)) mkdirSync(dataDir); + + return actual.init({ + password: ACTUAL_PASS, + serverURL: ACTUAL_HOST, + dataDir: dataDir + }) +} + +export async function get_budgets() { + await init_actual() + return await actual.getBudgets() +}
\ No newline at end of file diff --git a/app/src/lib/server/sb1.ts b/app/src/lib/server/sb1.ts index c060af5..1d6b869 100644 --- a/app/src/lib/server/sb1.ts +++ b/app/src/lib/server/sb1.ts @@ -24,12 +24,7 @@ type Transaction = { const auth = { async is_ready(): Promise<boolean> { const token = await this.get_access_token() - const ping = await fetch("https://developer.sparebank1.no/helloworld/ping", { - headers: { - "Authorization": "Bearer " + token - } - }) - return ping.ok + return token !== "" }, async get_auth_info() { const entity = await db.select({ @@ -39,7 +34,8 @@ const auth = { }).from(syncSession) if (!entity[0]) return undefined const { tokens, accessTokenCreated, refreshTokenCreated } = entity[0] - const tokensParsed = JSON.parse(tokens ?? "") + if (!tokens) return undefined + const tokensParsed = JSON.parse(tokens) if (!tokensParsed) return undefined const refreshTokenExpires = Temporal.Instant.fromEpochMilliseconds(Number(refreshTokenCreated)).add({ seconds: tokensParsed?.refresh_token_expires_in }) const accessTokenExpires = Temporal.Instant.fromEpochMilliseconds(Number(accessTokenCreated)).add({ seconds: tokensParsed?.expires_in }) @@ -56,22 +52,20 @@ const auth = { }) 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() }, async get_access_token() { const entity = await db.select({ tokens: syncSession.tokens }).from(syncSession) - const { tokens } = entity[0] - if (!tokens) return null - const parsed = JSON.parse(tokens) as Sb1Tokens + const res = entity[0] + if (!res?.tokens) return null + const parsed = JSON.parse(res.tokens) as Sb1Tokens return parsed.access_token as string }, async refresh_tokem() { @@ -127,15 +121,20 @@ const data = { async get_transactions(accountKey: string) { const token = await auth.get_access_token() if (token) return undefined - const url = new URL( - "https://api.sparebank1.no/personal/banking/transactions", - ); + const url = new URL("https://api.sparebank1.no/personal/banking/transactions/transactions") + console.log(token) url.searchParams.set("accountKey", accountKey); - const response = await fetch(url, { + console.log(accountKey) + const response = await fetch("https://api.sparebank1.no/personal/banking/transactions" + new URLSearchParams({ + "accountKey": accountKey, + "fromDate": "", + "toDate": "" + }), { headers: { Authorization: `Bearer ${token}`, }, }); + const json = await response.json() return (await response.json())["transactions"] as Transaction[]; } diff --git a/app/src/lib/ui/button.svelte b/app/src/lib/ui/button.svelte index 313cd90..8c66bd2 100644 --- a/app/src/lib/ui/button.svelte +++ b/app/src/lib/ui/button.svelte @@ -26,6 +26,7 @@ display: flex; gap: 3px; transition: 0.1s all ease; + height: fit-content; &:hover, &:focus { diff --git a/app/src/routes/+page.server.ts b/app/src/routes/+page.server.ts new file mode 100644 index 0000000..0d6016b --- /dev/null +++ b/app/src/routes/+page.server.ts @@ -0,0 +1,14 @@ +import type { PageServerLoad } from './$types'; +import { get_budgets } from '$lib/server/actual'; +import sb1 from "$lib/server/sb1" + +export const load = (async () => { + return { + actual: { + meta: await get_budgets() + }, + sb1: { + accounts: (await sb1.data.get_accounts())?.accounts + } + }; +}) satisfies PageServerLoad;
\ No newline at end of file diff --git a/app/src/routes/+page.svelte b/app/src/routes/+page.svelte index e89df9b..26db3e4 100644 --- a/app/src/routes/+page.svelte +++ b/app/src/routes/+page.svelte @@ -1,10 +1,10 @@ <script lang="ts"> import Button from "$lib/ui/button.svelte"; - 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"; + import { clear_auth_session, get_transactions, init_auth_session } from "./sb1.remote"; + import type { PageProps } from "./$types"; let navigating = $state(false); + let { data }: PageProps = $props(); async function authorize() { navigating = true; @@ -16,23 +16,16 @@ async function logout() { await clear_auth_session(); } - - onMount(async () => { - await get_actual_meta(); - }); </script> <main> - {#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)} + {#if data.sb1.accounts?.length} + <ul> + {#each data.sb1.accounts as account} <li>{account.name}</li> - {#if transactions?.length} + {#if (await get_transactions(account.key))?.length} <ul> - {#each transactions as transaction} + {#each await get_transactions(account.key) as transaction} <li>{JSON.stringify(transaction)}</li> {/each} </ul> @@ -40,12 +33,22 @@ <small>Ingen transaksjoner</small> {/if} {/each} - {/if} - {#if actual_meta} - <pre>{JSON.stringify(actual_meta, null, 2)}</pre> - {/if} + </ul> <Button onclick={logout}>Logg ut</Button> {:else} <Button onclick={authorize} loading={navigating}>Autentisér hos Sparebanken 1</Button> {/if} + + {#if data.actual.meta} + <pre>{JSON.stringify(data.actual.meta, null, 2)}</pre> + {/if} </main> + +<style> + main { + display: flex; + justify-content: center; + width: 100%; + height: 90vh; + } +</style> diff --git a/app/src/routes/actual.remote.ts b/app/src/routes/actual.remote.ts index 4bd70b4..9560d4d 100644 --- a/app/src/routes/actual.remote.ts +++ b/app/src/routes/actual.remote.ts @@ -5,7 +5,7 @@ import { existsSync, mkdirSync } from "node:fs"; import path from "node:path"; async function init_actual() { - const dataDir = path.resolve(__dirname, "actualDataDir"); + const dataDir = path.resolve(import.meta.dirname, "actualDataDir"); if (!existsSync(dataDir)) mkdirSync(dataDir); @@ -19,4 +19,4 @@ async function init_actual() { 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 d2eb0cc..c3967c1 100644 --- a/app/src/routes/sb1.remote.ts +++ b/app/src/routes/sb1.remote.ts @@ -8,24 +8,24 @@ const init_auth_session = command(async () => { return await sb1.auth.init_auth_session() }) -const is_ready = query(async () => { - return await sb1.auth.is_ready() +const is_ready = query(() => { + return sb1.auth.is_ready() }) -const get_accounts = query(async () => { - return await sb1.data.get_accounts() +const get_accounts = query(() => { + return sb1.data.get_accounts() }) -const get_transactions = query(v.string(), async (accountKey: string) => { - return await sb1.data.get_transactions(accountKey) +const get_transactions = query(v.string(), (accountKey: string) => { + return sb1.data.get_transactions(accountKey) }) const clear_auth_session = query(async () => { await db.delete(syncSession) }) -const get_auth_info = query(async () => { - return await sb1.auth.get_auth_info() +const get_auth_info = query(() => { + return sb1.auth.get_auth_info() }) const refresh_tokem = command(async () => { |
