From 6eed16547d9e9134c23d668fa3eba599946e3768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20=C3=96brink?= Date: Mon, 21 Dec 2020 09:51:51 +0100 Subject: [PATCH] Login token, logout method and set session cookie (#6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .github/.workflows/release.yml | 27 --------------- .releaserc | 3 ++ README.md | 60 +++++++++++++++++++++++++++------- lib/index.test.ts | 35 ++++++++++++++++++++ lib/index.ts | 37 +++++++++++++++++---- lib/login.test.ts | 15 +++++++-- lib/login.ts | 11 ++++--- lib/types.ts | 1 + package.json | 3 +- yarn.lock | 2 +- 10 files changed, 139 insertions(+), 55 deletions(-) delete mode 100644 .github/.workflows/release.yml create mode 100644 .releaserc create mode 100644 lib/index.test.ts diff --git a/.github/.workflows/release.yml b/.github/.workflows/release.yml deleted file mode 100644 index cc9410f79..000000000 --- a/.github/.workflows/release.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Release -on: - push: - branches: - - master -jobs: - release: - name: Release - runs-on: ubuntu-18.04 - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v1 - with: - node-version: 14 - - name: Install dependencies - run: yarn install --immutable --silent --non-interactive 2> >(grep -v warning 1>&2) - - name: Build - run: yarn build - - name: Release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: npx semantic-release \ No newline at end of file diff --git a/.releaserc b/.releaserc new file mode 100644 index 000000000..518f8dda1 --- /dev/null +++ b/.releaserc @@ -0,0 +1,3 @@ +{ + "branches": ["main"] +} \ No newline at end of file diff --git a/README.md b/README.md index ea0e205d9..3cb14b3e7 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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` +``` diff --git a/lib/index.test.ts b/lib/index.test.ts new file mode 100644 index 000000000..cd6d44886 --- /dev/null +++ b/lib/index.test.ts @@ -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 + 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) + }) + }) +}) diff --git a/lib/index.ts b/lib/index.ts index de24a8f28..3533592c0 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -7,14 +7,32 @@ import { } from './types' import { calendar, list } from './children' -class Api extends EventEmitter { +interface AsyncishFunction { (): void | Promise } + +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 { @@ -22,9 +40,7 @@ class Api extends EventEmitter { 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 } @@ -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) } diff --git a/lib/login.test.ts b/lib/login.test.ts index 6fb5fa0e2..02f95b2c6 100644 --- a/lib/login.test.ts +++ b/lib/login.test.ts @@ -2,7 +2,7 @@ import { login, checkStatus, getSessionCookie } from './login' import { Fetch, Headers, Response } from './types' describe('login', () => { - let fetch: jest.Mocked + let fetch: jest.Mocked let response: jest.Mocked let headers: jest.Mocked beforeEach(() => { @@ -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') diff --git a/lib/login.ts b/lib/login.ts index 54e544dea..3e8651fde 100644 --- a/lib/login.ts +++ b/lib/login.ts @@ -5,8 +5,8 @@ import { AuthTicket, Fetch } from './types' export const login = (fetch: Fetch) => async (personalNumber: string): Promise => { 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 } } /* @@ -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() } @@ -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 => { diff --git a/lib/types.ts b/lib/types.ts index aad7e5574..2ceb41f87 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -20,6 +20,7 @@ export interface Fetch { export interface AuthTicket { order: string + token: string } /** diff --git a/package.json b/package.json index 04b8d7ee9..ee6d0ad5f 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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" }, diff --git a/yarn.lock b/yarn.lock index 9ebfb916a..f7ca5e3ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==