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 * as v from "valibot" import { command, query } from "$app/server"; import { eq } from "drizzle-orm"; import { Temporal } from "temporal-polyfill"; export const createSb1SyncSessionAndReturnLoginUrl = command(async () => { return createSb1Auth() }) 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 } } else console.error(await response.text()) }) 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; }) 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 } export const clearTokens = 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)) }, } }) 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 { 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 { 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; } export type TransactionsResponse = { transactions: Array; }