Removed
- 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:
parent
480c0356f9
commit
6650e2cd2b
@ -1,14 +1,15 @@
|
||||
# Actual Budget
|
||||
ACTUAL_BUDGET_ID=your-budget-id
|
||||
ACTUAL_SYNC_ID=your-sync-id
|
||||
ACTUAL_SERVER_URL=your-server-url
|
||||
ACTUAL_SERVER_URL=http://your-server-url:5006
|
||||
ACTUAL_PASSWORD=your-password
|
||||
ACTUAL_ACCOUNT_IDS=your-account-id1,your-account-id2
|
||||
# Bank
|
||||
BANK_INITIAL_REFRESH_TOKEN=initial-valid-refresh-token
|
||||
BANK_OAUTH_CLIENT_ID=your-client-id
|
||||
BANK_OAUTH_CLIENT_SECRET=your-client-secret
|
||||
BANK_OAUTH_STATE=your-state
|
||||
BANK_OAUTH_REDIRECT_URI=your-redirect-uri
|
||||
BANK_OAUTH_REDIRECT_URI=http://your-redirect-uri.com
|
||||
BANK_ACCOUNT_IDS=your-account-id1,your-account-id2
|
||||
# Configuration
|
||||
# trace | error | warn | info | debug | trace
|
||||
LOG_LEVEL=info
|
||||
LOG_LEVEL=info# trace | error | warn | info | debug | trace
|
||||
|
@ -3,17 +3,17 @@ import dotenv from "dotenv"
|
||||
|
||||
dotenv.config()
|
||||
|
||||
export const ACTUAL_BUDGET_ID = getOrThrow("ACTUAL_BUDGET_ID")
|
||||
export const ACTUAL_SYNC_ID = getOrThrow("ACTUAL_SYNC_ID")
|
||||
export const ACTUAL_SERVER_URL = getOrThrow("ACTUAL_SERVER_URL")
|
||||
export const ACTUAL_PASSWORD = getOrThrow("ACTUAL_PASSWORD")
|
||||
export const ACTUAL_ACCOUNT_IDS = getArrayOrThrow("ACTUAL_ACCOUNT_IDS")
|
||||
export const ACTUAL_DATA_DIR = ".cache"
|
||||
|
||||
export const BANK_INITIAL_REFRESH_TOKEN = getOrThrow(
|
||||
"BANK_INITIAL_REFRESH_TOKEN",
|
||||
)
|
||||
export const BANK_OAUTH_CLIENT_ID = getOrThrow("BANK_OAUTH_CLIENT_ID")
|
||||
export const BANK_OAUTH_CLIENT_SECRET = getOrThrow("BANK_OAUTH_CLIENT_SECRET")
|
||||
export const BANK_OAUTH_REDIRECT_URI = getOrThrow("BANK_OAUTH_REDIRECT_URI")
|
||||
export const BANK_OAUTH_STATE = getOrThrow("BANK_OAUTH_STATE")
|
||||
export const BANK_ACCOUNT_IDS = getArrayOrThrow("BANK_ACCOUNT_IDS")
|
||||
|
||||
function getOrThrow(key: string): string {
|
||||
|
@ -15,6 +15,7 @@
|
||||
"@actual-app/api": "^24.12.0",
|
||||
"@dotenvx/dotenvx": "^1.31.3",
|
||||
"cron": "^3.3.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^16.4.7",
|
||||
"pino": "^9.5.0",
|
||||
"prettier": "^3.4.2"
|
||||
|
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@ -14,6 +14,9 @@ dependencies:
|
||||
cron:
|
||||
specifier: ^3.3.1
|
||||
version: 3.3.1
|
||||
dayjs:
|
||||
specifier: ^1.11.13
|
||||
version: 1.11.13
|
||||
dotenv:
|
||||
specifier: ^16.4.7
|
||||
version: 16.4.7
|
||||
@ -1401,6 +1404,10 @@ packages:
|
||||
engines: {node: '>= 12'}
|
||||
dev: false
|
||||
|
||||
/dayjs@1.11.13:
|
||||
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
||||
dev: false
|
||||
|
||||
/debug@4.4.0:
|
||||
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
|
||||
engines: {node: '>=6.0'}
|
||||
|
@ -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 []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
15
src/main.ts
15
src/main.ts
@ -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> {
|
||||
|
@ -1,26 +1,8 @@
|
||||
import type { Bank, OAuthTokenResponse, Transaction } from "@/bank/sparebank1.ts"
|
||||
|
||||
const tokenResponse: OAuthTokenResponse = {
|
||||
access_token: "my_access_token",
|
||||
token_type: "Bearer",
|
||||
expires_in: 3600,
|
||||
refresh_token: "my_refresh_token",
|
||||
refresh_token_expires_in: 3600,
|
||||
refresh_token_absolute_expires_in: 3600,
|
||||
}
|
||||
import type { Bank, Transaction } from "@/bank/sparebank1.ts"
|
||||
|
||||
export class BankStub implements Bank {
|
||||
async accessToken(): Promise<OAuthTokenResponse> {
|
||||
return tokenResponse
|
||||
}
|
||||
|
||||
async refreshToken(_unused: string): Promise<OAuthTokenResponse> {
|
||||
return tokenResponse
|
||||
}
|
||||
|
||||
async transactionsPastDay(
|
||||
_accountIds: ReadonlyArray<string> | string,
|
||||
_accessToken: string,
|
||||
): Promise<ReadonlyArray<Transaction>> {
|
||||
const someFields = {
|
||||
date: "2019-08-20",
|
||||
|
Loading…
x
Reference in New Issue
Block a user