mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 10:27:09 +00:00
Merge branch '304-refresh-authentication-status-for-connected-users' into 'devel'
Resolve "Refresh authentication status for connected users" Closes #304 See merge request wgp/dougal/software!56
This commit is contained in:
@@ -82,6 +82,8 @@ export default {
|
||||
if (event.channel == "project" && event.payload?.schema == "public") {
|
||||
// Projects changed in some way or another
|
||||
await this.refreshProjects();
|
||||
} else if (event.channel == ".jwt" && event.payload?.token) {
|
||||
await this.setCredentials({token: event.payload?.token});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,6 +7,8 @@ import vueDebounce from 'vue-debounce'
|
||||
import { mapMutations } from 'vuex';
|
||||
import { markdown, markdownInline } from './lib/markdown';
|
||||
import { geometryAsString } from './lib/utils';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
@@ -14,7 +16,7 @@ Vue.use(vueDebounce);
|
||||
|
||||
Vue.filter('markdown', markdown);
|
||||
Vue.filter('markdownInline', markdownInline);
|
||||
Vue.filter('position', (str, item, opts) =>
|
||||
Vue.filter('position', (str, item, opts) =>
|
||||
str
|
||||
.replace(/@POS(ITION)?@/g, geometryAsString(item, opts) || "(position unknown)")
|
||||
.replace(/@DMS@/g, geometryAsString(item, {...opts, dms:true}) || "(position unknown)")
|
||||
@@ -32,10 +34,16 @@ new Vue({
|
||||
user: null,
|
||||
|
||||
wsUrl: "/ws",
|
||||
ws: null
|
||||
ws: null,
|
||||
wsCredentialsCheckInterval: 300*1000, // ms
|
||||
wsCredentialsCheckTimer: null
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters(["jwt"])
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
markdown (value) {
|
||||
@@ -85,6 +93,17 @@ new Vue({
|
||||
this.setServerConnectionState(false);
|
||||
});
|
||||
|
||||
if (this.wsCredentialsCheckTimer) {
|
||||
clearInterval(this.wsCredentialsCheckTimer);
|
||||
this.wsCredentialsCheckTimer = null;
|
||||
}
|
||||
|
||||
this.wsCredentialsCheckTimer = setInterval( () => {
|
||||
this.ws.send(JSON.stringify({
|
||||
jwt: this.jwt
|
||||
}));
|
||||
}, this.wsCredentialsCheckInterval);
|
||||
|
||||
},
|
||||
|
||||
...mapMutations(['setServerEvent', 'setServerConnectionState'])
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
async function api ({state, commit, dispatch}, [resource, init = {}, cb]) {
|
||||
async function api ({state, getters, commit, dispatch}, [resource, init = {}, cb]) {
|
||||
try {
|
||||
commit("queueRequest");
|
||||
if (init && init.hasOwnProperty("body")) {
|
||||
@@ -9,9 +9,16 @@ async function api ({state, commit, dispatch}, [resource, init = {}, cb]) {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
if (typeof init.body != "string") {
|
||||
init.body = JSON.stringify(init.body);
|
||||
}
|
||||
}
|
||||
if (!init.headers) {
|
||||
init.headers = {};
|
||||
}
|
||||
// We also send Authorization: Bearer …
|
||||
if (getters.jwt) {
|
||||
init.headers["Authorization"] = "Bearer "+getters.jwt;
|
||||
}
|
||||
if (typeof init.body != "string") {
|
||||
init.body = JSON.stringify(init.body);
|
||||
}
|
||||
const url = /^https?:\/\//i.test(resource) ? resource : (state.apiUrl + resource);
|
||||
const res = await fetch(url, init);
|
||||
|
||||
@@ -11,7 +11,7 @@ async function login ({commit, dispatch}, loginRequest) {
|
||||
}
|
||||
const res = await dispatch('api', [url, init]);
|
||||
if (res && res.ok) {
|
||||
await dispatch('setCredentials', true);
|
||||
await dispatch('setCredentials', {force: true});
|
||||
await dispatch('loadUserPreferences');
|
||||
}
|
||||
}
|
||||
@@ -34,12 +34,12 @@ function cookieChanged (cookie) {
|
||||
return browserCookie != cookie;
|
||||
}
|
||||
|
||||
function setCredentials ({state, commit, getters, dispatch}, force = false) {
|
||||
if (cookieChanged(state.cookie) || force) {
|
||||
function setCredentials ({state, commit, getters, dispatch}, {force, token} = {}) {
|
||||
if (token || force || cookieChanged(state.cookie)) {
|
||||
try {
|
||||
const cookie = browserCookie();
|
||||
const decoded = cookie ? jwt_decode(cookie.split("=")[1]) : null;
|
||||
commit('setCookie', cookie);
|
||||
const decoded = (token ?? cookie) ? jwt_decode(token ?? cookie.split("=")[1]) : null;
|
||||
commit('setCookie', (cookie ?? (token && ("JWT="+token))) || undefined);
|
||||
commit('setUser', decoded);
|
||||
} catch (err) {
|
||||
if (err.name == "InvalidTokenError") {
|
||||
@@ -54,7 +54,7 @@ function setCredentials ({state, commit, getters, dispatch}, force = false) {
|
||||
|
||||
/**
|
||||
* Save user preferences to localStorage and store.
|
||||
*
|
||||
*
|
||||
* User preferences are identified by a key that gets
|
||||
* prefixed with the user name and role. The value can
|
||||
* be anything that JSON.stringify can parse.
|
||||
|
||||
@@ -3,6 +3,12 @@ function user (state) {
|
||||
return state.user;
|
||||
}
|
||||
|
||||
function jwt (state) {
|
||||
if (state.cookie?.startsWith("JWT=")) {
|
||||
return state.cookie.substring(4);
|
||||
}
|
||||
}
|
||||
|
||||
function writeaccess (state) {
|
||||
return state.user && ["user", "admin"].includes(state.user.role);
|
||||
}
|
||||
@@ -15,4 +21,4 @@ function preferences (state) {
|
||||
return state.preferences;
|
||||
}
|
||||
|
||||
export default { user, writeaccess, adminaccess, preferences };
|
||||
export default { user, jwt, writeaccess, adminaccess, preferences };
|
||||
|
||||
@@ -1,28 +1,21 @@
|
||||
const crypto = require('crypto');
|
||||
const cfg = require('../../../lib/config');
|
||||
const jwt = require('../../../lib/jwt');
|
||||
|
||||
async function login (req, res, next) {
|
||||
if (req.body) {
|
||||
const {user, password} = req.body;
|
||||
if (user && password) {
|
||||
const hash = crypto
|
||||
.pbkdf2Sync(password, 'Dougal'+user, 10712, 48, 'sha512')
|
||||
.toString('base64');
|
||||
for (const credentials of cfg._("global.users.login.user") || []) {
|
||||
if (credentials.name == user && credentials.hash == hash) {
|
||||
const payload = Object.assign({}, credentials);
|
||||
delete payload.hash;
|
||||
jwt.issue(payload, req, res);
|
||||
res.status(204).send();
|
||||
next();
|
||||
return;
|
||||
}
|
||||
}
|
||||
const payload = jwt.checkValidCredentials({user, password});
|
||||
if (payload) {
|
||||
jwt.issue(payload, req, res);
|
||||
res.status(204).send();
|
||||
next();
|
||||
return;
|
||||
} else {
|
||||
next({status: 401, message: "Unauthorised"});
|
||||
}
|
||||
} else {
|
||||
next({status: 400, message: "Bad request"});
|
||||
}
|
||||
next({status: 400, message: "Bad request"});
|
||||
}
|
||||
|
||||
module.exports = login;
|
||||
|
||||
@@ -1,9 +1,37 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const crypto = require('crypto');
|
||||
const JWT = require('jsonwebtoken');
|
||||
const cfg = require('./config');
|
||||
|
||||
|
||||
function checkValidCredentials ({user, password, jwt}) {
|
||||
if (jwt) {
|
||||
try {
|
||||
const decoded = JWT.verify(jwt, cfg.jwt.secret, {maxAge: "1d"});
|
||||
delete decoded.iat;
|
||||
delete decoded.exp;
|
||||
return decoded;
|
||||
} catch (err) {
|
||||
console.warn("Failed to verify credentials for", jwt);
|
||||
console.warn(err);
|
||||
return; // Invalid JWT
|
||||
}
|
||||
} else if (user && password) {
|
||||
const hash = crypto
|
||||
.pbkdf2Sync(password, 'Dougal'+user, 10712, 48, 'sha512')
|
||||
.toString('base64');
|
||||
for (const credentials of cfg._("global.users.login.user") || []) {
|
||||
if (credentials.name == user && credentials.hash == hash) {
|
||||
const payload = {...credentials};
|
||||
delete payload.hash;
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function issue (payload, req, res) {
|
||||
|
||||
const token = jwt.sign(payload, cfg.jwt.secret, cfg.jwt.options);
|
||||
const token = JWT.sign(payload, cfg.jwt.secret, cfg.jwt.options);
|
||||
|
||||
if (req) {
|
||||
req.user = payload;
|
||||
@@ -17,5 +45,6 @@ function issue (payload, req, res) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkValidCredentials,
|
||||
issue
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ const ws = require('ws');
|
||||
const URL = require('url');
|
||||
const { listen } = require('../lib/db/notify');
|
||||
const channels = require('../lib/db/channels');
|
||||
const jwt = require('../lib/jwt');
|
||||
|
||||
function start (server, pingInterval=30000) {
|
||||
|
||||
@@ -9,7 +10,31 @@ function start (server, pingInterval=30000) {
|
||||
wsServer.on('connection', socket => {
|
||||
socket.alive = true;
|
||||
socket.on('pong', function () { this.alive = true; })
|
||||
socket.on('message', message => console.log(message));
|
||||
socket.on('message', message => {
|
||||
// console.log("Websocket message:");
|
||||
// console.log(message);
|
||||
try {
|
||||
const payload = JSON.parse(message);
|
||||
if (payload?.jwt) {
|
||||
// console.log("Refresh JWT token", payload);
|
||||
const decoded = jwt.checkValidCredentials({jwt: payload.jwt});
|
||||
// console.log("Decoded", decoded);
|
||||
if (decoded) {
|
||||
delete decoded.exp;
|
||||
const token = jwt.issue(decoded);
|
||||
socket.send(JSON.stringify({
|
||||
channel: ".jwt",
|
||||
payload: {
|
||||
token
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("Websocket message decoding failed", err);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
|
||||
Reference in New Issue
Block a user