Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Panoptes JS: add lib-panoptes-js dev server, add experimental auth #6375

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
30 changes: 30 additions & 0 deletions packages/lib-panoptes-js/dev/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Panoptes.js dev app</title>
<style>
* {
box-sizing: border-box;
}

html, body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<h1>Panoptes.js dev app</h1>
<form
id="login-form"
method="POST"
>
<input type="text" name="login" />
<br>
<input type="password" name="password" />
<br>
<button type="submit">Login</button>
</form>
</body>
</html>
25 changes: 25 additions & 0 deletions packages/lib-panoptes-js/dev/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { signIn } from '@src/experimental-auth.js'

class App {
constructor () {
this.html = {
loginForm: document.getElementById('login-form')
}

this.html.loginForm.addEventListener('submit', this.loginForm_onSubmit.bind(this))
}

loginForm_onSubmit (e) {
const formData = new FormData(e.target)
signIn(formData.get('login'), formData.get('password'))

e.preventDefault()
return false
}
}

function init () {
new App()
}

window.onload = init
1 change: 1 addition & 0 deletions packages/lib-panoptes-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"directory": "packages/lib-panoptes-js"
},
"scripts": {
"dev": "BABEL_ENV=es6 webpack serve --config webpack.dev.js",
"lint": "zoo-standard --fix | snazzy",
"postversion": "npm publish",
"test": "NODE_ENV=test mocha --recursive --config ./test/.mocharc.json \"./src/**/*.spec.js\"",
Expand Down
2 changes: 1 addition & 1 deletion packages/lib-panoptes-js/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ panoptes.post(endpoint, data, authorization, query, host)
Create a project:

``` javascript
panoptes.get('/projects', { private: true }).then((response) => {
panoptes.post('/projects', { private: true }).then((response) => {
// Do something with the response
});
```
Expand Down
212 changes: 212 additions & 0 deletions packages/lib-panoptes-js/src/experimental-auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
/*
Experimental Auth Client
Based on PJC: https://github.com/zooniverse/panoptes-javascript-client/blob/8157794/lib/auth.js
*/

const globalStore = {
eventListeners: {},
_currentUserPromise: null,
//_bearerToken: '',
//_bearerTokenExpiration: NaN,
//_refreshToken: '',
//_tokenRefreshPromise: null,
}

async function checkCurrent (_store) {
const store = _store || globalStore
console.log('+++ experimental auth client: checkCurrent()')

if (!store._currentUserPromise) {
console.log('Checking current user')
store._currentUserPromise = _getUser(store)
await store._currentUserPromise
broadcastEvent('change', store._currentUserPromise, store)
}

return store._currentUserPromise
/*
Orignal PJC code

if (!this._currentUserPromise) {
console.log('Checking current user');
this._currentUserPromise = this._getUser();
await this._currentUserPromise;
this.emit('change', this._currentUserPromise);
}

return this._currentUserPromise;
*/
}

async function _getUser (_store) {
const store = _store || globalStore
console.log('+++ experimental auth client: getUser()')

try {
const token = await _getBearerToken(store)
return _getSession(store)
} catch (error) {
// Nobody's signed in. This isn't an error.
console.info('No current user')
return null
}
}

async function _getBearerToken (_store) {
const store = _store || globalStore
console.log('+++ experimental auth client: getBearerToken()')

// TODO
}

async function _getSession (_store) {
const store = _store || globalStore
console.log('+++ experimental auth client: _getSession()')

// TODO
}


/*
Adds event listener.
*/
function addEventListener (eventType, listener, _store) {
console.log('+++ experimental auth client: addEventListener()')

const store = _store || globalStore
if (!eventType || !listener) throw new Error('PanoptesJS auth.addEventListener(): requires event type (string) and listener (callback function).')

// Select array of listeners for specific event type. Create one if it doesn't already exist.
if (!store.eventListeners[eventType]) store.eventListeners[eventType] = []
const listenersForEventType = store.eventListeners[eventType]

// Add the callback function to the list of listeners, if it's not already on the list.
if (!listenersForEventType.find(l => l === listener)) {
listenersForEventType.push(listener)
} else {
console.log(`PanoptesJS auth.addEventListener(): listener already exists for event type '${eventType}'.`)
}
}

/*
Remove event listeners.
*/
function removeEventListener (eventType, listener, _store) {
const store = _store || globalStore
console.log('+++ experimental auth client: removeEventListener()')

// Check if the listener has already been registered for the event type.
if (!store.eventListeners[eventType] || !store.eventListeners[eventType]?.find(l => l === listener)) {
console.log(`PanoptesJS addEventListener: listener for event type '${eventType}' hasn't been registered.`)
return
}

// Remove the listener for that event type.
store.eventListeners[eventType] = store.eventListeners[eventType].filter(l => l !== listener)
return true
}

function broadcastEvent (eventType, args, _store) {
const store = _store || globalStore
store.eventListeners?.[eventType]?.forEach(listener => {
listener(args)
})
}

async function signIn (login, password, _store) {
// TODO
const store = _store || globalStore
console.log('+++ experimental auth client: signIn() ', login, password)


// Here's how to SIGN IN to Panoptes!

// Some general setup stuff.
const PANOPTES_HEADERS = { // the Panoptes API requires specific HTTP headers
'Content-Type': 'application/json',
'Accept': 'application/json',
}
// const login = 'janezooniverse'
// const password = 'bleepbloop'

// Step 1: get a CSRF token.
// - The CSRF token (or rather, the anti-cross-site request forgery token) is a
// unique, one-off, time-sensitive token. Kinda like the time-based OTPs
// provided by apps like the Google Authenticator.
// - This "authenticity token", as it will be later be called, prevents third
// parties from simply replaying the HTTPs-encoded sign-in request.
// - In our case, the CSRF token is provided Panoptes itself.
const request1 = new Request(`https://panoptes-staging.zooniverse.org/users/sign_in/?now=${Date.now()}`, {
method: 'GET',
headers: PANOPTES_HEADERS,
})
const response1 = await fetch(request1)
const csrfToken = response1?.headers.get('x-csrf-token') // The CSRF Token is in the response header
console.log('+++ Step 1: csrfToken received: ', csrfToken)
// Note: we don't actually care about the response body, which happens to be blank.

// Step 2: submit the login details.
// - IMPORTANT: at this point, Panoptes should be attaching (HTTP-only)
// cookies (_Panoptes_session and remember_user_token) to its responses.
// - These HTTP cookies identify requests as coming from us (or rather, from
// our particular session.
// - This is how request2 (submit username & password) and request3 (request
// bearer token for a logged in user) are magically linked and recognised
// as coming from the same person/session, even though request3 isn't
// providing any login data explicitly via the JavaScript code.
// - HTTP-only cookies can't be viewed or edited by JavaScript, as it happens.
// - HTTP-only cookies are automagically handled by the web browser and the
// server.
// - Our only control as front-end devs is to specify the fetch() of Request()'s
// `credentials` option to either "omit" (bad idea for us), "same-origin"
// (the default), or "include" (when you need things to work cross-origin)
// - ❗️ That `credentials: "include"` option is probably important if we need
// Panoptes.JS to work on non-*.zooniverse.org domains!

// TODO

// Note: old PJC doesn't actually care about the response body, which is the
// logged-in user's User resource. This is because PJC has a separate call for
// fetching the user resource, which is, uh, kinda extra work, but keeps the
// calls standardised I guess?

// Step 3: get the bearer token.

// TODO


/*
Original PJC code:

const user = await this.checkCurrent();
if (user) {
await this.signOut();
return this.signIn(credentials);
} else {
console.log('Signing in', credentials.login);
const token = await getCSRFToken(config.host)
const data = {
authenticity_token: token,
user: {
login: credentials.login,
password: credentials.password,
remember_me: true,
},
};

const signInRequest = this._makeSignInRequest(data);
this._currentUserPromise = signInRequest.catch(() => null);
await this._currentUserPromise;
this.emit('change', this._currentUserPromise);

return signInRequest;
}
*/
}

export {
signIn,
checkCurrent,
addEventListener,
removeEventListener,
}
shaunanoordin marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 2 additions & 0 deletions packages/lib-panoptes-js/src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const { config, env } = require('./config')
const auth = require('./auth')
const experimentalAuth = require('./experimental-auth')
const panoptes = require('./panoptes')
const talkAPI = require('./talkAPI')

Expand All @@ -15,6 +16,7 @@ module.exports = {
collections,
config,
env,
experimentalAuth,
media,
panoptes,
projects,
Expand Down
87 changes: 87 additions & 0 deletions packages/lib-panoptes-js/webpack.dev.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
const { execSync } = require('child_process')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
const webpack = require('webpack')

function gitCommit() {
try {
const commitHash = execSync('git describe --always').toString('utf8').trim()
return commitHash
} catch (error) {
console.log(error)
return 'Not a git repository.'
}
}

const EnvironmentWebpackPlugin = new webpack.EnvironmentPlugin({
COMMIT_ID: gitCommit(),
DEBUG: false,
NODE_ENV: 'development',
PANOPTES_ENV: 'staging'
})

const HtmlWebpackPluginConfig = new HtmlWebpackPlugin({
template: './dev/index.html',
filename: 'index.html',
inject: 'body'
})

module.exports = {
devServer: {
allowedHosts: [
'bs-local.com',
'localhost',
'.zooniverse.org'
],
server: 'https'
},
entry: [
'./dev/index.js'
],
mode: 'development',
resolve: {
alias: {
'@src': path.resolve(__dirname, 'src'),
},
fallback: {
fs: false,
// for markdown-it plugins
path: require.resolve("path-browserify"),
process: false,
url: false,
}
},
module: {
rules: [
{
test: /\.js?$/,
exclude: /node_modules/,
use: [{
loader: 'babel-loader',
options: { compact: false }
}]
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
output: {
path: path.resolve('build'),
filename: 'main.js',
library: '@zooniverse/user',
libraryTarget: 'umd',
umdNamedDefine: true
},
plugins: [
new webpack.ProvidePlugin({
process: 'process/browser',
}),
EnvironmentWebpackPlugin,
HtmlWebpackPluginConfig
]
}
Loading
Loading