More work on refactoring and making statements page function

This commit is contained in:
projectmoon 2023-03-21 16:58:19 +01:00
parent 1f7a3edba2
commit 32dfd7d7b1
11 changed files with 228 additions and 37 deletions

View File

@ -1,7 +1,8 @@
const matchGraphQL = { urls: ["https://graphql.landsbankinn.is/v2"] };
import { fetchAccounts } from '~/src/landsbankinn';
import { PartialStateUpdate, State } from '~/src/state';
import { fetchAccounts } from '../landsbankinn';
import { PartialStateUpdate } from '../state';
import { State } from '../state/level';
type HttpHeaders = browser.webRequest.HttpHeaders;
@ -50,3 +51,16 @@ browser.webRequest.onBeforeSendHeaders.addListener(e => {
}, matchGraphQL, ["requestHeaders"]);
export { }
// Here we intercept and record the necessary auth info. But what if
// we cannot do that, if user logs directly into a page where there
// are no graphql calls?
// We can get device fingerprint via content_script, and call session.
// Have a message called ENSURE_STATE.
// Sent to background page w/ device_fingerprint.
// If state not ready, call session endpoint, and parse output.
// Record state.
// Content script for ensure state runs everywhere.
// There is a value on the page that has everything we need!

View File

@ -1,6 +1,10 @@
import { State } from "~/src/state";
import { PartialStateUpdate } from "../state";
import { State } from '../state/level';
import * as landsbankinn from '../landsbankinn';
import { fetchAccounts } from "../landsbankinn";
enum Actions {
export enum Actions {
EnsureState = 'ENSURE_STATE',
GetAccounts = 'GET_ACCOUNTS',
DownloadTransactions = 'DOWNLOAD_TRANSACTIONS'
}
@ -16,8 +20,33 @@ async function getAccounts(sender: MessageSender): Promise<any> {
browser.tabs.sendMessage(sender.tab?.id!, message);
}
async function ensureState(message: any, sender: MessageSender) {
const clientState: PartialStateUpdate = message.clientState;
const state = await State.current;
if (!state.ready) {
await State.update({ ready: true });
const session = await landsbankinn.authSession();
const newState = await State.update({
authToken: session.accessToken, ...clientState
})
if (!newState.accounts) {
const accounts = await fetchAccounts(newState);
await State.update({ accounts });
console.info('Acquired account data');
}
console.info('Updated state from client and auth session');
console.log(await State.current);
}
}
browser.runtime.onMessage.addListener((message, sender) => {
console.log('received message', message);
switch (message.action) {
case Actions.EnsureState:
ensureState(message, sender);
case Actions.GetAccounts:
getAccounts(sender);
}

3
src/config.ts Normal file
View File

@ -0,0 +1,3 @@
export default {
StateTTL: 5 * 60 * 1000
}

View File

@ -0,0 +1,91 @@
import { PartialStateUpdate } from "../state"; // TODO do not run state on content scripts
import { Actions } from '../background/message-handler';
// The actual information we care about
interface ApiInformation {
graphQLUrl: string;
apikey: string;
locale: string;
}
interface RegularWindowProperties {
add_deviceprint(): string
LIPageData: ApiInformation;
}
interface MegaLIData {
megaHeader: ApiInformation;
}
interface MegaWindowProperties {
LIPageData: MegaLIData;
add_deviceprint(): string;
}
interface LIWindowRegular extends Window {
wrappedJSObject: RegularWindowProperties;
}
interface LIWindowMega extends Window {
wrappedJSObject: MegaWindowProperties;
}
type LIWindow = LIWindowRegular | LIWindowMega;
type AnyWindow = Window | LIWindowRegular | LIWindowMega
function isApiInformation(liData: ApiInformation) {
return liData.graphQLUrl !== undefined &&
liData.apikey !== undefined &&
liData.locale !== undefined;
}
function isRegularWindow(win: AnyWindow): win is LIWindowRegular {
const liWindow = win as LIWindowRegular;
return typeof liWindow.wrappedJSObject.add_deviceprint === 'function' &&
liWindow.wrappedJSObject.LIPageData !== undefined &&
isApiInformation(liWindow.wrappedJSObject.LIPageData);
}
function isMegaHeaderWindow(win: AnyWindow): win is LIWindowRegular {
const liWindow = win as LIWindowMega;
return typeof liWindow.wrappedJSObject.add_deviceprint === 'function' &&
liWindow.wrappedJSObject.LIPageData !== undefined &&
isApiInformation(liWindow.wrappedJSObject.LIPageData.megaHeader);
}
// Type guard that coerces the global window object to the specialized
// Landsbankinn window object with useful info attached to it. There
// are at least two different ways this data is presented.
function isLandsbankinnWindow(win: AnyWindow): win is LIWindow {
return isRegularWindow(win) || isMegaHeaderWindow(win);
}
function extractStateUpdate(win: LIWindow): PartialStateUpdate | undefined {
if (isRegularWindow(win)) {
return {
apiKey: win.wrappedJSObject.LIPageData.apikey,
fingerprintValue: win.wrappedJSObject.add_deviceprint()
};
} else if (isMegaHeaderWindow(win)) {
return {
apiKey: win.wrappedJSObject.LIPageData.megaHeader.apikey,
fingerprintValue: win.wrappedJSObject.add_deviceprint()
};
} else {
return undefined;
}
}
// Communicates data about the GraphQL URL and API key to the
// extension.
document.addEventListener('DOMContentLoaded', async () => {
console.log(window);
if (isLandsbankinnWindow(window)) {
console.info('Ensuring extension state');
const clientState = extractStateUpdate(window);
await browser.runtime.sendMessage({
action: Actions.EnsureState, clientState
});
}
});
export { }

View File

@ -34,7 +34,7 @@ export async function fetchAccounts(ctx: Context): Promise<Array<Account>> {
referrerPolicy: "strict-origin-when-cross-origin",
headers: {
apikey: ctx.apiKey,
authorization: ctx.authToken,
authorization: `Bearer ${ctx.authToken}`,
"rsa-fingerprint": ctx.fingerprintValue,
"onlinebank-type": "personal" //Todo can it be also business?
},

View File

@ -1,9 +1,19 @@
import { setDefaultOptions, format } from 'date-fns';
import { enUS } from 'date-fns/locale';
import { RawTransaction, DownloadTransactionsRequest } from './models';
import { RawTransaction, AuthSession } from './models';
setDefaultOptions({ locale: enUS });
export enum TransactionFormat { JSON };
export enum OutputFormat { CSV };
export interface DownloadTransactionsRequest {
accountNumber: string;
dateFrom: Date;
dateTo: Date;
outputFormat: OutputFormat;
}
const formatDate = (date: Date) => format(date, 'ccc LLL dd yyyy');
function getUrl(accountNumber: string, dateFrom: Date, dateTo: Date) {
@ -40,3 +50,31 @@ export async function downloadTransactions(req: DownloadTransactionsRequest): Pr
return new Array();
}
}
export async function authSession(): Promise<AuthSession> {
const url = 'https://netbanki.landsbankinn.is/api/auth/session';
const options: RequestInit = {
method: "GET",
credentials: "include",
cache: "no-cache",
referrerPolicy: "strict-origin-when-cross-origin",
headers: {
"Accept": "*/*",
"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']);
return Promise.reject(error);
} else {
return Promise.reject(result);
}
}
}

View File

@ -1,11 +1,9 @@
export enum TransactionFormat { JSON };
export enum OutputFormat { CSV };
export interface DownloadTransactionsRequest {
accountNumber: string;
dateFrom: Date;
dateTo: Date;
outputFormat: OutputFormat;
export interface AuthSession {
accessToken: string;
name: string;
type: string;
username: string;
ttlInSeconds: number;
}
export interface RawTransaction {

View File

@ -9,12 +9,21 @@
"webRequestBlocking"
],
"background": {
"scripts": [ "background/auth-state-handler.ts", "background/message-handler.ts" ],
"scripts": [
"background/message-handler.ts"
],
"persistent": false
},
"content_scripts": [{
"matches": ["https://netbanki.landsbankinn.is/Ebli/Statements/ClientSummary.aspx"],
"js": ["content-scripts/statement-page.tsx"],
"run_at": "document_start"
}]
"content_scripts": [
{
"matches": ["https://netbanki.landsbankinn.is/*"],
"js": ["content-scripts/ensure-state.ts"],
"run_at": "document_start"
},
{
"matches": ["https://netbanki.landsbankinn.is/Ebli/Statements/ClientSummary.aspx"],
"js": ["content-scripts/statement-page.tsx"],
"run_at": "document_start"
}
]
}

0
src/state/empty.ts Normal file
View File

25
src/state/index.ts Normal file
View File

@ -0,0 +1,25 @@
import { Account } from '../landsbankinn/models';
export interface ExporterState {
apiKey: string;
authToken: string;
fingerprintValue: string;
accounts: Array<Account>;
ready: boolean;
}
export interface PartialStateUpdate {
apiKey?: string;
authToken?: string;
fingerprintValue?: string;
accounts?: Array<Account>;
ready?: boolean;
}
// TODO export it and replace direct references to level with it
interface StateStore {
readonly current: Promise<ExporterState>;
update(u: ExporterState | PartialStateUpdate): Promise<ExporterState>;
clear(): Promise<any>;
}

View File

@ -1,21 +1,5 @@
import { Level } from 'level';
import { Account } from '~landsbankinn/models';
export interface ExporterState {
apiKey: string;
authToken: string;
fingerprintValue: string;
accounts: Array<Account>;
ready: boolean;
}
export interface PartialStateUpdate {
apiKey?: string;
authToken?: string;
fingerprintValue?: string;
accounts?: Array<Account>;
ready?: boolean;
}
import { ExporterState, PartialStateUpdate } from './index';
const FIVE_MINUTES = 0.25 * 60 * 1000;