diff options
Diffstat (limited to 'app/src/lib/server/sb1.ts')
| -rw-r--r-- | app/src/lib/server/sb1.ts | 158 |
1 files changed, 63 insertions, 95 deletions
diff --git a/app/src/lib/server/sb1.ts b/app/src/lib/server/sb1.ts index f6507ef..0a51649 100644 --- a/app/src/lib/server/sb1.ts +++ b/app/src/lib/server/sb1.ts @@ -1,84 +1,11 @@ import { SB1_FIN_INST, SB1_ID, SB1_REDIRECT_URI, SB1_SECRET } from "$env/static/private"; -import { eq } from "drizzle-orm"; +import { eq, sql } from "drizzle-orm"; import { Temporal } from "temporal-polyfill"; import { randomUUID } from "node:crypto"; import { db } from "./db"; -import { syncSession } from "./db/schema"; +import { SyncSessionTable, TransactionsTable } from "./db/schema"; import { add_session_log } from "./session-log"; - -export 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 -} - -export type Sb1Transaction = { - id: string - nonUniqueId: string - description: string - cleanedDescription: string - accountNumber: Sb1AccountNumber - amount: number - date: number - interestDate: number - typeCode: string - typeText: string - currencyCode: string - canShowDetails: boolean - source: string - isConfidential: boolean - bookingStatus: string - accountName: string - accountKey: string - accountCurrency: string - isFromCurrencyAccount: boolean - classificationInput: Sb1ClassificationInput -} - -export type Sb1Account = { - key: string; - accountNumber: string; - iban: string; - name: string; - description: string; - balance: number; - availableBalance: number; - currencyCode: string; - owner: Sb1AccountOwner; - productType: string; - type: string; - productId: string; - descriptionCode: string; - accountProperties: { [key: string]: boolean }; -} - -export type Sb1AccountOwner = { - name: string; - firstName: string; - lastName: string; - type: string; - age: number; - customerKey: string; - ssnKey: string; -} - - -export type Sb1AccountNumber = { - value: string - formatted: string - unformatted: string -} - -export type Sb1ClassificationInput = { - id: string - amount: number - type: string - text: string - date: string -} +import type { Sb1Account, Sb1Tokens, Sb1Transaction } from "$lib/shared"; const auth = { async is_ready(): Promise<boolean> { @@ -87,10 +14,10 @@ const auth = { }, async get_auth_info() { const entity = await db.select({ - refreshTokenCreated: syncSession.refreshTokenCreated, - accessTokenCreated: syncSession.accessTokenCreated, - tokens: syncSession.tokens - }).from(syncSession) + 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 @@ -104,7 +31,7 @@ const auth = { async init_auth_session(): Promise<string> { const state = randomUUID() - await db.insert(syncSession).values({ + await db.insert(SyncSessionTable).values({ authzState: state }) @@ -118,19 +45,25 @@ const auth = { }, async get_access_token() { const result = await db.select({ - tokens: syncSession.tokens, - refreshTokenCreated: syncSession.refreshTokenCreated, - accessTokenCreated: syncSession.accessTokenCreated - }).from(syncSession) + 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 @@ -141,9 +74,9 @@ const auth = { async refresh_token(): Promise<Sb1Tokens | null> { console.log("Refreshing tokens") const entity = await db.select({ - tokens: syncSession.tokens, - id: syncSession.id - }).from(syncSession) + tokens: SyncSessionTable.tokens, + id: SyncSessionTable.id + }).from(SyncSessionTable) const { tokens: currentTokens, id } = entity[0] @@ -167,7 +100,7 @@ const auth = { const tokens = await res.json() as Sb1Tokens const epoch = Temporal.Now.instant().epochMilliseconds if (res.ok) { - await db.update(syncSession).set({ tokens, accessTokenCreated: epoch.toString(), refreshTokenCreated: epoch.toString() }).where(eq(syncSession.id, id)) + 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 { @@ -195,25 +128,60 @@ const data = { }, 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")) + + 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[]; + return json["transactions"] as Sb1Transaction[] } } -export default { auth, data } +export default { auth, data, init } -export function formatInstant( +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" @@ -236,4 +204,4 @@ export function formatInstant( /yyyy|MM|dd|HH|mm|ss/g, (token) => replacements[token] ); -} +}
\ No newline at end of file |
