diff --git a/server/api/index.js b/server/api/index.js index e806bc5..563edda 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -221,11 +221,6 @@ const API = { return {error: null, user: user} } } - }, - External: { - serviceCallback: async function () { - - } } } diff --git a/server/api/news.js b/server/api/news.js new file mode 100644 index 0000000..f191ab0 --- /dev/null +++ b/server/api/news.js @@ -0,0 +1,107 @@ +import API from './index' +import Models from './models' +import config from '../../scripts/load-config' +import database from '../../scripts/load-database' + +const perPage = 8 + +function slugify (title) { + return title.toLowerCase().replace(/\W/g, '-').substring(0, 16) +} + +//** ppp - Posts Per Page; dcount - Post Count; page - number of current page +function Pagination (ppp, dcount, page) { + if (!ppp) ppp = 5 + if (!dcount) return null + + let pageCount = Math.ceil(dcount / ppp) + if (page > pageCount) page = pageCount + + let offset = (page - 1) * ppp + + return { + page: page, + perPage: ppp, + pages: pageCount, + offset: offset, + total: dcount + } +} + +async function cleanArticle (entry, shortenContent = false) { + let poster = await API.User.get(entry.user_id) + let article = { + id: entry.id, + slug: slugify(entry.title), + title: entry.title, + content: entry.content, + tags: entry.tags.split(','), + created_at: entry.created_at, + updated_at: entry.updated_at + } + + if (poster) { + article.author = { + id: poster.id, + display_name: poster.display_name + } + } + + if (shortenContent) { + article.content = article.content.replace(/(<([^>]+)>)/ig, '').substring(0, 128) + '...' + } + + return article +} + +const News = { + preview: async () => { + // Fetch 3 latest stories + let news = await Models.News.query().orderBy('created_at', 'desc').limit(3) + + if (!news.length) return [] + + let articles = [] + for (let i in news) { + let entry = news[i] + articles.push(await cleanArticle(entry, true)) + } + + return articles + }, + listNews: async (page) => { + let count = await Models.News.query().count('id as ids') + if (page < 1) page = 1 + + if (!count.length || !count[0]['ids'] || isNaN(page)) { + return {error: 'No articles found'} + } + + count = count[0].ids + let paginated = Pagination(perPage, parseInt(count), page) + let news = await Models.News.query().orderBy('created_at', 'desc').offset(paginated.offset).limit(perPage) + + let articles = [] + for (let i in news) { + let entry = news[i] + + articles.push(await cleanArticle(entry)) + } + + return { + page: paginated, + articles: articles + } + }, + article: async (id) => { + let article = await Models.News.query().where('id', id) + if (!article.length) return {} + article = article[0] + + let poster = await API.User.get(article.user_id) + + return await cleanArticle(article) + } +} + +module.exports = News diff --git a/server/api/oauth2/controller/code/code.js b/server/api/oauth2/controller/code/code.js index cc2a00a..0da45d5 100644 --- a/server/api/oauth2/controller/code/code.js +++ b/server/api/oauth2/controller/code/code.js @@ -9,7 +9,7 @@ module.exports = async (req, res, client, scope, user, redirectUri, createAllowF } if (createAllowFuture) { - if (!req.body || (typeof req.body['decision']) === 'undefined') { + if (!req.body || (typeof req.body['decision']) === undefined) { throw new error.InvalidRequest('No decision parameter passed') } else if (req.body['decision'] === '0') { throw new error.AccessDenied('User denied access to the resource') diff --git a/server/api/oauth2/controller/code/implicit.js b/server/api/oauth2/controller/code/implicit.js index 4a2442b..f00a497 100644 --- a/server/api/oauth2/controller/code/implicit.js +++ b/server/api/oauth2/controller/code/implicit.js @@ -9,7 +9,7 @@ module.exports = async (req, res, client, scope, user, redirectUri, createAllowF } if (createAllowFuture) { - if (!req.body || (typeof req.body['decision']) === 'undefined') { + if (!req.body || (typeof req.body['decision']) === undefined) { throw new error.InvalidRequest('No decision parameter passed') } else if (req.body['decision'] === '0') { throw new error.AccessDenied('User denied access to the resource') diff --git a/server/api/oauth2/controller/decision.js b/server/api/oauth2/controller/decision.js index c239dd3..de0cb9a 100644 --- a/server/api/oauth2/controller/decision.js +++ b/server/api/oauth2/controller/decision.js @@ -1,5 +1,3 @@ module.exports = function (req, res, client, scope, user) { - res.locals.client = client - res.locals.scope = scope - res.render('authorization') + res.render('authorization', { client: client, scope: scope }) } diff --git a/server/api/oauth2/index.js b/server/api/oauth2/index.js index e5be603..3f012e5 100644 --- a/server/api/oauth2/index.js +++ b/server/api/oauth2/index.js @@ -1,8 +1,6 @@ import middleware from './middleware' import controller from './controller' import decision from './controller/decision' -import response from './response' -import error from './error' import model from './model' class OAuth2Provider { @@ -10,8 +8,6 @@ class OAuth2Provider { this.bearer = middleware this.controller = controller this.decision = decision - this.response = response - this.error = error.OAuth2Error this.model = model } diff --git a/server/routes/api.js b/server/routes/api.js index bee73bf..7770d57 100644 --- a/server/routes/api.js +++ b/server/routes/api.js @@ -5,6 +5,7 @@ import config from '../../scripts/load-config' import wrap from '../../scripts/asyncRoute' import API from '../api' import APIExtern from '../api/external' +import News from '../api/news' let router = express.Router() @@ -206,4 +207,46 @@ router.get('/external/discord/callback', wrap(async (req, res) => { res.redirect(uri) })) +/* ======== + * NEWS + * ======== + */ + +// Get page of articles +router.get('/news/all/:page', wrap(async (req, res) => { + if (!req.params.page || isNaN(parseInt(req.params.page))) { + return res.status(400).jsonp({error: 'Invalid page number.'}) + } + + let page = parseInt(req.params.page) + + let articles = await News.listNews(page) + + res.jsonp(articles) +})) + +// Redirect to page one +router.get('/news/all/', (req, res) => { + res.redirect('/api/news/all/1') +}) + +// Fetch article +router.get('/news/:id', wrap(async (req, res) => { + 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 article = await News.article(id) + res.jsonp(article) +})) + +// Preview endpoint +router.get('/news', wrap(async (req, res) => { + let articles = await News.preview() + + res.jsonp(articles) +})) + module.exports = router diff --git a/server/routes/index.js b/server/routes/index.js index 60f2132..512f4e2 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -7,6 +7,7 @@ import config from '../../scripts/load-config' import wrap from '../../scripts/asyncRoute' import http from '../../scripts/http' import API from '../api' +import News from '../api/news' import apiRouter from './api' import oauthRouter from './oauth2' @@ -358,6 +359,31 @@ router.get('/docs/:name', (req, res) => { res.render('document', {doc: doc}) }) +router.get('/news/:id?-*', wrap(async (req, res) => { + let id = parseInt(req.params.id) + if (isNaN(id)) { + return res.status(404).render('article', {article: null}) + } + + let article = await News.article(id) + if (!article.id) { + return res.status(404).render('article', {article: null}) + } + + res.render('article', {article: article}) +})) + +router.get('/news/', wrap(async (req, res) => { + let page = parseInt(req.query.page) + if (isNaN(page)) { + page = 1 + } + + let news = await News.listNews(page) + + res.render('news', {news: news}) +})) + /* ========= OTHER diff --git a/server/routes/oauth2.js b/server/routes/oauth2.js index a05f9a0..af0f6be 100644 --- a/server/routes/oauth2.js +++ b/server/routes/oauth2.js @@ -34,6 +34,7 @@ router.post('/introspect', oauth.controller.introspection) router.get('/user', oauth.bearer, wrap(async (req, res) => { let accessToken = req.oauth2.accessToken let user = await uapi.User.get(accessToken.user_id) + if (!user) { return res.status(404).jsonp({ error: 'No such user' @@ -42,14 +43,17 @@ router.get('/user', oauth.bearer, wrap(async (req, res) => { let udata = { id: user.id, - name: user.display_name, + username: user.username, + display_name: user.display_name, avatar_file: user.avatar_file } + // Include Email if (accessToken.scope.indexOf('email') != -1) { udata.email = user.email } + // Include privilege number if (accessToken.scope.indexOf('privilege') != -1) { udata.privilege = user.nw_privilege } diff --git a/src/script/main.js b/src/script/main.js index 29dde45..7453877 100644 --- a/src/script/main.js +++ b/src/script/main.js @@ -2,9 +2,9 @@ window.$ = require('jquery') $(document).ready(function () { if (window.location.hash) { - let hash = window.location.hash - if ($(hash).length) { - $(window).scrollTop($(hash).offset().top - $('.navigator').innerHeight() * 2) + var locha = window.location.hash + if ($(locha).length) { + $(window).scrollTop($(locha).offset().top - $('.navigator').innerHeight() * 2) } } @@ -30,7 +30,7 @@ $(document).ready(function () { if (!$(this.hash).length) return e.preventDefault() - let dest = 0 + var dest = 0 if ($(this.hash).offset().top > $(document).height() - $(window).height()) { dest = $(document).height() - $(window).height() } else { @@ -55,8 +55,8 @@ $(document).ready(function () { if ($('#repeatcheck').length) { function pwcheck (e) { - let pw = $('#password').val() - let pwa = $('#password_repeat').val() + var pw = $('#password').val() + var pwa = $('#password_repeat').val() if (pwa !== pw) { $('#password_repeat').addClass('invalid') $('#repeatcheck').show() @@ -76,6 +76,30 @@ $(document).ready(function () { }) } + if ($('.newsfeed').length) { + $.ajax({ + type: 'get', + url: '/api/news', + dataType: 'json', + success: function (data) { + if (!data.length) { + return $('.newsfeed').html('There is nothing to show at this moment.') + } + var html = '' + for (var i in data) { + var article = data[i] + html += '
' + html += '' + article.title + '' + html += 'Published at ' + new Date(article.created_at) + '' + html += '
' + article.content + '
' + html += 'Read More' + html += '
' + } + $('.newsfeed').html(html) + } + }) + } + window.checkLoginState = function () { FB.getLoginStatus(function(response) { $.ajax({ @@ -83,7 +107,7 @@ $(document).ready(function () { url: '/api/external/facebook/callback', dataType: 'json', data: response, - success: (data) => { + success: function (data) { if (data.error) { $('.message').addClass('error') $('.message span').text(data.error) diff --git a/src/style/main.styl b/src/style/main.styl index 03fffe8..f64ad46 100644 --- a/src/style/main.styl +++ b/src/style/main.styl @@ -216,7 +216,7 @@ input:not([type="submit"]) box-shadow: inset 2px 2px 5px #ddd transition: border 0.1s linear -input[type="submit"] +.button, input[type="submit"] display: block padding: 5px 10px background-color: #fbfbfb @@ -226,6 +226,9 @@ input[type="submit"] margin: 10px 0 cursor: pointer +.button + display: inline-block + .boxcont .box .left, .right @@ -249,6 +252,14 @@ input[type="submit"] h1, h2, h3 margin-top: 0 +.pgn + display: inline-block + margin-left: 10px + +.pagenum + display: inline-block + padding: 10px + .dlbtn display: block img @@ -371,6 +382,45 @@ input.invalid color: red padding: 10px +.article + .title + font-size: 200% + font-weight: bold + .author + font-size: 80% + color: #565656 + .timestamp + display: inline-block + .content + margin-top: 20px + margin-bottom: 20px + +.newsfeed + .load + font-size: 120% + span + vertical-align: super + margin-left: 10px + .prvarticle + margin-bottom: 10px + .title + font-size: 200% + display: block + font-weight: bold + .timestamp + font-size: 80% + color: #565656 + .prvcontent + margin: 10px 0 + +.return + margin-top: 40px + +.content + hr + border-color: #ffffff + margin: 20px 0 + @media all and (max-width: 800px) .navigator padding: 0 10px diff --git a/views/article.pug b/views/article.pug new file mode 100644 index 0000000..3191332 --- /dev/null +++ b/views/article.pug @@ -0,0 +1,24 @@ +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/index.pug b/views/index.pug index 6fddc2a..52ed2ef 100644 --- a/views/index.pug +++ b/views/index.pug @@ -22,3 +22,8 @@ block body section#news .content h1 Icy Network News + .newsfeed + span.load + i.fa.fa-spin.fa-spinner.fa-2x + span Loading feed + a.older(href="/news") View older articles diff --git a/views/news.pug b/views/news.pug new file mode 100644 index 0000000..1c05b51 --- /dev/null +++ b/views/news.pug @@ -0,0 +1,34 @@ +extends layout.pug + +block title + |Icy Network - News + +block body + .document + .content + h1 Icy Network News + if news.error + span.error There are no articles to show. + else + if news.page + span.pagenum + |Page #{news.page.page} of #{news.page.pages} + .pgn + if news.page.page > 1 + a.button(href="/news/?page=" + news.page.page - 1) Previous + - var n = 0 + while n < news.page.pages + a.button(href="/news/?page=" + (n + 1))= n + 1 + - n++ + if news.page.pages > news.page.page + a.button(href="/news/?page=" + news.page.page + 1) Next + each val in news.articles + .article + a.title(href="/news/" + val.id + "-" + val.slug)= val.title + .author Published by + span #{val.author.display_name} + |at + .timestamp #{new Date(val.created_at)} + .content!= val.content + hr +