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"] };
|
||||
|
||||
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!
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
headers: {
|
||||
apikey: ctx.apiKey,
|
||||
authorization: ctx.authToken,
|
||||
authorization: `Bearer ${ctx.authToken}`,
|
||||
"rsa-fingerprint": ctx.fingerprintValue,
|
||||
"onlinebank-type": "personal" //Todo can it be also business?
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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": [{
|
||||
"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,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 { 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;
|
||||
|
Loading…
Reference in New Issue