diff options
| author | ivar <i@oiee.no> | 2025-12-11 00:44:59 +0100 |
|---|---|---|
| committer | ivar <i@oiee.no> | 2025-12-11 00:44:59 +0100 |
| commit | 008862f8a2431c8f755a38a0ef242b8faf125057 (patch) | |
| tree | 64087987855e95940110b3c65fc877921574dc8f /app/src/lib/server/sb1.ts | |
| parent | 3dfc7b11ca5b243c63c90bb3c2fafeb8e14dc7f0 (diff) | |
| download | sparebank1-actualbudget-008862f8a2431c8f755a38a0ef242b8faf125057.tar.xz sparebank1-actualbudget-008862f8a2431c8f755a38a0ef242b8faf125057.zip | |
WIP! Restructure
Diffstat (limited to 'app/src/lib/server/sb1.ts')
| -rw-r--r-- | app/src/lib/server/sb1.ts | 144 |
1 files changed, 144 insertions, 0 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 |
