More work on refactoring and making statements page function
This commit is contained in:
parent
1f7a3edba2
commit
32dfd7d7b1
|
@ -1,7 +1,8 @@
|
||||||
const matchGraphQL = { urls: ["https://graphql.landsbankinn.is/v2"] };
|
const matchGraphQL = { urls: ["https://graphql.landsbankinn.is/v2"] };
|
||||||
|
|
||||||
import { fetchAccounts } from '~/src/landsbankinn';
|
import { fetchAccounts } from '../landsbankinn';
|
||||||
import { PartialStateUpdate, State } from '~/src/state';
|
import { PartialStateUpdate } from '../state';
|
||||||
|
import { State } from '../state/level';
|
||||||
|
|
||||||
type HttpHeaders = browser.webRequest.HttpHeaders;
|
type HttpHeaders = browser.webRequest.HttpHeaders;
|
||||||
|
|
||||||
|
@ -50,3 +51,16 @@ browser.webRequest.onBeforeSendHeaders.addListener(e => {
|
||||||
}, matchGraphQL, ["requestHeaders"]);
|
}, matchGraphQL, ["requestHeaders"]);
|
||||||
|
|
||||||
export { }
|
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!
|
||||||
|
|
|
@ -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',
|
GetAccounts = 'GET_ACCOUNTS',
|
||||||
DownloadTransactions = 'DOWNLOAD_TRANSACTIONS'
|
DownloadTransactions = 'DOWNLOAD_TRANSACTIONS'
|
||||||
}
|
}
|
||||||
|
@ -16,8 +20,33 @@ async function getAccounts(sender: MessageSender): Promise<any> {
|
||||||
browser.tabs.sendMessage(sender.tab?.id!, message);
|
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) => {
|
browser.runtime.onMessage.addListener((message, sender) => {
|
||||||
|
console.log('received message', message);
|
||||||
switch (message.action) {
|
switch (message.action) {
|
||||||
|
case Actions.EnsureState:
|
||||||
|
ensureState(message, sender);
|
||||||
case Actions.GetAccounts:
|
case Actions.GetAccounts:
|
||||||
getAccounts(sender);
|
getAccounts(sender);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export default {
|
||||||
|
StateTTL: 5 * 60 * 1000
|
||||||
|
}
|
|
@ -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 { }
|
|
@ -34,7 +34,7 @@ export async function fetchAccounts(ctx: Context): Promise<Array<Account>> {
|
||||||
referrerPolicy: "strict-origin-when-cross-origin",
|
referrerPolicy: "strict-origin-when-cross-origin",
|
||||||
headers: {
|
headers: {
|
||||||
apikey: ctx.apiKey,
|
apikey: ctx.apiKey,
|
||||||
authorization: ctx.authToken,
|
authorization: `Bearer ${ctx.authToken}`,
|
||||||
"rsa-fingerprint": ctx.fingerprintValue,
|
"rsa-fingerprint": ctx.fingerprintValue,
|
||||||
"onlinebank-type": "personal" //Todo can it be also business?
|
"onlinebank-type": "personal" //Todo can it be also business?
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
import { setDefaultOptions, format } from 'date-fns';
|
import { setDefaultOptions, format } from 'date-fns';
|
||||||
import { enUS } from 'date-fns/locale';
|
import { enUS } from 'date-fns/locale';
|
||||||
import { RawTransaction, DownloadTransactionsRequest } from './models';
|
import { RawTransaction, AuthSession } from './models';
|
||||||
|
|
||||||
setDefaultOptions({ locale: enUS });
|
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');
|
const formatDate = (date: Date) => format(date, 'ccc LLL dd yyyy');
|
||||||
|
|
||||||
function getUrl(accountNumber: string, dateFrom: Date, dateTo: Date) {
|
function getUrl(accountNumber: string, dateFrom: Date, dateTo: Date) {
|
||||||
|
@ -40,3 +50,31 @@ export async function downloadTransactions(req: DownloadTransactionsRequest): Pr
|
||||||
return new Array();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
export enum TransactionFormat { JSON };
|
export interface AuthSession {
|
||||||
export enum OutputFormat { CSV };
|
accessToken: string;
|
||||||
|
name: string;
|
||||||
export interface DownloadTransactionsRequest {
|
type: string;
|
||||||
accountNumber: string;
|
username: string;
|
||||||
dateFrom: Date;
|
ttlInSeconds: number;
|
||||||
dateTo: Date;
|
|
||||||
outputFormat: OutputFormat;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RawTransaction {
|
export interface RawTransaction {
|
||||||
|
|
|
@ -9,12 +9,21 @@
|
||||||
"webRequestBlocking"
|
"webRequestBlocking"
|
||||||
],
|
],
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": [ "background/auth-state-handler.ts", "background/message-handler.ts" ],
|
"scripts": [
|
||||||
|
"background/message-handler.ts"
|
||||||
|
],
|
||||||
"persistent": false
|
"persistent": false
|
||||||
},
|
},
|
||||||
"content_scripts": [{
|
"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"],
|
"matches": ["https://netbanki.landsbankinn.is/Ebli/Statements/ClientSummary.aspx"],
|
||||||
"js": ["content-scripts/statement-page.tsx"],
|
"js": ["content-scripts/statement-page.tsx"],
|
||||||
"run_at": "document_start"
|
"run_at": "document_start"
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
||||||
|
}
|
|
@ -1,21 +1,5 @@
|
||||||
import { Level } from 'level';
|
import { Level } from 'level';
|
||||||
import { Account } from '~landsbankinn/models';
|
import { ExporterState, PartialStateUpdate } from './index';
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FIVE_MINUTES = 0.25 * 60 * 1000;
|
const FIVE_MINUTES = 0.25 * 60 * 1000;
|
||||||
|
|
Loading…
Reference in New Issue