- accessToken and refreshToken methods from interface
- accessToken param from transactionsPastDay
- Unused env variables from config.ts

Added
- Initial Refresh token to env
- Dayjs library for working with dates

Implemented
- getAccessToken
- getRefreshToken
- fetchNewRefreshToken
- transactionsPastDay

Signed-off-by: Martin Berg Alstad <git@martials.no>
This commit is contained in:
Martin Berg Alstad
2024-12-25 21:06:42 +01:00
committed by Martin Berg Alstad
parent 480c0356f9
commit 6650e2cd2b
7 changed files with 127 additions and 49 deletions

View File

@ -1,4 +1,12 @@
// TODO move types
import {
BANK_INITIAL_REFRESH_TOKEN,
BANK_OAUTH_CLIENT_ID,
BANK_OAUTH_CLIENT_SECRET,
} from "@/../config.ts"
import logger from "@/logger.ts"
import dayjs from "dayjs"
export interface OAuthTokenResponse {
access_token: string
expires_in: number
@ -8,6 +16,16 @@ export interface OAuthTokenResponse {
refresh_token: string
}
interface AccessToken {
access_token: string
expires_in: number
}
interface RefreshToken {
refresh_token: string
expires_in: number
}
export interface Transaction {
id: string
date: string
@ -22,36 +40,102 @@ export interface Transaction {
export type Bank = Sparebank1
export interface Sparebank1 {
accessToken: () => Promise<OAuthTokenResponse>
refreshToken: (refreshToken: string) => Promise<OAuthTokenResponse>
transactionsPastDay: (
accountKeys: ReadonlyArray<string> | string,
accessToken: string,
) => Promise<ReadonlyArray<Transaction>>
}
export class Sparebank1Impl implements Sparebank1 {
private baseUrl = "https://api.sparebank1.no"
private static baseUrl = "https://api.sparebank1.no"
private _accessToken: AccessToken | undefined
private _refreshToken: RefreshToken | undefined
// TODO remove?
async accessToken(): Promise<OAuthTokenResponse> {
throw new Error("Not implemented")
// if (response.ok) {
// return await response.json()
// }
// throw new Error(`Failed to get access token. ${response.statusText}`)
private set accessToken(accessToken: AccessToken) {
this._accessToken = accessToken
}
async refreshToken(refreshToken: string): Promise<OAuthTokenResponse> {
throw new Error("Not implemented")
private set refreshToken(refreshToken: RefreshToken) {
this._refreshToken = refreshToken
}
private async getAccessToken(): Promise<string> {
const accessToken = this._accessToken
if (!accessToken) {
const response = await this.fetchNewRefreshToken()
return response.access_token
}
return accessToken.access_token
}
private async getRefreshToken(): Promise<string> {
const refreshToken = this._refreshToken
// TODO check if valid, use jsonwebtoken npm library?
const isValid = true
if (!refreshToken) {
return BANK_INITIAL_REFRESH_TOKEN
} else if (isValid) {
return refreshToken.refresh_token
} else {
const response = await this.fetchNewRefreshToken()
return response.refresh_token
}
}
async fetchNewRefreshToken(): Promise<OAuthTokenResponse> {
const refreshToken: string = await this.getRefreshToken()
const queries = new URLSearchParams({
client_id: BANK_OAUTH_CLIENT_ID,
client_secret: BANK_OAUTH_CLIENT_SECRET,
refresh_token: refreshToken,
grant_type: "refresh_token",
})
const response = await fetch(`${Sparebank1Impl.baseUrl}/token?${queries}`)
if (!response.ok) {
throw new Error("Failed to fetch refresh token")
}
const oAuthToken: OAuthTokenResponse = await response.json()
this.accessToken = {
access_token: oAuthToken.access_token,
expires_in: oAuthToken.expires_in,
}
this.refreshToken = {
refresh_token: oAuthToken.refresh_token,
expires_in: oAuthToken.refresh_token_expires_in,
}
return oAuthToken
}
async transactionsPastDay(
accountKeys: ReadonlyArray<string> | string,
accessToken: string,
): Promise<ReadonlyArray<Transaction>> {
throw new Error("Not implemented")
const today = dayjs()
const lastDay = today.subtract(1, "day")
const queries = new URLSearchParams({
// TODO allow multiple accountKeys
accountKey:
typeof accountKeys === "string" ? accountKeys : accountKeys[0],
fromDate: lastDay.toString(),
toDate: today.toString(),
})
const accessToken = await this.getAccessToken()
const response = await fetch(
`${Sparebank1Impl.baseUrl}/transactions?${queries}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
)
if (response.ok) {
return response.json()
} else {
logger.warn(
`transactionsPastDay returned a ${response.status} with the text ${response.statusText}`,
)
return []
}
}
}

View File

@ -7,7 +7,7 @@ import {
} from "@/bank/sparebank1.ts"
import { bankTransactionIntoActualTransaction } from "@/mappings.ts"
import { ACTUAL_ACCOUNT_IDS, BANK_ACCOUNT_IDS } from "../config.ts"
import logger from "./logger.ts"
import logger from "@/logger.ts"
import type { UUID } from "node:crypto"
// TODO Transports api for pino https://github.com/pinojs/pino/blob/HEAD/docs/transports.md
@ -24,22 +24,25 @@ export async function daily(actual: Actual, bank: Bank): Promise<void> {
bankTransactionIntoActualTransaction(transaction, accountId),
)
logger.debug(`Mapped ${JSON.stringify(transactions)} to ${JSON.stringify(actualTransactions)} transactions`)
logger.debug(
`Mapped ${JSON.stringify(transactions)} to ${JSON.stringify(actualTransactions)} transactions`,
)
// TODO Import transactions into Actual
// If multiple accounts, loop over them
// Get account ID from mapper
const response = await actual.importTransactions(accountId, actualTransactions)
const response = await actual.importTransactions(
accountId,
actualTransactions,
)
logger.info(`ImportTransactionsResponse=${JSON.stringify(response)}`)
}
async function fetchTransactionsFromPastDay(
bank: Bank,
): Promise<ReadonlyArray<Transaction>> {
// TODO refresh token
const { access_token } = await bank.refreshToken("my_refresh_token")
return bank.transactionsPastDay(BANK_ACCOUNT_IDS, access_token)
return bank.transactionsPastDay(BANK_ACCOUNT_IDS)
}
async function main(): Promise<void> {