Compare commits

...

4 Commits

Author SHA1 Message Date
alban
77ccaef87d [enh] There should be a patch method. Also, DOM events should work... 2020-05-24 23:02:56 +02:00
alban
b0c6043741 [enh] The delete action should work 2020-05-23 23:26:51 +02:00
alban
3ce2e4514d [fix] tweaks 2020-05-23 22:06:24 +02:00
alban
5bedb0c2a9 [fix] simplify css 2020-05-23 21:47:49 +02:00
5 changed files with 304 additions and 130 deletions

View File

@ -6,17 +6,17 @@ const dbInit = {};
const bulkData = [ const bulkData = [
{ "index" : { "_index" : "changelog" } }, { "index" : { "_index" : "changelog" } },
{ "author" : "John Ripper <john@theripper.com", { "author" : "John Ripper <john@theripper.com>",
"content":"* machines: Installed the server\n```debootstrap -t foobar```", "content":"* machines: Installed the server\n```debootstrap -t foobar```",
"server": "server.example.com", "server": "server.example.com",
"created_at":"2020-05-23T09:50:33.397Z"}, "created_at":"2020-05-23T09:50:33.397Z"},
{ "index" : { "_index" : "changelog" } }, { "index" : { "_index" : "changelog" } },
{ "author" : "John Ripper <john@theripper.com", { "author" : "John Ripper <john@theripper.com>",
"content":"* db: Installed mysql\n```apt install mariadb-server```", "content":"* db: Installed mysql\n```apt install mariadb-server```",
"server": "server.example.com", "server": "server.example.com",
"created_at":"2020-05-23T10:50:33.397Z"}, "created_at":"2020-05-23T10:50:33.397Z"},
{ "index" : { "_index" : "changelog" } }, { "index" : { "_index" : "changelog" } },
{ "author" : "John Ripper <john@theripper.com", { "author" : "John Ripper <john@theripper.com>",
"content":"* nginx: add package\n```apt install nginx-full```", "content":"* nginx: add package\n```apt install nginx-full```",
"server": "server.example.com", "server": "server.example.com",
"created_at":"2020-05-23T16:50:33.397Z"} "created_at":"2020-05-23T16:50:33.397Z"}

View File

@ -29,8 +29,6 @@ dbInit.init({
seed : process.env.DB_SEED seed : process.env.DB_SEED
}); });
console.log( "exit")
const express = require('express'); const express = require('express');
const app = express(); const app = express();
@ -40,6 +38,7 @@ app.use(express.static('public'));
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
app.use(bodyParser.json()); app.use(bodyParser.json());
app.use(bodyParser.raw()); app.use(bodyParser.raw());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.text({ type : "text/*" })); app.use(bodyParser.text({ type : "text/*" }));
app.disable('x-powered-by'); app.disable('x-powered-by');
@ -64,14 +63,15 @@ function requireAuthentication( req, res, next ){
app.all('*', requireAuthentication); app.all('*', requireAuthentication);
const routes = require( "./routes"); const routes = require( "./routes");
app.get('/get/:id', routes.get);
app.get('/log/:id', routes.log); app.get('/log/:id', routes.log);
app.get('/health', routes.health); app.get('/health', routes.health);
app.get('/search', routes.search); app.get('/search', routes.search);
app.post('/*', routes.add);
app.get('/*', routes.main); app.get('/*', routes.main);
app.patch('/*', routes.main); app.post('/*', routes.add);
app.patch('/patch/:id', routes.patch);
app.put('/*', routes.main); app.put('/*', routes.main);
app.delete('/*', routes.main); app.delete('/delete/:id', routes.delete);
app.listen(port, () => { app.listen(port, () => {

View File

@ -5,45 +5,49 @@
*/ */
.log { .log {
margin-bottom: 10px; padding: 16px 0;
border-top: 1px solid #eee; border-top: 1px solid #eee;
padding-top: 6px;
}
.log p {
color: #666;
}
.log span.cmd {
background: #eee;
color: #003e80;
padding: 3px 8px;
}
.log pre {
font-size: 1.0rem;
color: #333;
} }
.log .meta a { .log .meta {
color: #666;
font-size: .875rem; font-size: .875rem;
color: #888;
line-height:1.2em;
text-align: right;
} }
.log .meta p { .log .meta p {
font-size: .875rem; margin: 0px;
line-height:1em; overflow: hidden;
margin: 0px; text-overflow: ellipsis;
overflow: hidden;
text-overflow: ellipsis;
} }
.log .meta p.server { .log .meta p.server {
color:#333; color: #333;
font-size:1.125rem;
} }
.log .meta .actions-toggle { .log .meta .actions-toggle {
margin: 0; margin: 0;
padding: 0; padding: 0;
color: #666; cursor: pointer;
cursor: pointer; line-height:1em;
line-height:1em;
} }
.log .meta .actions{ .log .meta .actions{
display:none; display:none;
} }
.log pre {
font-size: 1.125rem;
color: #333;
}
.log pre .cmd {
background: #eee;
color: #333f4d;
padding: 3px 8px;
}
@media (max-width: 991.98px){
.log .meta {
text-align: left;
}

View File

@ -1,103 +1,195 @@
/* global initData, authorizationToken */ /* global initData, authorizationToken */
// List of HTML entities for escaping. $(function(){
var htmlEscapes = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'/': '&#x2F;'
};
// Regex containing the keys listed immediately above. const actionToggle = function(e){
var htmlEscaper = /[&<>"'\/]/g; var el=e.target;
$(el).parent().siblings('.actions').show();
$(el).hide();
};
// Escape a string for HTML interpolation. const deleteButton = function(e){
escape = function(string) { const el = $(e.target);
return ('' + string).replace(htmlEscaper, function(match) { const url = el.attr('href');
return htmlEscapes[match]; $.ajax(url,{
}); method: "DELETE",
}; beforeSend: function(request) {
var urlRegex = /(\S+): (https?:&#x2F;&#x2F;[^\s]+)/g; request.setRequestHeader("authorizationToken", authorizationToken);
url = function(string){ }
return ''+string.replace(urlRegex, '<a target="_blank" href="$2">$1</a>') })
} .done(function(data) {
var titleRegex = /^(.*\n)/; $(el).parents('.log').remove();
title = function(string){ })
return ''+string.replace(titleRegex, '<b>$1</b>'); .fail(function() {
} alert( "error" );
var cmdRegex = /```([^`]*?)```/g });
cmd = function(string) { return false;
return ''+string.replace(cmdRegex, '<span class="cmd">$1</span>'); };
}
date = function(date){ const editButton = function(e){
var D = new Date(date); const el = $(e.target);
return D.toLocaleDateString()+" "+D.toLocaleTimeString(); const id = el.attr("rel");
} const pre = el.parents(".log").find("pre");
function updatePage(data){ $.ajax(`/get/${id}`,{
beforeSend: function(request) {
request.setRequestHeader("authorizationToken", authorizationToken);
}
})
.done(function(data) {
pre
.text(data._source.content)
.attr('rel',id)
.css("background","#ccc")
.attr("contenteditable",true)
.focus();
$('body pre[contenteditable]').on('focusout', contentEdited );
})
.fail(function(data,err) {
console.log(data,err);
alert( "error" );
});
return false;
};
const contentEdited = function(e) {
const el = $(e.target);
const id = el.attr('rel');
const content = el.text();
$.ajax(`/patch/${id}`,{
method: "PATCH",
data: {content:content},
beforeSend: function(request) {
request.setRequestHeader("authorizationToken", authorizationToken);
}
})
.done(function(data) {
el
.css("background","")
.html( formatContent(content) );
})
.fail(function(data,err) {
console.log(data,err);
alert( "error" );
});
};
var content = ""; const search = function(e){
var item = {}; const el = $(e.target);
var id = ''; const val = el.val();
// If the log entry is unique, simulate a search result if( val.length < 3 ){ return; }
if( ! data['hits'] ){ $.ajax("/search",{
data = {hits:{hits:[data]}}; beforeSend: function(request) {
} request.setRequestHeader("authorizationToken", authorizationToken);
$.each(data.hits.hits, (k,v)=>{ },
data: {
item = v._source; q:val
id = v._id; }
content += ` })
.done(function(data) {
updatePage(data);
})
.fail(function() {
alert( "error" );
});
};
<div class="log row"> // List of HTML entities for escaping.
<div class="meta col-lg-2 text-right"> var htmlEscapes = {
<p class="server"> ${escape(item.server)} </p> '&': '&amp;',
<a href="/log/${id}"> '<': '&lt;',
${date(escape(item.created_at))} <br/> '>': '&gt;',
</a> '"': '&quot;',
<div class="d-none d-lg-block"> "'": '&#x27;',
<p class="author"> ${escape(item.author)} </p> '/': '&#x2F;'
<p> };
<a class="actions-toggle btn-link btn-sm">Actions</a>
</p> // Regex containing the keys listed immediately above.
<div class="actions btn-group btn-group-sm" role="group" aria-label="log actions"> var htmlEscaper = /[&<>"'\/]/g;
<a class="destroy btn btn btn-outline-secondary" href="/destroy/${id}">Remove</a>
<a class="edit btn btn btn-outline-secondary" href="/edit/${id}">Edit</a> // Escape a string for HTML interpolation.
</div> const escape = function(string) {
</div> return ('' + string).replace(htmlEscaper, function(match) {
</div> return htmlEscapes[match];
<div class="col-lg"> });
<pre> ${cmd(title(url(escape(item.content))))}</pre> };
</div> const urlRegex = /(\S+): (https?:&#x2F;&#x2F;[^\s]+)/g;
</div> const url = function(string){
`; return ''+string.replace(urlRegex, '<a target="_blank" href="$2">$1</a>');
}); };
$("#content").html(content); var titleRegex = /^(.*\n)/;
const title = function(string){
return ''+string.replace(titleRegex, '<b>$1</b>');
};
var cmdRegex = /```([^`]*?)```/g;
const cmd = function(string) {
return ''+string.replace(cmdRegex, '<span class="cmd">$1</span>');
};
const date = function(string){
var D = new Date(string);
return D.toLocaleDateString()+" "+D.toLocaleTimeString();
};
const mailRegexp = /(.*) &lt;(.+@.+)&gt;/;
const mail = function( string ){
return ''+string.replace(mailRegexp, `<a href="mailto:${string}">$1</a>`);
};
} const formatContent = function(string){
return cmd(title(url(escape(string))));
};
function updatePage(data){
$("input").on("keyup",function(e){ var content = "";
const el = $(e.target); var item = {};
const val = el.val(); var id = '';
if( val.length < 3 ){ return; } // If the log entry is unique, simulate a search result
$.ajax("/search",{ if( ! data['hits'] ){
beforeSend: function(request) { data = {hits:{hits:[data]}};
request.setRequestHeader("authorizationToken", authorizationToken);
},
data: {
q:val,
} }
}) $.each(data.hits.hits, (k,v)=>{
.done(function(data) {
updatePage(data); item = v._source;
}) id = v._id;
.fail(function() { content += `
alert( "error" );
}); <div class="log row">
<div class="meta col-lg-2 ">
<p class="server"> ${escape(item.server)} </p>
<a href="/log/${id}">
${date(escape(item.created_at))} <br/>
</a>
<div class="d-none d-lg-block">
<p class="author"> ${mail(escape(item.author))} </p>
<p>
<a class="actions-toggle btn-link btn-sm">Actions</a>
</p>
<div class="actions btn-group btn-group-sm" role="group" aria-label="log actions">
<a class="delete btn btn btn-outline-secondary" rel="${id}" href="/delete/${id}">Remove</a>
<a class="edit btn btn btn-outline-secondary" rel="${id}" href="/edit/${id}">Edit</a>
</div>
</div>
</div>
<div class="col-lg data">
<pre> ${formatContent(item.content)}</pre>
</div>
</div>
`;
});
$("#content").html(content);
// attache events
$(".actions-toggle").on("click",actionToggle );
$('.delete').on('click', deleteButton);
$('.edit').on('click', editButton );
}
$("input").on("keyup",search );
updatePage( initData );
}); });
updatePage( initData );
$(".actions-toggle").on("click",(e) => { var el=e.target; $(el).parent().siblings('.actions').show(); $(el).hide(); } )

View File

@ -54,6 +54,53 @@ const routes = {
res.json({"health":0,"msg":"Lost connection to ES"}); res.json({"health":0,"msg":"Lost connection to ES"});
}); });
}, },
delete: (req,res) => {
const id= req.params.id;
// Reindex the doc to the "trash" index
var log = client.reindex({
refresh: true,
max_docs: 1,
body: {
source: {
index: 'changelog',
query: {
term: {
_id: id
}
}
},
dest: {
index: 'changelog-trash',
}
}
})
.then( (results, err) => {
console.log(`reindexing success for id ${id}`)
// Remove it from the original index
return client.delete({
index: "changelog",
id: id
});
}, (e) => {
console.log("reindexing error")
res.status(400);
res.end("error");
})
.then( (results, err) => {
console.log(`Delete success for id ${id}`)
res.end("ok");
},(results, err) => {
console.log(`Delete error for id ${id}`)
res.status(400);
res.end("error");
});
},
add: (req, res) => { add: (req, res) => {
const body = req.body; const body = req.body;
@ -69,12 +116,43 @@ const routes = {
res.end("error"); res.end("error");
}); });
} , } ,
get: (req, res) => {
const id= req.params.id;
var log = client.get({
index: 'changelog',
id: id
}).then( (results, err) => {
res.json(results);
}, (e) => {
res.status(400);
res.json({msg:"Failed to get record"});
});
},
patch: (req,res) => {
const id= req.params.id;
const content= req.body.content;
var log = client.update({
index: 'changelog',
id: id,
body:{
doc:{
content:content
}
}
}).then( (results, err) => {
res.json(results);
}, (e) => {
res.status(400);
res.json({msg:"Failed to get record"});
}); },
log: (req, res) => { log: (req, res) => {
const id= req.params.id; const id= req.params.id;
var log = client.get({ var log = client.get({
index: 'changelog', index: 'changelog',
id: id id: id
}).then( (results, err) => { }).then( (results, err) => {
res.render('index', { res.render('index', {
title: 'changelog', title: 'changelog',