Refactoring and fix landsbankinn graphql API
This commit is contained in:
parent
10dc0401be
commit
505ca314ba
|
@ -5,45 +5,46 @@ import { PartialStateUpdate, State } from './state';
|
|||
|
||||
type HttpHeaders = browser.webRequest.HttpHeaders;
|
||||
|
||||
function extractFromHeaders(headers: HttpHeaders): PartialStateUpdate {
|
||||
function extractAuthStateFromHeaders(headers: HttpHeaders): PartialStateUpdate {
|
||||
let apiKey = '';
|
||||
let gqlAuthToken = '';
|
||||
let rsaFingerprint = '';
|
||||
let authToken = '';
|
||||
let fingerprintValue = '';
|
||||
|
||||
for (const header of headers) {
|
||||
switch (header.name.toLowerCase()) {
|
||||
case "authorization":
|
||||
gqlAuthToken = header.value || '';
|
||||
authToken = header.value || '';
|
||||
break;
|
||||
case "apikey":
|
||||
apiKey = header.value || '';
|
||||
break;
|
||||
case "rsa-fingerprint":
|
||||
rsaFingerprint = header.value || '';
|
||||
case "fingerprintvalue":
|
||||
fingerprintValue = header.value || '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return { apiKey, gqlAuthToken, rsaFingerprint };
|
||||
return { apiKey, authToken, fingerprintValue };
|
||||
}
|
||||
|
||||
browser.webRequest.onBeforeSendHeaders.addListener(e => {
|
||||
// TODO if request errors, then clear state and try again.
|
||||
(async function() {
|
||||
const state = await State.current;
|
||||
let state = await State.current;
|
||||
if (!state.ready && e.requestHeaders) {
|
||||
console.info('Will update auth info from headers');
|
||||
|
||||
// Lock ready to true so we don't have multiple attempts
|
||||
// at getting account info.
|
||||
await State.update({ ready: true });
|
||||
let newState = extractFromHeaders(e.requestHeaders);
|
||||
let newState = extractAuthStateFromHeaders(e.requestHeaders);
|
||||
state = await State.update(newState);
|
||||
|
||||
State.update(newState)
|
||||
.then(state => !state.accounts ? fetchAccounts() : state.accounts)
|
||||
.then(accounts => State.update({ accounts }))
|
||||
.then(() => console.log('done'));
|
||||
if (!newState.accounts) {
|
||||
const accounts = await fetchAccounts(state);
|
||||
await State.update({ accounts });
|
||||
console.info('Acquired account data');
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, matchGraphQL, ["requestHeaders"]);
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
import { gql } from 'graphql-request/build/esm';
|
||||
|
||||
// Currently not imported due to a bug with Parcel and the bundle-text
|
||||
// importer, and complications with running the query from the
|
||||
// imported text.
|
||||
|
||||
// Exports
|
||||
export const fetchAccounts = gql`
|
||||
query fetchAccounts($first: Int, $after: String, $filter: AccountFilterArgs, $orderBy: AccountOrderBy) {
|
||||
viewer {
|
||||
id
|
||||
accounts(first: $first, after: $after, filter: $filter, orderBy: $orderBy) {
|
||||
nodes {
|
||||
...AccountSelectorAccountParts
|
||||
__typename
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment AccountSelectorAccountParts on Account {
|
||||
id
|
||||
accountNumber
|
||||
name
|
||||
kennitala
|
||||
owner
|
||||
state
|
||||
isVaxtareikningur30
|
||||
isNoticeAccount
|
||||
currency
|
||||
isCurrencyAccount
|
||||
availableAmount
|
||||
balance
|
||||
orderedAmountAvailable
|
||||
orderedAmountTotal
|
||||
__typename
|
||||
}
|
||||
`
|
|
@ -1,140 +0,0 @@
|
|||
import { GraphQLClient } from 'graphql-request/build/esm';
|
||||
import { setDefaultOptions, format } from 'date-fns';
|
||||
import { enUS } from 'date-fns/locale';
|
||||
import { RequestConfig } from 'graphql-request/build/esm/types';
|
||||
import { State } from './state';
|
||||
import * as queries from './graphql/queries';
|
||||
import { FetchAccountsResponse } from '~graphql/models';
|
||||
|
||||
setDefaultOptions({ locale: enUS });
|
||||
|
||||
const apiEndpoint = 'https://netbanki.landsbankinn.is/services/accounts/$accountID/transactions';
|
||||
|
||||
export enum TransactionFormat { JSON };
|
||||
export enum OutputFormat { CSV };
|
||||
|
||||
export interface DownloadTransactionsRequest {
|
||||
accountNumber: string;
|
||||
dateFrom: Date;
|
||||
dateTo: Date;
|
||||
outputFormat: OutputFormat;
|
||||
}
|
||||
|
||||
export interface RawTransaction {
|
||||
//txn id?
|
||||
id: string;
|
||||
//often null
|
||||
accountNumber: string | null;
|
||||
amount: Number;
|
||||
currencyCode: string;
|
||||
//ISO date
|
||||
bookingDate: string;
|
||||
//ISO date
|
||||
valueDate: string;
|
||||
|
||||
balanceAfterTransaction: Number;
|
||||
|
||||
displayText: string;
|
||||
|
||||
//how do these relate to id?
|
||||
reference: string;
|
||||
referenceNumber: string;
|
||||
|
||||
//how does it relate to the booking/value dates?
|
||||
//exact time of txn/
|
||||
timestamp: string;
|
||||
|
||||
textCodeDescription: string;
|
||||
payerDescription: string;
|
||||
|
||||
// e.g. Utteky med debitkorti
|
||||
description: String;
|
||||
|
||||
batchID: string;
|
||||
|
||||
// only for individuals?
|
||||
recipientKennitala: string;
|
||||
|
||||
recipientName: string;
|
||||
//probably some kind of txn category (central bank?)
|
||||
transactionCodekey: string;
|
||||
|
||||
bankBranch: string;
|
||||
debitCardWarrantyNumber: string | null;
|
||||
|
||||
//currency to ISK exchange rate
|
||||
exchangeRate: Number;
|
||||
|
||||
// what is displayed to the user in the table
|
||||
customerDescription: string;
|
||||
|
||||
//don't know what this could be.
|
||||
details: null;
|
||||
|
||||
}
|
||||
|
||||
//Sat Jan 03 2023
|
||||
const formatDate = (date: Date) => format(date, 'ccc LLL dd yyyy');
|
||||
|
||||
function getUrl(accountNumber: string, dateFrom: Date, dateTo: Date) {
|
||||
const url = `https://netbanki.landsbankinn.is/services/accounts/${accountNumber}/transactions/`;
|
||||
const from = encodeURIComponent(formatDate(dateFrom));
|
||||
const to = encodeURIComponent(formatDate(dateTo));
|
||||
const params = `?from=${from}&to=${to}`
|
||||
return url + params;
|
||||
}
|
||||
|
||||
export async function downloadTransactions(req: DownloadTransactionsRequest): Promise<Array<RawTransaction>> {
|
||||
const url = getUrl(req.accountNumber, req.dateFrom, req.dateTo);
|
||||
const options: RequestInit = {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
cache: "no-cache",
|
||||
referrerPolicy: "strict-origin-when-cross-origin",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
const result = await fetch(url, options);
|
||||
if (result.ok) {
|
||||
return await result.json();
|
||||
} else {
|
||||
//attempt to read json.
|
||||
const error = await result.json();
|
||||
if (error.hasOwnProperty('message')) {
|
||||
console.error(error['message']);
|
||||
}
|
||||
console.error(result);
|
||||
return new Array();
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAccounts(): Promise<FetchAccountsResponse> {
|
||||
const state = await State.current;
|
||||
const options: RequestConfig = {
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
cache: "no-cache",
|
||||
referrerPolicy: "strict-origin-when-cross-origin",
|
||||
headers: {
|
||||
apikey: state.apiKey,
|
||||
authorization: state.gqlAuthToken,
|
||||
"rsa-fingerprint": state.rsaFingerprint
|
||||
},
|
||||
};
|
||||
|
||||
const graphQLClient = new GraphQLClient('https://graphql.landsbankinn.is/v2', options);
|
||||
|
||||
const variables = {
|
||||
"first": 100,
|
||||
"filter": {
|
||||
"includeClosed": true
|
||||
},
|
||||
};
|
||||
|
||||
const data: FetchAccountsResponse = await graphQLClient.request(queries.fetchAccounts, variables);
|
||||
//TODO transform janky graphql response into nicer data structure for us.
|
||||
return data;
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import { GraphQLClient } from 'graphql-request/build/esm';
|
||||
import { RequestConfig } from 'graphql-request/build/esm/types';
|
||||
import * as queries from './queries';
|
||||
import { FetchAccountsResponse } from './models';
|
||||
import { Account } from '../models';
|
||||
|
||||
// The Context interface separates the required authentication
|
||||
// information from the actual GraphQL API call. Anything that meets
|
||||
// the shape (i.e. the application auth state) can be passed in.
|
||||
export interface Context {
|
||||
apiKey: string;
|
||||
authToken: string;
|
||||
fingerprintValue: string;
|
||||
}
|
||||
|
||||
function convertToDomain(resp: FetchAccountsResponse): Array<Account> {
|
||||
const accounts = [];
|
||||
for (const edge of resp.viewer.accounts.edges) {
|
||||
accounts.push({
|
||||
accountNumber: edge.node.accountNumber,
|
||||
currency: edge.node.currency,
|
||||
name: edge.node.name
|
||||
});
|
||||
}
|
||||
|
||||
return accounts;
|
||||
}
|
||||
|
||||
export async function fetchAccounts(ctx: Context): Promise<Array<Account>> {
|
||||
const options: RequestConfig = {
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
cache: "no-cache",
|
||||
referrerPolicy: "strict-origin-when-cross-origin",
|
||||
headers: {
|
||||
apikey: ctx.apiKey,
|
||||
authorization: ctx.authToken,
|
||||
"rsa-fingerprint": ctx.fingerprintValue,
|
||||
"onlinebank-type": "personal" //Todo can it be also business?
|
||||
},
|
||||
};
|
||||
|
||||
const graphQLClient = new GraphQLClient('https://graphql.landsbankinn.is/v2', options);
|
||||
|
||||
const variables = {
|
||||
"pageSize": 40,
|
||||
};
|
||||
|
||||
const data: FetchAccountsResponse = await graphQLClient.request(queries.fetchAccounts, variables);
|
||||
return convertToDomain(data);
|
||||
}
|
|
@ -8,23 +8,31 @@ export interface Viewer {
|
|||
}
|
||||
|
||||
export interface Accounts {
|
||||
nodes: Array<Node>;
|
||||
edges: Array<Edge>;
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export interface Edge {
|
||||
//appears to be an id?
|
||||
cursor: string;
|
||||
node: Node;
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
id: string;
|
||||
accountNumber: string;
|
||||
idAccount: string;
|
||||
name: string;
|
||||
kennitala: string;
|
||||
owner: string;
|
||||
state: string;
|
||||
accountNumber: string;
|
||||
balance: number;
|
||||
availableAmount: number;
|
||||
currency: string;
|
||||
accountProductName: string;
|
||||
overdraftLimit: number;
|
||||
isVaxtareikningur30: boolean;
|
||||
isNoticeAccount: boolean;
|
||||
currency: string;
|
||||
isCurrencyAccount: boolean;
|
||||
availableAmount: number;
|
||||
balance: number;
|
||||
orderedAmountAvailable: number;
|
||||
orderedAmountTotal: number;
|
||||
hasDebitCards: boolean;
|
||||
canBeRegularSavingsAccount: boolean;
|
||||
isWithdrawalAccount: boolean;
|
||||
isOverdraftAllowed: boolean;
|
||||
showOnFrontpage: boolean;
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import { gql } from 'graphql-request/build/esm';
|
||||
|
||||
// Currently not imported due to a bug with Parcel and the bundle-text
|
||||
// importer, and complications with running the query from the
|
||||
// imported text.
|
||||
|
||||
// Exports
|
||||
export const fetchAccounts = gql`
|
||||
query fetchDomesticAccounts($after: String, $pageSize: Int = 20) {
|
||||
viewer {
|
||||
id
|
||||
accounts(first: $pageSize, after: $after, filter: {includedCurrencies: ["ISK"]}) {
|
||||
edges {
|
||||
cursor
|
||||
node {
|
||||
id
|
||||
idAccount
|
||||
name
|
||||
accountNumber
|
||||
balance
|
||||
availableAmount
|
||||
currency
|
||||
accountProductName
|
||||
overdraftLimit
|
||||
isVaxtareikningur30
|
||||
isNoticeAccount
|
||||
hasDebitCards
|
||||
canBeRegularSavingsAccount
|
||||
isWithdrawalAccount
|
||||
isOverdraftAllowed
|
||||
showOnFrontpage
|
||||
__typename
|
||||
}
|
||||
__typename
|
||||
}
|
||||
totalCount
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
currentPage
|
||||
__typename
|
||||
}
|
||||
__typename
|
||||
}
|
||||
__typename
|
||||
}
|
||||
}
|
||||
`
|
|
@ -0,0 +1,5 @@
|
|||
// Simply re-exports the organized code into one unified API.
|
||||
export * from './netbanki/api';
|
||||
export * from './netbanki/models';
|
||||
export * from './graphql/api';
|
||||
export * from './graphql/models';
|
|
@ -0,0 +1,9 @@
|
|||
// These models are common, unified, domain-specific types for the
|
||||
// extension. The models under the specific API clients are specific
|
||||
// to Landsbankinn's own domain.
|
||||
|
||||
export interface Account {
|
||||
accountNumber: string;
|
||||
name: String;
|
||||
currency: string;
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { setDefaultOptions, format } from 'date-fns';
|
||||
import { enUS } from 'date-fns/locale';
|
||||
import { RawTransaction, DownloadTransactionsRequest } from './models';
|
||||
|
||||
setDefaultOptions({ locale: enUS });
|
||||
|
||||
const formatDate = (date: Date) => format(date, 'ccc LLL dd yyyy');
|
||||
|
||||
function getUrl(accountNumber: string, dateFrom: Date, dateTo: Date) {
|
||||
const url = `https://netbanki.landsbankinn.is/services/accounts/${accountNumber}/transactions/`;
|
||||
const from = encodeURIComponent(formatDate(dateFrom));
|
||||
const to = encodeURIComponent(formatDate(dateTo));
|
||||
const params = `?from=${from}&to=${to}`
|
||||
return url + params;
|
||||
}
|
||||
|
||||
export async function downloadTransactions(req: DownloadTransactionsRequest): Promise<Array<RawTransaction>> {
|
||||
const url = getUrl(req.accountNumber, req.dateFrom, req.dateTo);
|
||||
const options: RequestInit = {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
cache: "no-cache",
|
||||
referrerPolicy: "strict-origin-when-cross-origin",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
|
||||
const result = await fetch(url, options);
|
||||
if (result.ok) {
|
||||
return await result.json();
|
||||
} else {
|
||||
//attempt to read error json.
|
||||
const error = await result.json();
|
||||
if (error.hasOwnProperty('message')) {
|
||||
console.error(error['message']);
|
||||
}
|
||||
console.error(result);
|
||||
return new Array();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
export enum TransactionFormat { JSON };
|
||||
export enum OutputFormat { CSV };
|
||||
|
||||
export interface DownloadTransactionsRequest {
|
||||
accountNumber: string;
|
||||
dateFrom: Date;
|
||||
dateTo: Date;
|
||||
outputFormat: OutputFormat;
|
||||
}
|
||||
|
||||
export interface RawTransaction {
|
||||
//txn id?
|
||||
id: string;
|
||||
//often null
|
||||
accountNumber: string | null;
|
||||
amount: Number;
|
||||
currencyCode: string;
|
||||
//ISO date
|
||||
bookingDate: string;
|
||||
//ISO date
|
||||
valueDate: string;
|
||||
|
||||
balanceAfterTransaction: Number;
|
||||
|
||||
displayText: string;
|
||||
|
||||
//how do these relate to id?
|
||||
reference: string;
|
||||
referenceNumber: string;
|
||||
|
||||
//how does it relate to the booking/value dates?
|
||||
//exact time of txn/
|
||||
timestamp: string;
|
||||
|
||||
textCodeDescription: string;
|
||||
payerDescription: string;
|
||||
|
||||
// e.g. Utteky med debitkorti
|
||||
description: String;
|
||||
|
||||
batchID: string;
|
||||
|
||||
// only for individuals?
|
||||
recipientKennitala: string;
|
||||
|
||||
recipientName: string;
|
||||
//probably some kind of txn category (central bank?)
|
||||
transactionCodekey: string;
|
||||
|
||||
bankBranch: string;
|
||||
debitCardWarrantyNumber: string | null;
|
||||
|
||||
//currency to ISK exchange rate
|
||||
exchangeRate: Number;
|
||||
|
||||
// what is displayed to the user in the table
|
||||
customerDescription: string;
|
||||
|
||||
//don't know what this could be.
|
||||
details: null;
|
||||
}
|
18
src/state.ts
18
src/state.ts
|
@ -1,23 +1,23 @@
|
|||
import { Level } from 'level';
|
||||
import { FetchAccountsResponse } from '~graphql/models';
|
||||
import { Account } from '~landsbankinn/models';
|
||||
|
||||
export interface ExporterState {
|
||||
apiKey: string;
|
||||
gqlAuthToken: string;
|
||||
rsaFingerprint: string;
|
||||
accounts: FetchAccountsResponse;
|
||||
authToken: string;
|
||||
fingerprintValue: string;
|
||||
accounts: Array<Account>;
|
||||
ready: boolean;
|
||||
}
|
||||
|
||||
export interface PartialStateUpdate {
|
||||
apiKey?: string;
|
||||
gqlAuthToken?: string;
|
||||
rsaFingerprint?: string;
|
||||
accounts?: FetchAccountsResponse;
|
||||
authToken?: string;
|
||||
fingerprintValue?: string;
|
||||
accounts?: Array<Account>;
|
||||
ready?: boolean;
|
||||
}
|
||||
|
||||
const FIVE_MINUTES = 5 * 60 * 1000;
|
||||
const FIVE_MINUTES = 0.25 * 60 * 1000;
|
||||
|
||||
type LevelError = Error | null | undefined;
|
||||
|
||||
|
@ -63,7 +63,7 @@ class LevelState {
|
|||
this.currentTTLHandle = window.setTimeout(async () => await this.clear(), FIVE_MINUTES);
|
||||
}
|
||||
|
||||
async update(stateUpdate: ExporterState | PartialStateUpdate) {
|
||||
async update(stateUpdate: ExporterState | PartialStateUpdate): Promise<ExporterState> {
|
||||
this.clearTTL();
|
||||
const currentState: ExporterState = await this.current;
|
||||
let newState = { ...currentState, ...stateUpdate };
|
||||
|
|
Loading…
Reference in New Issue