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;
|
type HttpHeaders = browser.webRequest.HttpHeaders;
|
||||||
|
|
||||||
function extractFromHeaders(headers: HttpHeaders): PartialStateUpdate {
|
function extractAuthStateFromHeaders(headers: HttpHeaders): PartialStateUpdate {
|
||||||
let apiKey = '';
|
let apiKey = '';
|
||||||
let gqlAuthToken = '';
|
let authToken = '';
|
||||||
let rsaFingerprint = '';
|
let fingerprintValue = '';
|
||||||
|
|
||||||
for (const header of headers) {
|
for (const header of headers) {
|
||||||
switch (header.name.toLowerCase()) {
|
switch (header.name.toLowerCase()) {
|
||||||
case "authorization":
|
case "authorization":
|
||||||
gqlAuthToken = header.value || '';
|
authToken = header.value || '';
|
||||||
break;
|
break;
|
||||||
case "apikey":
|
case "apikey":
|
||||||
apiKey = header.value || '';
|
apiKey = header.value || '';
|
||||||
break;
|
break;
|
||||||
case "rsa-fingerprint":
|
case "fingerprintvalue":
|
||||||
rsaFingerprint = header.value || '';
|
fingerprintValue = header.value || '';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { apiKey, authToken, fingerprintValue };
|
||||||
return { apiKey, gqlAuthToken, rsaFingerprint };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
browser.webRequest.onBeforeSendHeaders.addListener(e => {
|
browser.webRequest.onBeforeSendHeaders.addListener(e => {
|
||||||
// TODO if request errors, then clear state and try again.
|
// TODO if request errors, then clear state and try again.
|
||||||
(async function() {
|
(async function() {
|
||||||
const state = await State.current;
|
let state = await State.current;
|
||||||
if (!state.ready && e.requestHeaders) {
|
if (!state.ready && e.requestHeaders) {
|
||||||
console.info('Will update auth info from headers');
|
console.info('Will update auth info from headers');
|
||||||
|
|
||||||
// Lock ready to true so we don't have multiple attempts
|
// Lock ready to true so we don't have multiple attempts
|
||||||
// at getting account info.
|
// at getting account info.
|
||||||
await State.update({ ready: true });
|
await State.update({ ready: true });
|
||||||
let newState = extractFromHeaders(e.requestHeaders);
|
let newState = extractAuthStateFromHeaders(e.requestHeaders);
|
||||||
|
state = await State.update(newState);
|
||||||
|
|
||||||
State.update(newState)
|
if (!newState.accounts) {
|
||||||
.then(state => !state.accounts ? fetchAccounts() : state.accounts)
|
const accounts = await fetchAccounts(state);
|
||||||
.then(accounts => State.update({ accounts }))
|
await State.update({ accounts });
|
||||||
.then(() => console.log('done'));
|
console.info('Acquired account data');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, matchGraphQL, ["requestHeaders"]);
|
}, 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 {
|
export interface Accounts {
|
||||||
nodes: Array<Node>;
|
edges: Array<Edge>;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Edge {
|
||||||
|
//appears to be an id?
|
||||||
|
cursor: string;
|
||||||
|
node: Node;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Node {
|
export interface Node {
|
||||||
id: string;
|
id: string;
|
||||||
accountNumber: string;
|
idAccount: string;
|
||||||
name: string;
|
name: string;
|
||||||
kennitala: string;
|
accountNumber: string;
|
||||||
owner: string;
|
balance: number;
|
||||||
state: string;
|
availableAmount: number;
|
||||||
|
currency: string;
|
||||||
|
accountProductName: string;
|
||||||
|
overdraftLimit: number;
|
||||||
isVaxtareikningur30: boolean;
|
isVaxtareikningur30: boolean;
|
||||||
isNoticeAccount: boolean;
|
isNoticeAccount: boolean;
|
||||||
currency: string;
|
hasDebitCards: boolean;
|
||||||
isCurrencyAccount: boolean;
|
canBeRegularSavingsAccount: boolean;
|
||||||
availableAmount: number;
|
isWithdrawalAccount: boolean;
|
||||||
balance: number;
|
isOverdraftAllowed: boolean;
|
||||||
orderedAmountAvailable: number;
|
showOnFrontpage: boolean;
|
||||||
orderedAmountTotal: number;
|
|
||||||
}
|
}
|
|
@ -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 { Level } from 'level';
|
||||||
import { FetchAccountsResponse } from '~graphql/models';
|
import { Account } from '~landsbankinn/models';
|
||||||
|
|
||||||
export interface ExporterState {
|
export interface ExporterState {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
gqlAuthToken: string;
|
authToken: string;
|
||||||
rsaFingerprint: string;
|
fingerprintValue: string;
|
||||||
accounts: FetchAccountsResponse;
|
accounts: Array<Account>;
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PartialStateUpdate {
|
export interface PartialStateUpdate {
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
gqlAuthToken?: string;
|
authToken?: string;
|
||||||
rsaFingerprint?: string;
|
fingerprintValue?: string;
|
||||||
accounts?: FetchAccountsResponse;
|
accounts?: Array<Account>;
|
||||||
ready?: boolean;
|
ready?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FIVE_MINUTES = 5 * 60 * 1000;
|
const FIVE_MINUTES = 0.25 * 60 * 1000;
|
||||||
|
|
||||||
type LevelError = Error | null | undefined;
|
type LevelError = Error | null | undefined;
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ class LevelState {
|
||||||
this.currentTTLHandle = window.setTimeout(async () => await this.clear(), FIVE_MINUTES);
|
this.currentTTLHandle = window.setTimeout(async () => await this.clear(), FIVE_MINUTES);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(stateUpdate: ExporterState | PartialStateUpdate) {
|
async update(stateUpdate: ExporterState | PartialStateUpdate): Promise<ExporterState> {
|
||||||
this.clearTTL();
|
this.clearTTL();
|
||||||
const currentState: ExporterState = await this.current;
|
const currentState: ExporterState = await this.current;
|
||||||
let newState = { ...currentState, ...stateUpdate };
|
let newState = { ...currentState, ...stateUpdate };
|
||||||
|
|
Loading…
Reference in New Issue