mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 09:07:09 +00:00
Compare commits
22 Commits
v1
...
61-user-au
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80451796e1 | ||
|
|
141d5805ae | ||
|
|
250ffe243d | ||
|
|
59aaacbeee | ||
|
|
3c86981dc6 | ||
|
|
5594b6863c | ||
|
|
7201c29df5 | ||
|
|
947736e8c1 | ||
|
|
d782a30e90 | ||
|
|
987dbb7700 | ||
|
|
cdd007ce88 | ||
|
|
a38066ec82 | ||
|
|
2aca34e488 | ||
|
|
324306a77d | ||
|
|
ab8a66bdcf | ||
|
|
b3f393a6f1 | ||
|
|
1ee886db63 | ||
|
|
fc9450434c | ||
|
|
00f4fcf292 | ||
|
|
0512ac2c3c | ||
|
|
dd32982cbe | ||
|
|
a3bfb73937 |
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"jwt": {
|
||||
"secret": ""
|
||||
"secret": "",
|
||||
"options": {
|
||||
"expiresIn": 1800
|
||||
}
|
||||
},
|
||||
"db": {
|
||||
"user": "postgres",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import DougalNavigation from './components/navigation';
|
||||
import DougalFooter from './components/footer';
|
||||
|
||||
@@ -61,9 +62,14 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions(["setCredentials"])
|
||||
},
|
||||
|
||||
mounted () {
|
||||
// Local Storage values are always strings
|
||||
this.$vuetify.theme.dark = localStorage.getItem("darkTheme") == "true";
|
||||
this.setCredentials()
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -15,14 +15,39 @@
|
||||
<v-breadcrumbs :items="path"></v-breadcrumbs>
|
||||
|
||||
<template v-if="$route.name != 'Login'">
|
||||
<v-btn text link to="/login" v-if="!$root.user">Log in</v-btn>
|
||||
<template v-else>
|
||||
<v-btn title="Edit profile" disabled>
|
||||
{{$root.user.name}}
|
||||
</v-btn>
|
||||
<v-btn class="ml-2" title="Log out" link to="/?logout=1">
|
||||
<v-icon>mdi-logout</v-icon>
|
||||
<v-btn text link to="/login" v-if="!user && !loading">Log in</v-btn>
|
||||
<template v-else-if="user">
|
||||
|
||||
<v-menu
|
||||
offset-y
|
||||
>
|
||||
<template v-slot:activator="{on, attrs}">
|
||||
<v-avatar :color="user.colour || 'primary'" :title="`${user.name} (${user.role})`" v-bind="attrs" v-on="on">
|
||||
<span class="white--text">{{user.name.slice(0, 5)}}</span>
|
||||
</v-avatar>
|
||||
</template>
|
||||
|
||||
<v-list dense>
|
||||
<v-list-item link to="/login" v-if="user.autologin">
|
||||
<v-list-item-icon><v-icon small>mdi-login</v-icon></v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Log in as a different user</v-list-item-title>
|
||||
<v-list-item-subtitle>Autologin from {{user.ip}}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item link to="/logout" v-else>
|
||||
<v-list-item-icon><v-icon small>mdi-logout</v-icon></v-list-item-icon>
|
||||
<v-list-item-title>Log out</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
</v-menu>
|
||||
|
||||
<!--
|
||||
<v-btn small text class="ml-2" title="Log out" link to="/?logout=1">
|
||||
<v-icon small>mdi-logout</v-icon>
|
||||
</v-btn>
|
||||
-->
|
||||
</template>
|
||||
</template>
|
||||
<template v-slot:extension v-if="$route.matched.find(i => i.name == 'Project')">
|
||||
@@ -35,6 +60,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'DougalNavigation',
|
||||
@@ -58,7 +84,9 @@ export default {
|
||||
computed: {
|
||||
tab () {
|
||||
return this.tabs.findIndex(t => t.href == this.$route.path.split(/\/+/)[3]);
|
||||
}
|
||||
},
|
||||
|
||||
...mapGetters(['user', 'loading'])
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import Home from '../views/Home.vue'
|
||||
import Login from '../views/Login.vue'
|
||||
import Logout from '../views/Logout.vue'
|
||||
import Project from '../views/Project.vue'
|
||||
import ProjectList from '../views/ProjectList.vue'
|
||||
import ProjectSummary from '../views/ProjectSummary.vue'
|
||||
@@ -31,6 +33,27 @@ Vue.use(VueRouter)
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
|
||||
},
|
||||
{
|
||||
pathToRegexpOptions: { strict: true },
|
||||
path: "/login",
|
||||
redirect: "/login/"
|
||||
},
|
||||
{
|
||||
pathToRegexpOptions: { strict: true },
|
||||
name: "Login",
|
||||
path: "/login/",
|
||||
component: Login,
|
||||
meta: {
|
||||
// breadcrumbs: [
|
||||
// { text: "Projects", href: "/projects", disabled: true }
|
||||
// ]
|
||||
}
|
||||
},
|
||||
{
|
||||
// pathToRegexpOptions: { strict: true },
|
||||
path: "/logout",
|
||||
component: Logout,
|
||||
},
|
||||
{
|
||||
pathToRegexpOptions: { strict: true },
|
||||
path: "/projects",
|
||||
|
||||
@@ -2,6 +2,7 @@ import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
import api from './modules/api'
|
||||
import user from './modules/user'
|
||||
import snack from './modules/snack'
|
||||
import project from './modules/project'
|
||||
import notify from './modules/notify'
|
||||
@@ -11,6 +12,7 @@ Vue.use(Vuex)
|
||||
export default new Vuex.Store({
|
||||
modules: {
|
||||
api,
|
||||
user,
|
||||
snack,
|
||||
project,
|
||||
notify
|
||||
|
||||
@@ -18,6 +18,9 @@ async function api ({state, commit, dispatch}, [resource, init = {}, cb]) {
|
||||
cb(null, res);
|
||||
}
|
||||
if (res.ok) {
|
||||
|
||||
await dispatch('setCredentials');
|
||||
|
||||
try {
|
||||
return await res.json();
|
||||
} catch (err) {
|
||||
|
||||
50
lib/www/client/source/src/store/modules/user/actions.js
Normal file
50
lib/www/client/source/src/store/modules/user/actions.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import jwt_decode from 'jwt-decode';
|
||||
|
||||
async function login ({commit, dispatch}, loginRequest) {
|
||||
const url = "/login";
|
||||
const init = {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: loginRequest
|
||||
}
|
||||
const res = await dispatch('api', [url, init]);
|
||||
if (res && res.ok) {
|
||||
dispatch('setCredentials', true);
|
||||
}
|
||||
}
|
||||
|
||||
async function logout ({commit, dispatch}) {
|
||||
commit('setCookie', null);
|
||||
commit('setUser', null);
|
||||
await dispatch('api', ["/logout"]);
|
||||
// Should delete JWT cookie
|
||||
}
|
||||
|
||||
function browserCookie (state) {
|
||||
return document.cookie.split(/; */).find(i => /^JWT=.+/.test(i));
|
||||
}
|
||||
|
||||
function cookieChanged (cookie) {
|
||||
return browserCookie != cookie;
|
||||
}
|
||||
|
||||
function setCredentials ({state, commit, getters, dispatch}, force = false) {
|
||||
if (cookieChanged(state.cookie) || force) {
|
||||
try {
|
||||
const cookie = browserCookie();
|
||||
const decoded = cookie ? jwt_decode(cookie.split("=")[1]) : null;
|
||||
commit('setCookie', cookie);
|
||||
commit('setUser', decoded);
|
||||
} catch (err) {
|
||||
if (err.name == "InvalidTokenError") {
|
||||
console.warn("Failed to decode", browserCookie());
|
||||
} else {
|
||||
console.error("setCredentials", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default { login, logout, setCredentials };
|
||||
6
lib/www/client/source/src/store/modules/user/getters.js
Normal file
6
lib/www/client/source/src/store/modules/user/getters.js
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
function user (state) {
|
||||
return state.user;
|
||||
}
|
||||
|
||||
export default { user };
|
||||
6
lib/www/client/source/src/store/modules/user/index.js
Normal file
6
lib/www/client/source/src/store/modules/user/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import state from './state'
|
||||
import getters from './getters'
|
||||
import actions from './actions'
|
||||
import mutations from './mutations'
|
||||
|
||||
export default { state, getters, actions, mutations };
|
||||
10
lib/www/client/source/src/store/modules/user/mutations.js
Normal file
10
lib/www/client/source/src/store/modules/user/mutations.js
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
function setCookie (state, cookie) {
|
||||
state.cookie = cookie;
|
||||
}
|
||||
|
||||
function setUser (state, user) {
|
||||
state.user = user;
|
||||
}
|
||||
|
||||
export default { setCookie, setUser };
|
||||
6
lib/www/client/source/src/store/modules/user/state.js
Normal file
6
lib/www/client/source/src/store/modules/user/state.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const state = () => ({
|
||||
cookie: null,
|
||||
user: null,
|
||||
});
|
||||
|
||||
export default state;
|
||||
98
lib/www/client/source/src/views/Login.vue
Normal file
98
lib/www/client/source/src/views/Login.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-form :disabled="loading">
|
||||
<v-card class="mx-auto" max-width="600" tile>
|
||||
<v-card-title style="word-break: normal;">Login</v-card-title>
|
||||
<v-card-text>
|
||||
|
||||
<v-text-field
|
||||
v-model="credentials.user"
|
||||
label="User"
|
||||
required
|
||||
>
|
||||
</v-text-field>
|
||||
<v-text-field
|
||||
v-model="credentials.password"
|
||||
type="password"
|
||||
label="Password"
|
||||
required
|
||||
>
|
||||
</v-text-field>
|
||||
|
||||
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
|
||||
<v-btn
|
||||
:disabled="!valid"
|
||||
color="success"
|
||||
class="mr-4"
|
||||
@click="submit"
|
||||
>Login
|
||||
</v-btn>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="warning"
|
||||
class="mr-4"
|
||||
@click="reset"
|
||||
>Reset
|
||||
</v-btn>
|
||||
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-form>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
|
||||
components: {
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
credentials: {
|
||||
user: null,
|
||||
password: null
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
valid () {
|
||||
return this.credentials.user !== null && this.credentials.password !== null;
|
||||
},
|
||||
|
||||
...mapGetters(['loading', 'user'])
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
async submit () {
|
||||
|
||||
await this.logout();
|
||||
await this.login(this.credentials);
|
||||
|
||||
if (this.user && !this.user.autologin) {
|
||||
this.$router.replace("/");
|
||||
} else {
|
||||
this.showSnack(["Bad login", "warning"]);
|
||||
}
|
||||
},
|
||||
|
||||
reset () {
|
||||
this.credentials = {user: null, password: null};
|
||||
},
|
||||
|
||||
...mapActions(["login", "logout", "showSnack"])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
19
lib/www/client/source/src/views/Logout.vue
Normal file
19
lib/www/client/source/src/views/Logout.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'Logout',
|
||||
|
||||
methods: {
|
||||
...mapActions(["logout"])
|
||||
},
|
||||
|
||||
async created () {
|
||||
await this.logout();
|
||||
this.$router.replace("/");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -62,8 +62,22 @@ const allMeta = (key, value) => {
|
||||
return { all: [ meta(key, value) ] };
|
||||
};
|
||||
|
||||
// These routes do not require authentication
|
||||
app.map({
|
||||
'*': { all: [ meta() ] }, // Create the req.meta object
|
||||
'/login': {
|
||||
post: [ mw.user.login ]
|
||||
},
|
||||
'/logout': {
|
||||
get: [ mw.user.logout ],
|
||||
post: [ mw.user.logout ]
|
||||
}
|
||||
});
|
||||
|
||||
app.use(mw.auth.authentify);
|
||||
|
||||
// We must be authenticated before we can access these
|
||||
app.map({
|
||||
'/project': {
|
||||
get: [ mw.project.list ], // Get list of projects
|
||||
},
|
||||
@@ -93,7 +107,7 @@ app.map({
|
||||
},
|
||||
'/project/:project/line/:line': {
|
||||
// get: [ mw.line.get ],
|
||||
patch: [ mw.line.patch ],
|
||||
patch: [ mw.auth.access.write, mw.line.patch ],
|
||||
},
|
||||
|
||||
'/project/:project/sequence/': {
|
||||
@@ -101,30 +115,30 @@ app.map({
|
||||
},
|
||||
'/project/:project/sequence/:sequence': {
|
||||
// get: [ mw.sequence.get ],
|
||||
patch: [ mw.sequence.patch ],
|
||||
patch: [ mw.auth.access.write, mw.sequence.patch ],
|
||||
},
|
||||
|
||||
'/project/:project/plan/': {
|
||||
get: [ mw.plan.list ],
|
||||
put: [ mw.plan.put ],
|
||||
post: [ mw.plan.post ]
|
||||
put: [ mw.auth.access.write, mw.plan.put ],
|
||||
post: [ mw.auth.access.write, mw.plan.post ]
|
||||
},
|
||||
'/project/:project/plan/:sequence': {
|
||||
// get: [ mw.plan.get ],
|
||||
patch: [ mw.plan.patch ],
|
||||
delete: [ mw.plan.delete ]
|
||||
patch: [ mw.auth.access.write, mw.plan.patch ],
|
||||
delete: [ mw.auth.access.write, mw.plan.delete ]
|
||||
},
|
||||
//
|
||||
'/project/:project/event/': {
|
||||
get: [ mw.event.cache.get, mw.event.list, mw.event.cache.save ],
|
||||
post: [ mw.event.post ],
|
||||
put: [ mw.event.put ],
|
||||
delete: [ mw.event.delete ],
|
||||
post: [ mw.auth.access.write, mw.event.post ],
|
||||
put: [ mw.auth.access.write, mw.event.put ],
|
||||
delete: [ mw.auth.access.write, mw.event.delete ],
|
||||
':type/': {
|
||||
':id/': {
|
||||
// get: [ mw.event.get ],
|
||||
put: [ mw.event.put ],
|
||||
delete: [ mw.event.delete ]
|
||||
put: [ mw.auth.access.write, mw.event.put ],
|
||||
delete: [mw.auth.access.write, mw.event.delete ]
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -134,7 +148,7 @@ app.map({
|
||||
},
|
||||
'/project/:project/configuration/:path(*)?': {
|
||||
get: [ mw.configuration.get ],
|
||||
// post: [ mw.label.post ],
|
||||
// post: [ mw.auth.access.admin, mw.label.post ],
|
||||
},
|
||||
'/project/:project/info/:path(*)': {
|
||||
get: [ mw.info.get ],
|
||||
@@ -165,7 +179,7 @@ app.map({
|
||||
'gis/:featuretype(line|point)': {
|
||||
get: [ mw.gis.navdata.get ]
|
||||
}
|
||||
}
|
||||
},
|
||||
//
|
||||
// '/user': {
|
||||
// get: [ mw.user.get ],
|
||||
@@ -177,12 +191,6 @@ app.map({
|
||||
// // delete: [ mw.user.delete ]
|
||||
// },
|
||||
//
|
||||
// '/login': {
|
||||
// post: [ mw.user.login ]
|
||||
// },
|
||||
// '/logout': {
|
||||
// post: [ mw.user.logout ]
|
||||
// }
|
||||
});
|
||||
|
||||
// Generic error handler. Stops stack dumps
|
||||
|
||||
31
lib/www/server/api/middleware/auth/access.js
Normal file
31
lib/www/server/api/middleware/auth/access.js
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
|
||||
async function read (req, res, next) {
|
||||
if (req.user) {
|
||||
next();
|
||||
} else {
|
||||
next({status: 403, message: "Access denied"});
|
||||
}
|
||||
}
|
||||
|
||||
async function write (req, res, next) {
|
||||
if (req.user && (req.user.role == "user" || req.user.role == "admin")) {
|
||||
next();
|
||||
} else {
|
||||
next({status: 403, message: "Access denied"});
|
||||
}
|
||||
}
|
||||
|
||||
async function admin (req, res, next) {
|
||||
if (req.user && req.user.role == "admin") {
|
||||
next();
|
||||
} else {
|
||||
next({status: 403, message: "Access denied"});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
read,
|
||||
write,
|
||||
admin
|
||||
};
|
||||
92
lib/www/server/api/middleware/auth/authentify.js
Normal file
92
lib/www/server/api/middleware/auth/authentify.js
Normal file
@@ -0,0 +1,92 @@
|
||||
const dns = require('dns');
|
||||
const { Netmask } = require('netmask');
|
||||
const cfg = require('../../../lib/config');
|
||||
const jwt = require('../../../lib/jwt');
|
||||
|
||||
async function authorisedIP (req, res) {
|
||||
const validIPs = cfg._("global.users.login.ip") || {};
|
||||
for (const key in validIPs) {
|
||||
const block = new Netmask(key);
|
||||
if (block.contains(req.ip)) {
|
||||
const payload = Object.assign({
|
||||
ip: req.ip,
|
||||
autologin: true
|
||||
}, validIPs[key]);
|
||||
jwt.issue(payload, req, res);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function authorisedHost (req, res) {
|
||||
const validHosts = cfg._("global.users.login.host") || {};
|
||||
for (const key in validHosts) {
|
||||
try {
|
||||
const ip = await dns.promises.resolve(key);
|
||||
if (ip == req.ip) {
|
||||
const payload = Object.assign({
|
||||
ip: req.ip,
|
||||
host: key,
|
||||
autologin: true
|
||||
}, validHosts[key]);
|
||||
jwt.issue(payload, req, res);
|
||||
return true;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code != "ENODATA") {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function auth (req, res, next) {
|
||||
|
||||
if (res.headersSent) {
|
||||
// Nothing to do, this request must have been
|
||||
// handled already by another middleware.
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for a valid JWT (already decoded by a previous
|
||||
// middleware).
|
||||
if (req.user) {
|
||||
if (!req.user.autologin) {
|
||||
// If this is not an automatic login, check if the token is in the
|
||||
// second half of its lifetime. If so, reissue a new one, valid for
|
||||
// another cfg.jwt.options.expiresIn seconds.
|
||||
if (req.user.exp) {
|
||||
const ttl = req.user.exp - Date.now()/1000;
|
||||
if (ttl < cfg.jwt.options.expiresIn/2) {
|
||||
const credentials = cfg._("global.users.login.user").find(i => i.name == req.user.name && i.role == req.user.role);
|
||||
if (credentials) {
|
||||
// Refresh token
|
||||
payload = Object.assign({}, credentials);
|
||||
delete payload.hash;
|
||||
jwt.issue(Object.assign({}, credentials), req, res);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the IP is known to us
|
||||
if (await authorisedIP(req, res)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the hostname is known to us
|
||||
if (await authorisedHost(req, res)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
next({status: 401, message: "Not authorised"});
|
||||
}
|
||||
|
||||
module.exports = auth;
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
exports.jwt = require('./jwt');
|
||||
// exports.access = require('./access');
|
||||
exports.authentify = require('./authentify');
|
||||
exports.access = require('./access');
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
exports.login = require('./login');
|
||||
exports.logout = require('./logout');
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
next({status: 401, message: "Unauthorised"});
|
||||
}
|
||||
}
|
||||
next({status: 400, message: "Bad request"});
|
||||
}
|
||||
|
||||
module.exports = login;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
async function logout (req, res, next) {
|
||||
res.clearCookie("JWT");
|
||||
res.status(204).send();
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = logout;
|
||||
|
||||
@@ -6,6 +6,7 @@ const YAML = require('yaml');
|
||||
const cfgPrefix = process.env.DOUGAL_ROOT || ((process.env.HOME || ".") + "/software");
|
||||
const cfgPath = process.env.DOUGAL_API_CONFIG || (cfgPrefix+"/etc/www/config.json");
|
||||
const globalCfgPath = cfgPrefix+"/etc/config.yaml";
|
||||
const usersCfgPath = cfgPrefix+"/etc/users.yaml";
|
||||
|
||||
let config = {}
|
||||
|
||||
@@ -24,7 +25,10 @@ try {
|
||||
|
||||
config = {
|
||||
"jwt": {
|
||||
secret
|
||||
secret,
|
||||
options: {
|
||||
expiresIn: 15*60*1000
|
||||
}
|
||||
},
|
||||
"db": {
|
||||
"user": "postgres",
|
||||
@@ -68,6 +72,17 @@ try {
|
||||
config.global = YAML.parse(text);
|
||||
}
|
||||
}
|
||||
|
||||
if (fs.existsSync(usersCfgPath)) {
|
||||
const text = fs.readFileSync(usersCfgPath, 'utf8');
|
||||
if (text) {
|
||||
if (!config.global) {
|
||||
config.global = {};
|
||||
}
|
||||
config.global.users = YAML.parse(text);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
21
lib/www/server/lib/jwt.js
Normal file
21
lib/www/server/lib/jwt.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const cfg = require('./config');
|
||||
|
||||
function issue (payload, req, res) {
|
||||
|
||||
const token = jwt.sign(payload, cfg.jwt.secret, cfg.jwt.options);
|
||||
|
||||
if (req) {
|
||||
req.user = payload;
|
||||
}
|
||||
|
||||
if (res) {
|
||||
res.cookie("JWT", token, {maxAge: cfg.jwt.options.expiresIn*1000 || 0});
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
issue
|
||||
};
|
||||
5
lib/www/server/package-lock.json
generated
5
lib/www/server/package-lock.json
generated
@@ -366,6 +366,11 @@
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
|
||||
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
|
||||
},
|
||||
"netmask": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz",
|
||||
"integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU="
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"express": "^4.17.1",
|
||||
"express-jwt": "^6.0.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"netmask": "^1.0.6",
|
||||
"node-fetch": "^2.6.1",
|
||||
"pg": "^8.3.3",
|
||||
"ws": "^7.3.1",
|
||||
|
||||
Reference in New Issue
Block a user