aboutsummaryrefslogtreecommitdiffstats
path: root/app/src
diff options
context:
space:
mode:
Diffstat (limited to 'app/src')
-rw-r--r--app/src/lib/server/sb1.ts144
-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
4 files changed, 201 insertions, 213 deletions
diff --git a/app/src/lib/server/sb1.ts b/app/src/lib/server/sb1.ts
new file mode 100644
index 0000000..c060af5
--- /dev/null
+++ b/app/src/lib/server/sb1.ts
@@ -0,0 +1,144 @@
+import { SB1_FIN_INST, SB1_ID, SB1_REDIRECT_URI, SB1_SECRET } from "$env/static/private";
+import { eq } from "drizzle-orm";
+import { Temporal } from "temporal-polyfill";
+import { randomUUID } from "node:crypto";
+import { db } from "./db";
+import { syncSession } from "./db/schema";
+
+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 Transaction = {
+ description: string
+ amount: number
+ date: string
+ mcc: string
+}
+
+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
+ },
+ async get_auth_info() {
+ 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 ?? "")
+ 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 })
+ return {
+ refreshTokenExpires,
+ accessTokenExpires
+ }
+ },
+ async init_auth_session(): Promise<string> {
+ 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()
+ },
+ 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
+ return parsed.access_token as string
+ },
+ async refresh_tokem() {
+ 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) as Sb1Tokens
+
+ if (!parsed.refresh_token) throw new Error("No refresh token");
+
+ const params = new URLSearchParams();
+
+ params.set("client_id", SB1_ID);
+ params.set("client_secret", SB1_SECRET);
+ params.set("refresh_token", parsed.refresh_token);
+ params.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: params,
+ });
+
+ 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))
+ }
+}
+
+const data = {
+ async get_accounts() {
+ const token = await auth.get_access_token()
+ 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())
+ },
+ 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",
+ );
+ url.searchParams.set("accountKey", accountKey);
+ const response = await fetch(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+ return (await response.json())["transactions"] as Transaction[];
+
+ }
+}
+
+export default { auth, data } \ No newline at end of file
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