Skip to content

Commit

Permalink
feat: 🎸 Possibly first working version
Browse files Browse the repository at this point in the history
  • Loading branch information
JohanObrink committed Dec 20, 2020
1 parent 084e961 commit 0e4acba
Show file tree
Hide file tree
Showing 16 changed files with 1,217 additions and 368 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module.exports = {
extends: [
'airbnb-typescript/base',
],
ignorePatterns: ['*.test.ts'],
rules: {
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
// e.g. "@typescript-eslint/explicit-function-return-type": "off",
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,5 @@ dist

# TernJS port file
.tern-port

run.js
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,46 @@
# embedded-api

Since the proxy was blocked (and also deemed a bad idea by some), this is a reboot of the API running in process in the app(s).

## How to use

### Installing

`npm i -S @skolplattformen/embedded-api` or `yarn add @skolplattformen/embedded-api`

### Calling

#### Import and init

```javascript
import init from "@skolplattformen/embedded-api";

const api = init(fetch);
```

#### Login

```javascript
api.on("login", () => {
// keep going
});

const loginStatus = await api.login("YYYYMMDDXXXX");
loginStatus.on("PENDING", console.log("BankID app not yet opened"));
loginStatus.on("USER_SIGN", console.log("BankID app is open"));
loginStatus.on("ERROR", console.log("Something went wrong"));
loginStatus.on(
"OK",
console.log("BankID sign successful. Session will be established.")
);
```

#### Loading data

```javascript
// List children
const children = await api.getChildren();

// Get calendar
const calendar = await api.getCalendar(children[0].id);
```
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'.(ts|tsx)': '<rootDir>/node_modules/ts-jest/preprocessor.js'
'.(ts|tsx)': 'ts-jest'
},
testRegex: '(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$',
moduleFileExtensions: ["ts", "tsx", "js"]
Expand Down
6 changes: 0 additions & 6 deletions lib/__mocks__/axios.ts

This file was deleted.

21 changes: 15 additions & 6 deletions lib/children.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { AxiosAdapter, AxiosInstance } from 'axios'
import routes from './routes'
import { Child } from './types'
import { CalendarItem, Child, Fetch, RequestInit } from './types'
import { etjanst, child, calendarItem } from './parse'

export const list = (client: AxiosInstance) => async (): Promise<Child[]> => {
export const list = (fetch: Fetch, init?: RequestInit) => async (): Promise<Child[]> => {
const url = routes.children
const response = await client.get(url)
return response.data
const response = await fetch(url, init)
const data = await response.json()
return etjanst(data).map(child)
}

export const details = (client: AxiosAdapter) => async (id: string): Promise<Child> => ({})
export const calendar = (fetch: Fetch, init?:RequestInit) => async (childId: string): Promise<CalendarItem[]> => {
const url = routes.calendar(childId)
const response = await fetch(url, init)
const data = await response.json()
console.log(etjanst(data))
return etjanst(data).map(calendarItem)
}

// export const details = (_fetch: Fetch) => async (_id: string): Promise<Child> => ({})
88 changes: 36 additions & 52 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,43 @@
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
import { list } from './children'
import { checkStatus, getCookies, login } from './login'
import { EventEmitter } from 'events'
import {
checkStatus, getSessionCookie, login, LoginStatus,
} from './login'
import { CalendarItem, Child, Fetch, RequestInit } from './types'
import { calendar, list } from './children'

const pause = async (ms: number) => new Promise<void>((r) => setTimeout(r, ms))
class Api extends EventEmitter {
private fetch: Fetch

const emitter = new EventEmitter()
const init = () => {
const config: AxiosRequestConfig = {
headers: {
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.87 Safari/537.36'
},
maxRedirects: 0,
withCredentials: true
private session?: RequestInit

constructor(fetch: Fetch) {
super()
this.fetch = fetch
}

async login(personalNumber: string): Promise<LoginStatus> {
const ticket = await login(this.fetch)(personalNumber)
const loginStatus = checkStatus(this.fetch)(ticket)
loginStatus.on('OK', async () => {
const sessionCookie = await getSessionCookie(this.fetch)()
this.session = { headers: { Cookie: sessionCookie } }

this.emit('login')
})
return loginStatus
}
const client = axios.create(config)
let cookies: any = {}

client.interceptors.request.use((config) => {
console.log('request', config.method, config.url)
config.headers.Cookie = Object.entries(cookies)
.map(([key, value]) => `${key}=${value}`)
.join('; ')
return config
})
client.interceptors.response.use((response) => {
console.log('response', response.status, response.statusText, response.headers['set-cookie'])
if (response.headers['set-cookie']) {
const setCookies: string[] = response.headers['set-cookie']
setCookies.map((c) => c.split('=')).forEach(([key, value]) => cookies[key] = value)
}
return response
})

return {
...emitter,
login: async (personalNumber: string) => {
const ticket = await login(client)(personalNumber)
await pause(1000)
const check = checkStatus(client)(ticket)
check.on('OK', async () => {
console.log('get cookie')
const newCookies = await getCookies(client)()
cookies = {...cookies, ...newCookies}
console.log(cookies)

emitter.emit('login')
})
return check
},
getChildren: async () => {
const result = await list(client)()
return result
},

async getChildren(): Promise<Child[]> {
const data = await list(this.fetch, this.session)()
return data
}

async getCalendar(childId: string): Promise<CalendarItem[]> {
const data = await calendar(this.fetch, this.session)(childId)
return data
}
}

export default init
export default function init(fetch: Fetch) {
return new Api(fetch)
}
49 changes: 24 additions & 25 deletions lib/login.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import axios, { AxiosInstance } from 'axios'
import { login, checkStatus, getCookie } from './login'

jest.mock('axios')

let client: jest.Mocked<AxiosInstance>
import { login, checkStatus, getSessionCookie } from './login'
import { Fetch, Headers, Response } from './types'

describe('login', () => {
let fetch: jest.Mocked<Fetch >
let response: jest.Mocked<Response>
let headers: jest.Mocked<Headers>
beforeEach(() => {
client = axios.create() as jest.Mocked<AxiosInstance>
client.get.mockReset()
client.post.mockReset()
headers = { get: jest.fn() }
response = {
json: jest.fn(),
text: jest.fn(),
headers,
}
fetch = jest.fn().mockResolvedValue(response)
})
describe('#login', () => {
it('returns the correct result', async () => {
Expand All @@ -19,8 +22,8 @@ describe('login', () => {
order: '5fe57e4c-9ad2-4b52-b794-48adef2f6663',
}

client.post.mockResolvedValue({ data })
const result = await login(client)(personalNumber)
response.json.mockResolvedValue(data)
const result = await login(fetch)(personalNumber)

expect(result).toEqual({ order: '5fe57e4c-9ad2-4b52-b794-48adef2f6663' })
})
Expand All @@ -29,34 +32,30 @@ describe('login', () => {
const ticket = { order: '5fe57e4c-9ad2-4b52-b794-48adef2f6663' }

it('emits PENDING', (done) => {
client.get.mockResolvedValue({ data: 'PENDING' })
response.text.mockResolvedValue('PENDING')

const check = checkStatus(client)(ticket)
const check = checkStatus(fetch)(ticket)
check.on('PENDING', async () => {
await check.cancel()
done()
})
})
it('retries on PENDING', (done) => {
client.get.mockResolvedValueOnce({ data: 'PENDING' })
client.get.mockResolvedValueOnce({ data: 'OK' })
response.text.mockResolvedValueOnce('PENDING')
response.text.mockResolvedValueOnce('OK')

const check = checkStatus(client)(ticket)
const check = checkStatus(fetch)(ticket)
check.on('OK', () => {
expect(client.get).toHaveBeenCalledTimes(2)
expect(fetch).toHaveBeenCalledTimes(2)
done()
})
})
})
describe('#getCookie', () => {
it('sets cookie as client interceptor', async () => {
client.get.mockResolvedValue({
headers: {
'set-cookie': 'cookie',
},
})
describe('#getSessionCookie', () => {
it('returns session cookie', async () => {
headers.get.mockReturnValue('cookie')

const cookie = await getCookie(client)()
const cookie = await getSessionCookie(fetch)()

expect(cookie).toEqual('cookie')
})
Expand Down
65 changes: 18 additions & 47 deletions lib/login.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { EventEmitter } from 'events'
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios'
import routes from './routes'
import { AuthTicket } from './types'
import { AuthTicket, Fetch } from './types'

export const login = (client: AxiosInstance) => async (personalNumber: string): Promise<AuthTicket> => {
export const login = (fetch: Fetch) => async (personalNumber: string): Promise<AuthTicket> => {
const url = routes.login(personalNumber)
const result = await client.get<AuthTicket>(url)
return { order: result.data.order }
const response = await fetch(url)
const { order } = await response.json()
return { order }
}

/*
Expand All @@ -21,21 +21,22 @@ export enum LoginEvent {
export class LoginStatus extends EventEmitter {
private url: string

private client: AxiosInstance
private fetch: Fetch

private cancelled: boolean = false

constructor(client: AxiosInstance, url: string) {
constructor(fetch: Fetch, url: string) {
super()
this.client = client
this.fetch = fetch
this.url = url
this.check()
}

async check() {
const status = await this.client.get<string>(this.url)
this.emit(status.data)
if (!this.cancelled && status.data !== 'OK' && status.data !== 'ERROR!') {
const response = await this.fetch(this.url)
const status = await response.text()
this.emit(status)
if (!this.cancelled && status !== 'OK' && status !== 'ERROR!') {
setTimeout(() => this.check(), 1000)
}
}
Expand All @@ -45,43 +46,13 @@ export class LoginStatus extends EventEmitter {
}
}

export const checkStatus = (client: AxiosInstance) => (ticket: AuthTicket): LoginStatus => {
export const checkStatus = (fetch: Fetch) => (ticket: AuthTicket): LoginStatus => {
const url = routes.loginStatus(ticket.order)
return new LoginStatus(client, url)
return new LoginStatus(fetch, url)
}

const parseCookies = (newCookies: string[]): any => {
return newCookies
.map((c) => c.split('=')).map(([key, val]) => ({[key]: val}))
.reduce((obj1, obj2) => ({...obj1, ...obj2}))
}

export const getCookies = (client: AxiosInstance) => async (url = routes.loginCookie, cookies = {}): Promise<any> => {
try {
const response = await client.get(url)
if (response.headers['set-cookie']) {
cookies = {
...cookies,
...parseCookies(response.headers['set-cookie'])
}
}
return cookies
} catch (err) {
const { response } = err as AxiosError
if (response?.status === 302) {
if (response.headers['set-cookie']) {
cookies = {
...cookies,
...parseCookies(response.headers['set-cookie'])
}
}
if (response.headers.location) {
return getCookies(client)(response.headers.location, cookies)
} else {
return cookies
}
} else {
throw err
}
}
export const getSessionCookie = (fetch: Fetch) => async (): Promise<string> => {
const url = routes.loginCookie
const response = await fetch(url)
return response.headers.get('set-cookie') || ''
}
Loading

0 comments on commit 0e4acba

Please sign in to comment.