WiP on namespaces and users

This commit is contained in:
Tomas Bures 2017-05-15 16:22:06 -04:00
parent 432e6ffaeb
commit 1b73282e90
18 changed files with 513 additions and 0 deletions

2
app.js
View file

@ -42,6 +42,7 @@ const grapejs = require('./routes/grapejs');
const mosaico = require('./routes/mosaico');
const reports = require('./routes/reports');
const reportsTemplates = require('./routes/report-templates');
const namespaces = require('./routes/namespaces');
const app = express();
@ -213,6 +214,7 @@ app.use('/api', api);
app.use('/editorapi', editorapi);
app.use('/grapejs', grapejs);
app.use('/mosaico', mosaico);
app.use('/namespaces', namespaces);
if (config.reports && config.reports.enabled === true) {
app.use('/reports', reports);

View file

@ -177,3 +177,18 @@ templates=[["demo", "Demo Template"]]
# The bottom line is that if people who are creating report templates or have write access to the DB cannot be trusted,
# then it's safer to switch off the reporting functionality below.
enabled=false
[shares.list.master]
name="Master"
description="All permissions"
permissions=["view"]
[shares.namespace.master]
name="Master"
description="All permissions"
[shares.namespace.master.permissions]
list=["view"]
namespace=["view", "edit", "create", "delete", "create list"]

15
lib/knex.js Normal file
View file

@ -0,0 +1,15 @@
'use strict';
const config = require('config');
const knex = require('knex')({
client: 'mysql',
connection: config.mysql,
migrations: {
directory: __dirname + '/../setup/knex/migrations'
}
});
knex.migrate.latest();
module.exports = knex;

8
lib/models/namespaces.js Normal file
View file

@ -0,0 +1,8 @@
'use strict';
const config = require('config');
const knex = require('../knex');
module.exports.list = () => {
return knex('namespaces');
};

View file

@ -72,6 +72,7 @@
"jquery-file-upload-middleware": "^0.1.8",
"jsdom": "^9.12.0",
"juice": "^4.0.2",
"knex": "^0.13.0",
"libmime": "^3.1.0",
"marked": "^0.3.6",
"memory-cache": "^0.1.6",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 B

68
routes/namespaces.js Normal file
View file

@ -0,0 +1,68 @@
'use strict';
const express = require('express');
const passport = require('../lib/passport');
const router = new express.Router();
const _ = require('../lib/translate')._;
const namespaces = require('../lib/models/namespaces');
router.all('/*', (req, res, next) => {
if (!req.user) {
req.flash('danger', _('Need to be logged in to access restricted content'));
return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl));
}
// res.setSelectedMenu('namespaces');
next();
});
router.get('/', (req, res) => {
res.render('namespaces/namespaces', {
title: _('Namespaces'),
useFancyTree: true
});
});
router.get('/list/ajax', (req, res, next) => {
const entries = {};
const roots = [];
namespaces.list().then(rows => {
for (let row of rows) {
let entry;
if (!entries[row.id]) {
entry = {
children: []
};
entries[row.id] = entry;
} else {
entry = entries[row.id];
}
if (row.parent) {
if (entries[row.parent]) {
entries[row.parent] = {
children: []
};
}
entries[row.parent].children.push(entry);
} else {
roots.push(entry);
}
entry.title = row.name;
entry.key = row.id;
}
console.log(roots);
return res.json(roots);
}).catch(err => {
return res.json({
error: err.message || err,
});
});
});
module.exports = router;

9
setup/knex/config.js Normal file
View file

@ -0,0 +1,9 @@
'use strict';
if (!process.env.NODE_CONFIG_DIR) {
process.env.NODE_CONFIG_DIR = __dirname + '/../../config';
}
const config = require('config');
module.exports = config;

8
setup/knex/knexfile.js Normal file
View file

@ -0,0 +1,8 @@
'use strict';
const config = require('./config');
module.exports = {
client: 'mysql',
connection: config.mysql
};

View file

