Skip to content

Commit

Permalink
Login token, logout method and set session cookie (#6)
Browse files Browse the repository at this point in the history
* feat: 🎸 Exposes token on login status check

✅ Closes: #3

* feat: 🎸 Logout and set session cookie

Added logout method and logout event. Also added method for setting
sessionCookie from memory

✅ Closes: #4, #5
  • Loading branch information
JohanObrink authored Dec 21, 2020
1 parent dd1cdbd commit 6eed165
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 55 deletions.
27 changes: 0 additions & 27 deletions .github/.workflows/release.yml

This file was deleted.

3 changes: 3 additions & 0 deletions .releaserc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"branches": ["main"]
}
60 changes: 48 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,67 @@

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
## Installing

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

### Calling
## Calling

### Import and init

Since fetch and cookies behave distinctly different in node, react-native and the browser,
the concrete implementation of fetch and cookie handler must be injected.

#### Import and init
#### react-native

```javascript
import init from "@skolplattformen/embedded-api";
import CookieManager from "@react-native-community/cookies";

const api = init(fetch);
const api = init(fetch, () => CookieManager.clearAll());
```

#### Login
#### node

```javascript
api.on("login", () => {
// keep going
import init from "@skolplattformen/embedded-api";
import nodeFetch from "node-fetch";
import fetchCookie from "fetch-cookie/node-fetch";
import { CookieJar } from "tough-cookie";

const cookieJar = new CookieJar();
const fetch = fetchCookie(nodeFetch, cookieJar);

const api = init(fetch, () => cookieJar.removeAllCookies());
```

### Login / logout

```javascript
api.on("login", async () => {
// do stuff
console.log(api.isLoggedIn) // true
await api.logout()
});
api.on('logout', () => {
// handle logout
console.log(api.isLoggedIn) // false
}

const loginStatus = await api.login("YYYYMMDDXXXX");
window.open(`https://app.bankid.com/?autostarttoken=${loginStatus.token}&redirect=null`);
window.open(
`https://app.bankid.com/?autostarttoken=${loginStatus.token}&redirect=null`
);

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", () =>
loginStatus.on("OK", () =>
console.log("BankID sign successful. Session will be established.")
);
```
#### Loading data
### Loading data
```javascript
// List children
Expand All @@ -45,3 +71,13 @@ const children = await api.getChildren();
// Get calendar
const calendar = await api.getCalendar(children[0].id);
```
### Setting session cookie
It is possible to resurrect a logged in session by manually setting the session cookie.
```javascript
const sessionCookie = "some value";

api.setSessionCookie(sessionCookie); // will trigger `on('login')` event and set `.isLoggedIn = true`
```
35 changes: 35 additions & 0 deletions lib/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import init, { Api } from './'
import { list } from './children'
import { Fetch } from './types'

jest.mock('./login', () => {
login: jest.fn()
})

describe('api', () => {
let fetch: jest.Mocked<Fetch>
let clearCookies: jest.Mock
let api: Api
beforeEach(() => {
fetch = jest.fn()
clearCookies = jest.fn()
api = init(fetch, clearCookies)
})
describe('#logout', () => {
it('clears cookies', async () => {
await api.logout()
expect(clearCookies).toHaveBeenCalled()
})
it('emits logout event', async () => {
const listener = jest.fn()
api.on('logout', listener)
await api.logout()
expect(listener).toHaveBeenCalled()
})
it('sets .isLoggedIn', async () => {
api.isLoggedIn = true
await api.logout()
expect(api.isLoggedIn).toBe(false)
})
})
})
37 changes: 30 additions & 7 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,40 @@ import {
} from './types'
import { calendar, list } from './children'

class Api extends EventEmitter {
interface AsyncishFunction { (): void | Promise<void> }

export class Api extends EventEmitter {
private fetch: Fetch

private session?: RequestInit

constructor(fetch: Fetch) {
private clearCookies: AsyncishFunction

public isLoggedIn: boolean = false

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

setSessionCookie(cookie: string) {
this.session = {
headers: {
Cookie: cookie,
},
}

this.isLoggedIn = true
this.emit('login')
}

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')
this.setSessionCookie(sessionCookie)
})
return loginStatus
}
Expand All @@ -38,8 +54,15 @@ class Api extends EventEmitter {
const data = await calendar(this.fetch, this.session)(childId)
return data
}

async logout() {
this.session = undefined
await this.clearCookies()
this.isLoggedIn = false
this.emit('logout')
}
}

export default function init(fetch: Fetch) {
return new Api(fetch)
export default function init(fetch: Fetch, clearCookies: AsyncishFunction): Api {
return new Api(fetch, clearCookies)
}
15 changes: 12 additions & 3 deletions lib/login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { login, checkStatus, getSessionCookie } from './login'
import { Fetch, Headers, Response } from './types'

describe('login', () => {
let fetch: jest.Mocked<Fetch >
let fetch: jest.Mocked<Fetch>
let response: jest.Mocked<Response>
let headers: jest.Mocked<Headers>
beforeEach(() => {
Expand All @@ -25,12 +25,21 @@ describe('login', () => {
response.json.mockResolvedValue(data)
const result = await login(fetch)(personalNumber)

expect(result).toEqual({ order: '5fe57e4c-9ad2-4b52-b794-48adef2f6663' })
expect(result).toEqual(data)
})
})
describe('#checkStatus', () => {
const ticket = { order: '5fe57e4c-9ad2-4b52-b794-48adef2f6663' }
const ticket = {
token: '9462cf77-bde9-4029-bb41-e599f3094613',
order: '5fe57e4c-9ad2-4b52-b794-48adef2f6663',
}
it('exposes token', () => {
response.text.mockResolvedValue('PENDING')

const check = checkStatus(fetch)(ticket)
expect(check.token).toEqual(ticket.token)
check.cancel()
})
it('emits PENDING', (done) => {
response.text.mockResolvedValue('PENDING')

Expand Down
11 changes: 7 additions & 4 deletions lib/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { AuthTicket, Fetch } from './types'
export const login = (fetch: Fetch) => async (personalNumber: string): Promise<AuthTicket> => {
const url = routes.login(personalNumber)
const response = await fetch(url)
const { order } = await response.json()
return { order }
const { order, token } = await response.json()
return { order, token }
}

/*
Expand All @@ -19,16 +19,19 @@ export enum LoginEvent {
*/

export class LoginStatus extends EventEmitter {
public token: string

private url: string

private fetch: Fetch

private cancelled: boolean = false

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

Expand All @@ -48,7 +51,7 @@ export class LoginStatus extends EventEmitter {

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

export const getSessionCookie = (fetch: Fetch) => async (): Promise<string> => {
Expand Down
1 change: 1 addition & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface Fetch {

export interface AuthTicket {
order: string
token: string
}

/**
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@skolplattformen/embedded-api",
"version": "0.0.0",
"version": "0.1.0",
"description": "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).",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand All @@ -27,6 +27,7 @@
"fetch-cookie": "^0.11.0",
"jest": "^26.6.3",
"node-fetch": "^2.6.1",
"tough-cookie": "^4.0.0",
"ts-jest": "^26.4.4",
"typescript": "^4.1.3"
},
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4243,7 +4243,7 @@ tough-cookie@^2.3.3, tough-cookie@~2.5.0:
psl "^1.1.28"
punycode "^2.1.1"

"tough-cookie@^2.3.3 || ^3.0.1 || ^4.0.0":
"tough-cookie@^2.3.3 || ^3.0.1 || ^4.0.0", tough-cookie@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4"
integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==
Expand Down

0 comments on commit 6eed165

Please sign in to comment.