From c12ed739c77bc14b570635e8e7547ded0d4b9b9f Mon Sep 17 00:00:00 2001 From: Evert Date: Tue, 29 Aug 2017 15:00:36 +0300 Subject: [PATCH] news composing and editing --- server/api/news.js | 25 ++++++++++++++++++ server/routes/api.js | 20 ++++++++++++++ server/routes/index.js | 48 ++++++++++++++++++++++++++++++---- src/script/admin.js | 4 +-- views/admin/layout.pug | 18 ++++++------- views/article.pug | 24 ----------------- views/layout.pug | 9 ++++--- views/news/article.pug | 55 +++++++++++++++++++++++++++++++++++++++ views/news/composer.pug | 31 ++++++++++++++++++++++ views/{ => news}/news.pug | 6 +++-- 10 files changed, 194 insertions(+), 46 deletions(-) delete mode 100644 views/article.pug create mode 100644 views/news/article.pug create mode 100644 views/news/composer.pug rename views/{ => news}/news.pug (82%) diff --git a/server/api/news.js b/server/api/news.js index 41d2f92..eb81e1f 100644 --- a/server/api/news.js +++ b/server/api/news.js @@ -78,6 +78,31 @@ const News = { article = article[0] return cleanArticle(article) + }, + compose: async (user, body) => { + let article = { + title: body.title, + content: body.content, + tags: body.tags || '', + user_id: user.id, + created_at: new Date() + } + + let result = await Models.News.query().insert(article) + result.slug = slugify(result.title) + + return result + }, + edit: async (id, body) => { + if (!body.content) return {error: 'Content required'} + let patch = { + content: body.content, + updated_at: new Date() + } + + let result = await Models.News.query().patchAndFetchById(id, patch) + if (!result) return {error: 'Something went wrong.'} + return {} } } diff --git a/server/routes/api.js b/server/routes/api.js index 7090782..183421f 100644 --- a/server/routes/api.js +++ b/server/routes/api.js @@ -288,6 +288,21 @@ router.get('/news/all/', (req, res) => { res.redirect('/api/news/all/1') }) +router.post('/news/edit/:id', wrap(async (req, res, next) => { + if (!req.session.user || req.session.user.privilege < 1) return next() + if (!req.params.id || isNaN(parseInt(req.params.id))) { + return res.status(400).jsonp({error: 'Invalid ID number.'}) + } + + let id = parseInt(req.params.id) + let result = await News.edit(id, req.body) + if (result.error) { + return res.status(400).jsonp({error: result.error}) + } + + res.status(204).end() +})) + // Fetch article router.get('/news/:id', wrap(async (req, res) => { if (!req.params.id || isNaN(parseInt(req.params.id))) { @@ -438,4 +453,9 @@ router.use((req, res) => { res.status(404).jsonp({error: 'Not found'}) }) +router.use((err, req, res, next) => { + console.error(err) + res.jsonp({error: 'Internal server error.'}) +}) + module.exports = router diff --git a/server/routes/index.js b/server/routes/index.js index 61b1151..a7bc558 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -31,6 +31,7 @@ function setSession (req, user) { display_name: user.display_name, email: user.email, avatar_file: user.avatar_file, + privilege: user.nw_privilege, session_refresh: Date.now() + 1800000 // 30 minutes } } @@ -65,10 +66,10 @@ router.use(wrap(async (req, res, next) => { // Update user session let udata = await API.User.get(req.session.user.id) + setSession(req, udata) // Update IP address await API.User.update(udata, {ip_address: req.realIP}) - setSession(req, udata) } } @@ -431,7 +432,7 @@ router.post('/register', accountLimiter, wrap(async (req, res, next) => { } // 6th Check: reCAPTCHA (if configuration contains key) - if (config.security.recaptcha && config.security.recaptcha.site_key) { + if (config.security && config.security.recaptcha && config.security.recaptcha.site_key) { if (!req.body['g-recaptcha-response']) return formError(req, res, 'Please complete the reCAPTCHA!') try { @@ -648,6 +649,39 @@ router.get('/docs/:name', (req, res, next) => { res.render('document', {doc: doc}) }) +/* + ======== + NEWS + ======== +*/ + +function privileged (req, res, next) { + if (!req.session.user) return res.redirect('/news') + if (req.session.user.privilege < 1) return res.redirect('/news') + next() +} + +router.get('/news/writer', privileged, wrap(async (req, res) => { + res.render('news/composer') +})) + +router.post('/news/writer', privileged, wrap(async (req, res) => { + if (req.body.csrf !== req.session.csrf) { + return formError(req, res, 'Invalid session! Try reloading the page.') + } + + if (!req.body.title || !req.body.content) { + return formError(req, res, 'Required fields missing!') + } + + let result = await News.compose(req.session.user, req.body) + if (result.error) { + return formError(req, res, result.error) + } + + res.redirect('/news/' + result.id + '-' + result.slug) +})) + // Serve news router.get('/news/:id?-*', wrap(async (req, res) => { let id = parseInt(req.params.id) @@ -660,8 +694,12 @@ router.get('/news/:id?-*', wrap(async (req, res) => { return res.status(404).render('article', {article: null}) } - res.header('Cache-Control', 'max-age=' + 24 * 60 * 60 * 1000) // 1 day - res.render('article', {article: article}) + let editing = false + if (req.query.edit === '1' && req.session.user && req.session.user.privilege > 1) { + editing = true + } + + res.render('news/article', {article: article, editing: editing}) })) router.get('/news/', wrap(async (req, res) => { @@ -672,7 +710,7 @@ router.get('/news/', wrap(async (req, res) => { let news = await News.listNews(page) - res.render('news', {news: news}) + res.render('news/news', {news: news}) })) // Render partials diff --git a/src/script/admin.js b/src/script/admin.js index c728cf6..854eb1a 100644 --- a/src/script/admin.js +++ b/src/script/admin.js @@ -228,8 +228,8 @@ $(document).ready(function () { }) } - window.Dialog.openTemplate = function (title, template, data = {}) { - window.Dialog.open(title, buildTemplateScript(template, data), true) + window.Dialog.openTemplate = function (title, template, data) { + window.Dialog.open(title, buildTemplateScript(template, data || {}), true) } $('#dialog #close').click(function (e) { diff --git a/views/admin/layout.pug b/views/admin/layout.pug index c4c6adb..1d5717b 100644 --- a/views/admin/layout.pug +++ b/views/admin/layout.pug @@ -1,16 +1,16 @@ html head meta(charset="utf8") - link(rel="stylesheet", type="text/css", href="https://fonts.googleapis.com/css?family=Open+Sans") - link(rel="stylesheet", type="text/css", href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css") - link(rel="stylesheet", type="text/css", href="/style/main.css") - link(rel="stylesheet", type="text/css", href="/style/admin.css") block links - script. - window.variables = { - server_time: parseInt('#{server_time}') - }; - script(src="/script/admin.js") + link(rel="stylesheet", type="text/css", href="https://fonts.googleapis.com/css?family=Open+Sans") + link(rel="stylesheet", type="text/css", href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css") + link(rel="stylesheet", type="text/css", href="/style/main.css") + link(rel="stylesheet", type="text/css", href="/style/admin.css") + script. + window.variables = { + server_time: parseInt('#{server_time}') + }; + script(src="/script/admin.js") title block title |Icy Network - Administration diff --git a/views/article.pug b/views/article.pug deleted file mode 100644 index 3191332..0000000 --- a/views/article.pug +++ /dev/null @@ -1,24 +0,0 @@ -extends layout.pug - -block title - if article - |Icy Network - News - #{article.title} - else - |Icy Network - News - 404 - -block body - .document - .content - if !article - span.error No such article - else - .article - .title= article.title - .author Published by - span #{article.author.display_name} - |at - .timestamp #{new Date(article.created_at)} - .content!= article.content - .return - a(href="/news") Back to news directory - diff --git a/views/layout.pug b/views/layout.pug index 1eac9fb..fcf339a 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -3,10 +3,11 @@ html head meta(charset="utf8") meta(name="viewport", content="width=device-width, initial-scale=1") - link(rel="stylesheet", type="text/css", href="/style/main.css") - link(rel="stylesheet", type="text/css", href="https://fonts.googleapis.com/css?family=Open+Sans") - link(rel="stylesheet", type="text/css", href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css") - script(src="/script/main.js") + block links + link(rel="stylesheet", type="text/css", href="/style/main.css") + link(rel="stylesheet", type="text/css", href="//fonts.googleapis.com/css?family=Open+Sans") + link(rel="stylesheet", type="text/css", href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css") + script(src="/script/main.js") title block title body diff --git a/views/news/article.pug b/views/news/article.pug new file mode 100644 index 0000000..65ad88b --- /dev/null +++ b/views/news/article.pug @@ -0,0 +1,55 @@ +extends ../layout.pug + +block append links + if editing + script(src="//cdnjs.cloudflare.com/ajax/libs/ckeditor/4.7.2/ckeditor.js") + +block title + if article + |Icy Network - News - #{article.title} + else + |Icy Network - News - 404 + +block body + .document + .content + if !article + span.error No such article + else + .article + if user && user.privilege && user.privilege > 0 && !editing + a.button(style="float: right;" href="?edit=1") Edit + .title= article.title + .author Published by + span #{article.author.display_name} + |at + .timestamp #{new Date(article.created_at)} + if editing + .content(contenteditable="true" id="editor1")!= article.content + else + .content!= article.content + if editing + .button(id="done") Done editing + br + script. + CKEDITOR.disableAutoInline = true; + CKEDITOR.inline('editor1'); + $('#done').click(function (e) { + let data = CKEDITOR.instances.editor1.getData(); + $.post({ + url: '/api/news/edit/#{article.id}', + data: {content: data}, + success: function () { + window.location.href = '/news/#{article.id}-#{article.slug}' + }, + error: function (e) { + if (e.responseJSON && e.responseJSON.error) { + alert(e.responseJSON.error); + } + } + }); + }); + + .return + a(href="/news") Back to the news archive + diff --git a/views/news/composer.pug b/views/news/composer.pug new file mode 100644 index 0000000..a655419 --- /dev/null +++ b/views/news/composer.pug @@ -0,0 +1,31 @@ +extends ../layout.pug + +block append links + script(src="//cdnjs.cloudflare.com/ajax/libs/ckeditor/4.7.2/ckeditor.js") + +block title + |Icy Network - News - Compose + +block body + .document + .content + if message.text + if message.error + .message.error + span #{message.text} + else + .message + span #{message.text} + form(action="", method="post") + input(type="hidden", name="csrf", value=csrf) + label(for="title") Title + input(type="text", name="title", id="title") + label(for="composer1") Content + textarea(name="content" id="composer1") + label(for="tags") Tags + input(type="text", name="tags", id="tags") + input(type="submit", value="Submit") + script. + CKEDITOR.replace('composer1') + a(href="/news") Back to news directory + diff --git a/views/news.pug b/views/news/news.pug similarity index 82% rename from views/news.pug rename to views/news/news.pug index 2879fcc..b7876d7 100644 --- a/views/news.pug +++ b/views/news/news.pug @@ -1,4 +1,4 @@ -extends layout.pug +extends ../layout.pug block title |Icy Network - News @@ -6,7 +6,9 @@ block title block body .document .content - h1 Icy Network News + if user && user.privilege && user.privilege > 0 + a.button(style="float: right;" href="/news/writer") New Article + h1 Icy Network News Archive if news.error span.error There are no articles to show. else