@ -0,0 +1,75 @@
exports.up = function(knex, Promise) {
/* This is shows what it would look like when we specify the "users" table with Knex.
In some sense, this is probably the most complicated table we have in Mailtrain.
return knex.schema.hasTable('users'))
.then(exists => {
if (!exists) {
return knex.schema.createTable('users', table => {
table.increments('id').primary();
table.string('username').notNullable().defaultTo('');
table.string('password').notNullable().defaultTo('');
table.string('email').notNullable();
table.string('access_token', 40).index();
table.string('reset_token').index();
table.dateTime('reset_expire');
table.timestamp('created').defaultTo(knex.fn.now());
})
// INNODB tables have the limit of 767 bytes for an index.
// Combined with the charset used, this poses limits on the size of keys. Knex does not offer API
// for such settings, thus we resort to raw queries.
.raw('ALTER TABLE `users` MODIFY `email` VARCHAR(255) CHARACTER SET utf8 NOT NULL')
.raw('ALTER TABLE `users` ADD UNIQUE KEY `email` (`email`)')
.raw('ALTER TABLE `users` ADD KEY `username` (`username`(191))')
.raw('ALTER TABLE `users` ADD KEY `check_reset` (`username`(191),`reset_token`,`reset_expire`)')
.then(() => knex('users').insert({
id: 1,
username: 'admin',
password: '$2a$10$FZV.tFT252o4iiHoZ9b2sOZOc.EBDOcY2.9HNCtNwshtSLf21mB1i',
email: 'hostmaster@sathyasai.org'
}));
}
});
*/
// We should check here if the tables already exist and upgrade them to db_schema_version 28, which is the baseline.
// For now, we just check whether our DB is up-to-date based on the existing SQL migration infrastructure in Mailtrain.
return knex('settings').where({key: 'db_schema_version'}).first('value')
.then(row => {
if (!row || Number(row.value) !== 28) {
throw new Error('Unsupported DB schema version: ' + row.value);
}
})
// We have to update data types of primary keys and related foreign keys. Mailtrain uses unsigned int(11), while
// Knex uses unsigned int (which is unsigned int(10) ).
.then(() => knex.schema
.raw('ALTER TABLE `users` MODIFY `id` int unsigned not null auto_increment')
.raw('ALTER TABLE `lists` MODIFY `id` int unsigned not null auto_increment')
.raw('ALTER TABLE `confirmations` MODIFY `list` int unsigned not null')
.raw('ALTER TABLE `custom_fields` MODIFY `list` int unsigned not null')
.raw('ALTER TABLE `importer` MODIFY `list` int unsigned not null')
.raw('ALTER TABLE `segments` MODIFY `list` int unsigned not null')
.raw('ALTER TABLE `triggers` MODIFY `list` int unsigned not null')
.raw('ALTER TABLE `custom_forms` MODIFY `list` int unsigned not null')
)
/*
Remaining foreign keys:
-----------------------
links campaign campaigns id
segment_rules segment segments id
import_failed import importer id
rss parent campaigns id
attachments campaign campaigns id
custom_forms_data form custom_forms id
report_template report_template report_templates id
*/
};
exports.down = function(knex, Promise) {
// return knex.schema.dropTable('users');
};

View file

@ -0,0 +1,31 @@
exports.up = function(knex, Promise) {
return knex.schema.createTable('namespaces', table => {
table.increments('id').primary();
table.string('name');
table.text('description');
table.integer('parent').unsigned().references('namespaces.id').onDelete('CASCADE');
})
.table('lists', table => {
table.integer('namespace').unsigned().notNullable();
})
.then(() => knex('namespaces').insert({
id: 1,
name: 'Global',
description: 'Global namespace'
}))
.then(() => knex('lists').update({
namespace: 1
}))
.then(() => knex.schema.table('lists', table => {
table.foreign('namespace').references('namespaces.id').onDelete('CASCADE');
}))
;
};
exports.down = function(knex, Promise) {
return knex.schema.dropTable('namespaces');
};

View file

@ -0,0 +1,51 @@
const config = require('../config');
exports.up = function(knex, Promise) {
return knex.schema.createTable('shares_list', table => {
table.increments('id').primary();
table.integer('list').unsigned().notNullable().references('lists.id').onDelete('CASCADE');
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
table.integer('level').notNullable();
table.unique(['list', 'user']);
})
.createTable('shares_namespace', table => {
table.increments('id').primary();
table.integer('namespace').unsigned().notNullable().references('namespaces.id').onDelete('CASCADE');
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
table.string('level', 64).notNullable();
table.unique(['namespace', 'user']);
})
.createTable('permissions_list', table => {
table.increments('id').primary();
table.integer('list').unsigned().notNullable().references('lists.id').onDelete('CASCADE');
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
table.string('permission', 64).notNullable();
table.unique(['list', 'user', 'permission']);
})
.createTable('permissions_namespace', table => {
table.increments('id').primary();
table.integer('namespace').unsigned().notNullable().references('lists.id').onDelete('CASCADE');
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
table.string('permission', 64).notNullable();
table.unique(['namespace', 'user', 'permission']);
})
.then(() => knex('shares_namespace').insert({
id: 1,
namespace: 1,
user: 1,
level: 'master'
}))
;
};
exports.down = function(knex, Promise) {
return knex.schema.dropTable('shares_namespace')
.dropTable('shares_list')
.dropTable('permissions_namespace')
.dropTable('permissions_list');
};

View file

