diff options
Diffstat (limited to 'app/src/lib/server/sb1.ts')
| -rw-r--r-- | app/src/lib/server/sb1.ts | 207 |
1 files changed, 0 insertions, 207 deletions
diff --git a/app/src/lib/server/sb1.ts b/app/src/lib/server/sb1.ts deleted file mode 100644 index 0a51649..0000000 --- a/app/src/lib/server/sb1.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { SB1_FIN_INST, SB1_ID, SB1_REDIRECT_URI, SB1_SECRET } from "$env/static/private"; -import { eq, sql } from "drizzle-orm"; -import { Temporal } from "temporal-polyfill"; -import { randomUUID } from "node:crypto"; -import { db } from "./db"; -import { SyncSessionTable, TransactionsTable } from "./db/schema"; -import { add_session_log } from "./session-log"; -import type { Sb1Account, Sb1Tokens, Sb1Transaction } from "$lib/shared"; - -const auth = { - async is_ready(): Promise<boolean> { - const token = await this.get_access_token() - return token !== "" - }, - async get_auth_info() { - const entity = await db.select({ - refreshTokenCreated: SyncSessionTable.refreshTokenCreated, - accessTokenCreated: SyncSessionTable.accessTokenCreated, - tokens: SyncSessionTable.tokens - }).from(SyncSessionTable) - if (!entity[0]) return undefined - const { tokens, accessTokenCreated, refreshTokenCreated } = entity[0] - if (!tokens) return undefined - const refreshTokenExpires = Temporal.Instant.fromEpochMilliseconds(Number(refreshTokenCreated)).add({ seconds: tokens.refresh_token_expires_in }) - const accessTokenExpires = Temporal.Instant.fromEpochMilliseconds(Number(accessTokenCreated)).add({ seconds: tokens.expires_in }) - return { - refreshTokenExpires, - accessTokenExpires - } - }, - async init_auth_session(): Promise<string> { - const state = randomUUID() - - await db.insert(SyncSessionTable).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 result = await db.select({ - tokens: SyncSessionTable.tokens, - refreshTokenCreated: SyncSessionTable.refreshTokenCreated, - accessTokenCreated: SyncSessionTable.accessTokenCreated - }).from(SyncSessionTable) - - if (!result[0]) return undefined - - const { tokens, accessTokenCreated, refreshTokenCreated } = result[0] - - if (!tokens) return undefined - - const nowInstant = Temporal.Now.instant() - - const accessTokenExpiredInstant = Temporal.Instant.fromEpochMilliseconds(Number(accessTokenCreated)).add({ seconds: tokens.expires_in }) - if (Temporal.Instant.compare(nowInstant, accessTokenExpiredInstant) >= 0) { - const refreshedTokens = await this.refresh_token() - if (refreshedTokens) return refreshedTokens.access_token - } - - const refreshTokenExpiredInstant = Temporal.Instant.fromEpochMilliseconds(Number(refreshTokenCreated)).add({ seconds: tokens.refresh_token_expires_in }) - if (Temporal.Instant.compare(nowInstant, refreshTokenExpiredInstant) >= 0) { - return undefined - } - - return tokens?.access_token as string - }, - async refresh_token(): Promise<Sb1Tokens | null> { - console.log("Refreshing tokens") - const entity = await db.select({ - tokens: SyncSessionTable.tokens, - id: SyncSessionTable.id - }).from(SyncSessionTable) - - const { tokens: currentTokens, id } = entity[0] - - if (!currentTokens) return null - if (!currentTokens.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", currentTokens.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 tokens = await res.json() as Sb1Tokens - const epoch = Temporal.Now.instant().epochMilliseconds - if (res.ok) { - await db.update(SyncSessionTable).set({ tokens, accessTokenCreated: epoch.toString(), refreshTokenCreated: epoch.toString() }).where(eq(SyncSessionTable.id, id)) - await add_session_log(id, "REFRESH_SB1_TOKEN", "Done") - return tokens - } else { - console.error("Failed to refresh tokens", tokens) - await add_session_log(id, "REFRESH_SB1_TOKEN", "Failed: " + JSON.stringify(res)) - return null - } - } -} - -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<Sb1Account> } - else console.error(await response.text()) - }, - async get_transactions(accountKey: string, delta?: Temporal.Instant) { - const token = await auth.get_access_token() - if (!token) return undefined - - const params = new URLSearchParams({ - "accountKey": accountKey, - }); - - if (delta) { - params.append("fromDate", formatInstant(delta, "yyyy-MM-dd")) - params.append("Transaction source", "ALL") - } - - const response = await fetch("https://api.sparebank1.no/personal/banking/transactions?" + params, { - headers: { - Authorization: `Bearer ${token}`, - Accept: "application/vnd.sparebank1.v1+json;charset=utf-8" - }, - }); - - const json = await response.json() - return json["transactions"] as Sb1Transaction[] - } -} - -export default { auth, data, init } - -let importInterval: NodeJS.Timeout -let inited = false - -async function init() { - if (inited) return - if (importInterval) clearInterval(importInterval) - await importTransactions() - importInterval = setInterval(async () => importTransactions, 60 * 60 * 1000) - inited = true -} - -async function importTransactions() { - console.log("Creating sb1 transactions indb") - const accounts = await data.get_accounts() - for (const account of accounts?.accounts ?? []) { - const transactions = await data.get_transactions(account.key) - for (const transaction of transactions ?? []) { - // if (await transactionExists(transaction.id)) continue - await db.insert(TransactionsTable).values({ transaction }) - } - } -} - -async function transactionExists(transactionId: string) { - const query = sql`select data ->>'id' as id from ${TransactionsTable} where id=${transactionId}`; - return (await db.execute(query)).rowCount ?? 0 > 0 -} - -function formatInstant( - instant: Temporal.Instant, - format: string, - timeZone: string = "UTC" -): string { - const zdt = instant.toZonedDateTimeISO(timeZone); - - const pad = (value: number, length = 2) => - value.toString().padStart(length, "0"); - - const replacements: Record<string, string> = { - yyyy: pad(zdt.year, 4), - MM: pad(zdt.month), - dd: pad(zdt.day), - HH: pad(zdt.hour), - mm: pad(zdt.minute), - ss: pad(zdt.second), - }; - - return format.replace( - /yyyy|MM|dd|HH|mm|ss/g, - (token) => replacements[token] - ); -}
\ No newline at end of file |
