kemal_jwt_auth_companion
Version:
A client-side companion to make autheticating with a Kemal server a breeze.
124 lines (116 loc) • 4.48 kB
JavaScript
// get the fetch capability whether we're in the browser or node
const fetch = window? window.fetch
: require('node-fetch')
export class InvalidToken extends Error {
constructor(token_value) {
super(`got invalid token value "${token_value}"`)
}
}
export class FailedLogin extends Error {
constructor(user, response) {
let errors
if( typeof response === 'string' ||
response instanceof String )
errors = response
if( isNonEmptyArray(response.errors) )
{
errors = response.errors.join("; ")
}
super( 'failed to log in "' +
user +
'": status ' +
response.statusCode +
errors? 'errors: ' + errors
: '')
}
}
export class NoLoginInfo extends Error {
constructor() {
super("can't log in with no authentication information -- please specify a username or a password!")
}
}
let token
let handleError = console.error
// Returns the given number of minutes as seconds
const minutes = count => 60 * count
// Returns the given number of hours as seconds
const hours = count => 60 * minutes(count)
// Returns the given number of days as seconds
const days = count => 24 * hours(count)
// Parse the document.cookie response into an indexable mapping.
const cookieNamed = (name) => {
let key, value
document
.cookie
.split(';')
.find(cookie => {
[key, value] = cookie.trim().split('=', 2)
if( key === name && value )
return true
value = null
})
return value
}
// True if the given value is an array whose size is truthy (not 0)
const isNonEmptyArray = (potArr) => Array.isArray(potArr) && potArr.length
// store the given value as a new authentication token.
const keep = (newToken) => {
if( !newToken ) throw new InvalidToken(newToken)
// token is a global variable
token = newToken
document.cookie = "auth=" + newToken + ";samesite=strict;secure;max-age=" + days(7)
return newToken
}
export default function (host, signInEndpoint = '/sign_in') {
// The next few lines are a big guard against invalid values. Nothing but a
// string gets through, and the empty string gets assigned to a reasonable
// default before checking its 0 index (which the empty string does not have)
if (!host || !(typeof host === 'string' || host instanceof String))
host = '/'
if (!signInEndpoint || !(typeof signInEndpoint === 'string' || signInEndpoint instanceof String))
signInEndpoint = '/'
if (!(signInEndpoint[0] == '/')) signInEndpoint = '/' + signInEndpoint
if (host[host.length - 1] != '/') host += '/'
const SIGN_IN_ADDR = host.slice(0, host.length - 1) + signInEndpoint
// Log in with a given username and password.
//
// This stores the login token in the document's cookie and returns it. Either
// the username or password can be left off if the API supports it.
const login = (username, password) => new Promise((resolve, reject) => {
let data = {}
if( username ) data['name'] = username
if( password ) data['auth'] = password
if( Object.keys(data).length === 0 ) throw new NoLoginInfo
fetch(SIGN_IN_ADDR, {method: 'POST', body: JSON.stringify(data)})
.then(response => {
if( response.ok ) return response.json()
return response.json()
.then( ({errors}) => reject(new FailedLogin(username, errors)))
.catch(e => { console.error(e); return response.text()})
.then(text => reject(new FailedLogin(username, text)))
.catch(() => reject(new FailedLogin(username, `status code ${response.status}`)))})
.then(json => {
if( !json ) return reject(`got json response '${json}'`)
const { errors, token } = json
if( isNonEmptyArray(errors) ) errors.forEach(e => handleError(e))
resolve(keep(token)) })
.catch(handleError)
})
// This function mirrors the functionality of the normal `fetch()` function,
// but adds the authentication token to the header before calling fetch.
const authFetch = (input, init) => {
if( input.startsWith(host) || input.startsWith('/') ) {
init = init || {}
init.headers = init.headers || {}
init.headers["X-Token"] = token || cookieNamed("auth")
}
return fetch(input, init)
}
const authXHR = () => {
const out = new XMLHttpRequest()
out.setRequestHeader('X-Token', token || cookieNamed('auth'))
return out
}
return {login, authFetch, authXHR}
}
export const onError = (action) => handleError = action