@ -25,6 +25,10 @@
<link rel="stylesheet" href="/summernote/summernote.css">
{{/if}}
{{#if useFancyTree}}
<link rel="stylesheet" href="/fancytree/skin-bootstrap/ui.fancytree.min.css" >
{{/if}}
{{#each customStyles}}
<link rel="stylesheet" href="{{this}}">
{{/each}}
@ -33,6 +37,20 @@
<script src="{{this}}"></script>
{{/each}}
<style type="text/css">
#tree .fancytree-container {
height: 100px;
overflow: auto;
position: relative;
}
#tree .fancytree-active {
background-color: #5094ce;
}
#tree .fancytree-title {
background-color: transparent;
border-color: transparent;
}
</style>
</head>
<body class="{{#if user}}logged-in user-{{user.username}}{{/if}}">
@ -166,6 +184,12 @@
<script src="/javascript/editor.js"></script>
{{/if}}
{{#if useFancyTree}}
<script src="/javascript/jquery-ui-1.12.1.min.js" type="text/javascript" charset="utf-8"></script>
<script src="/fancytree/jquery.fancytree-all.min.js"></script>
{{/if}}
{{> tracking_scripts}}
</body>

39
views/namespaces/edit.hbs Normal file
View file

@ -0,0 +1,39 @@
<ol class="breadcrumb">
<li><a href="/">{{#translate}}Home{{/translate}}</a></li>
<li>{{#translate}}Namespaces{{/translate}}</li>
<li class="active">{{#translate}}Edit Namespace{{/translate}}</li>
</ol>
<div class="pull-right">
<a class="btn btn-primary" href="/reports/create" role="button"><i class="glyphicon glyphicon-plus"></i> {{#translate}}Create Namespace{{/translate}}</a>
</div>
<h2>{{#translate}}Namespaces{{/translate}}</h2>
<hr>
<form method="post" class="delete-form" id="namespaces-delete" action="/namespaces/delete">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<input type="hidden" name="id" value="{{id}}" />
</form>
<form class="form-horizontal" method="post" action="/namespaces/edit">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<input type="hidden" name="id" value="{{id}}" />
<div>
<hr/>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<div class="pull-right">
<button type="submit" form="namespaces-delete" class="btn btn-danger"><i class="glyphicon glyphicon-remove"></i> {{#translate}}Delete Namespace{{/translate}}</button>
</div>
<button type="submit" class="btn btn-primary"><i class="glyphicon glyphicon-ok"></i> {{#translate}}Update{{/translate}}</button>
</div>
</div>
</div>
</form>

View file

@ -0,0 +1,111 @@
<ol class="breadcrumb">
<li><a href="/">{{#translate}}Home{{/translate}}</a></li>
<li class="active">{{#translate}}Namespaces{{/translate}}</li>
</ol>
<div class="pull-right">
<a class="btn btn-primary" href="/reports/create" role="button"><i class="glyphicon glyphicon-plus"></i> {{#translate}}Create Namespace{{/translate}}</a>
</div>
<h2>{{#translate}}Namespaces{{/translate}}</h2>
<hr>
<div id="tree">
</div>
<div id="treetable-container" style="height: 100px; overflow: auto;">
<table id="treetable" class="table table-hover table-striped table-condensed">
<thead>
<tr>
<th>Name</th>
<th>B</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
glyph_opts = {
map: {
expanderClosed: 'glyphicon glyphicon-menu-right',
expanderLazy: 'glyphicon glyphicon-menu-right', // glyphicon-plus-sign
expanderOpen: 'glyphicon glyphicon-menu-down', // glyphicon-collapse-down
}
};
$('#tree').fancytree({
extensions: ['glyph'],
glyph: glyph_opts,
selectMode: 1,
icon: false,
autoScroll: true,
source: [
{title: 'A', key: '1', expanded: true},
{title: 'B', key: '2', expanded: true, folder: true, children: [
{title: 'BA', key: '3', expanded: true, folder: true, children: [
{title: 'BAA', key: '4', expanded: true},
{title: 'BAB', key: '5', expanded: true}
]},
{title: 'BB', key: '6', expanded: true, folder: true, children: [
{title: 'BBA', key: '7', expanded: true},
{title: 'BBB', key: '8', expanded: true}
]}
]}
]
});
/*
$('#treetable').fancytree({
extensions: ['glyph', 'table'],
glyph: glyph_opts,
selectMode: 1,
icon: false,
autoScroll: true,
scrollParent: $("#treetable-container"),
source: [
{title: 'A', key: '1', expanded: true},
{title: 'B', key: '2', expanded: true, folder: true, children: [
{title: 'BA', key: '3', expanded: true, folder: true, children: [
{title: 'BAA', key: '4', expanded: true},
{title: 'BAB', key: '5', expanded: true}
]},
{title: 'BB', key: '6', expanded: true, folder: true, children: [
{title: 'BBA', key: '7', expanded: true},
{title: 'BBB', key: '8', expanded: true}
]}
]}
],
table: {
nodeColumnIdx: 0
}
});
*/
$('#treetable').fancytree({
extensions: ['glyph', 'table'],
glyph: glyph_opts,
selectMode: 1,
icon: false,
autoScroll: true,
scrollParent: $("#treetable-container"),
source: {
url: "/namespaces/list/ajax",
cache: true
},
table: {
nodeColumnIdx: 0
}
});
});
</script>