diff --git a/scripts/logger.js b/scripts/logger.js index a4c9dd2..1c155e1 100644 --- a/scripts/logger.js +++ b/scripts/logger.js @@ -26,8 +26,8 @@ console.warn = function () { const realConsoleError = console.error console.error = function () { - process.stdout.write('\x1b[2K\r') - process.stdout.write('[ err] [' + dateFormat(new Date()) + '] ') + process.stderr.write('\x1b[2K\r') + process.stderr.write('[ err] [' + dateFormat(new Date()) + '] ') realConsoleError.apply(this, arguments) } diff --git a/server/api/admin.js b/server/api/admin.js index 2866b4d..4975d2b 100644 --- a/server/api/admin.js +++ b/server/api/admin.js @@ -3,7 +3,7 @@ import Models from './models' const perPage = 6 -function cleanUserObject (dbe) { +function cleanUserObject (dbe, admin) { return { id: dbe.id, username: dbe.username, @@ -12,10 +12,11 @@ function cleanUserObject (dbe) { avatar_file: dbe.avatar_file, activated: dbe.activated === 1, locked: dbe.locked === 1, - ip_addess: dbe.ip_addess, + ip_address: dbe.ip_address, password: dbe.password !== null, nw_privilege: dbe.nw_privilege, - created_at: dbe.created_at + created_at: dbe.created_at, + bannable: dbe.nw_privilege < admin.nw_privilege && dbe.id !== admin.id } } @@ -40,8 +41,29 @@ async function cleanClientObject (dbe) { } } +async function cleanBanObject (dbe) { + let user = await Users.User.get(dbe.user_id) + let admin = await Users.User.get(dbe.admin_id) + return { + id: dbe.id, + reason: dbe.reason, + user: { + id: user.id, + display_name: user.display_name + }, + admin: { + id: admin.id, + display_name: admin.display_name + }, + expires_at: dbe.expires_at, + created_at: dbe.created_at, + ip_address: dbe.associated_ip, + expired: dbe.expires_at && new Date(dbe.expires_at).getTime() < Date.now() + } +} + const API = { - getAllUsers: async function (page) { + getAllUsers: async function (page, adminId) { let count = await Models.User.query().count('id as ids') if (!count.length || !count[0]['ids'] || isNaN(page)) { return {error: 'No users found'} @@ -50,12 +72,13 @@ const API = { count = count[0].ids let paginated = Users.Pagination(perPage, parseInt(count), page) let raw = await Models.User.query().offset(paginated.offset).limit(perPage) + let admin = await Users.User.get(adminId) let users = [] for (let i in raw) { let entry = raw[i] - users.push(cleanUserObject(entry)) + users.push(cleanUserObject(entry, admin)) } return { @@ -110,6 +133,7 @@ const API = { try { await Models.OAuth2Client.query().patchAndFetchById(id, data) + await Models.OAuth2AuthorizedClient.query().delete().where('client_id', id) } catch (e) { return {error: 'No such client'} } @@ -159,6 +183,54 @@ const API = { await Models.OAuth2AccessToken.query().delete().where('client_id', id) await Models.OAuth2RefreshToken.query().delete().where('client_id', id) return true + }, + getAllBans: async function (page) { + let count = await Models.Ban.query().count('id as ids') + if (!count.length || !count[0]['ids'] || isNaN(page)) { + return {error: 'No bans on record'} + } + + count = count[0].ids + let paginated = Users.Pagination(perPage, parseInt(count), page) + let raw = await Models.Ban.query().offset(paginated.offset).limit(perPage) + + let bans = [] + for (let i in raw) { + let entry = raw[i] + + bans.push(await cleanBanObject(entry)) + } + + return { + page: paginated, + bans: bans + } + }, + removeBan: async function (banId) { + if (isNaN(banId)) return {error: 'Invalid number'} + return Models.Ban.query().delete().where('id', banId) + }, + addBan: async function (data, adminId) { + let user = await Users.User.get(parseInt(data.user_id)) + + if (!user) return {error: 'No such user.'} + if (user.id === adminId) return {error: 'Cannot ban yourself!'} + + let admin = await Users.User.get(adminId) + + if (user.nw_privilege > admin.nw_privilege) return {error: 'Cannot ban user.'} + + let banAdd = { + reason: data.reason || 'Unspecified ban', + admin_id: adminId, + user_id: user.id, + expires_at: data.expires_at != null ? new Date(data.expires_at) : null, + created_at: new Date(), + associated_ip: data.ip_address || user.ip_address || null + } + + await Models.Ban.query().insert(banAdd) + return {} } } diff --git a/server/routes/admin.js b/server/routes/admin.js index ff41940..812e8b5 100644 --- a/server/routes/admin.js +++ b/server/routes/admin.js @@ -49,7 +49,7 @@ router.post('/', wrap(async (req, res, next) => { let passReady = await User.Login.password(req.session.user, req.body.password) if (passReady) { - req.session.accesstime = Date.now() + 300000 // 5 minutes + req.session.accesstime = Date.now() + 600000 // 10 minutes return res.redirect('/admin') } else { req.flash('message', {error: true, text: 'Invalid password'}) @@ -62,7 +62,7 @@ router.post('/', wrap(async (req, res, next) => { router.use(wrap(async (req, res, next) => { if (req.session.accesstime) { if (req.session.accesstime > Date.now()) { - req.session.accesstime = Date.now() + 300000 + req.session.accesstime = Date.now() + 600000 return next() } @@ -96,10 +96,14 @@ apiRouter.get('/users', wrap(async (req, res) => { page = 1 } - let users = await API.getAllUsers(page) + let users = await API.getAllUsers(page, req.session.user.id) res.jsonp(users) })) +/* =============== + * OAuth2 Data + * =============== + */ apiRouter.get('/clients', wrap(async (req, res) => { let page = parseInt(req.query.page) if (isNaN(page) || page < 1) { @@ -177,6 +181,49 @@ apiRouter.post('/client/delete/:id', wrap(async (req, res) => { res.jsonp(client) })) +/* ======== + * Bans + * ======== + */ + +apiRouter.get('/bans', wrap(async (req, res) => { + let page = parseInt(req.query.page) + if (isNaN(page) || page < 1) { + page = 1 + } + + let bans = await API.getAllBans(page) + res.jsonp(bans) +})) + +apiRouter.post('/ban/pardon/:id', wrap(async (req, res) => { + let id = parseInt(req.params.id) + if (isNaN(id)) { + return res.status(400).jsonp({error: 'Invalid number'}) + } + + let ban = await API.removeBan(id) + if (ban.error) { + return res.status(400).jsonp({error: ban.error}) + } + + res.jsonp(ban) +})) + +apiRouter.post('/ban', wrap(async (req, res) => { + if (!req.body.user_id) return res.status(400).jsonp({error: 'ID missing'}) + if (req.body.csrf !== req.session.csrf) { + return res.status(400).jsonp({error: 'Invalid session'}) + } + + let result = await API.addBan(req.body, req.session.user.id) + if (result.error) { + return res.status(400).jsonp({error: result.error}) + } + + res.jsonp(result) +})) + apiRouter.use((err, req, res, next) => { console.error(err) return res.status(500).jsonp({error: 'Internal server error'}) diff --git a/server/routes/index.js b/server/routes/index.js index a402b9c..61b1151 100644 --- a/server/routes/index.js +++ b/server/routes/index.js @@ -53,7 +53,9 @@ router.use(wrap(async (req, res, next) => { } if (req.session.user.session_refresh < Date.now()) { - // Check for ban + console.debug('User session update') + + // Check for bans let banStatus = await API.User.getBanStatus(req.session.user.id) if (banStatus.length) { @@ -63,6 +65,9 @@ router.use(wrap(async (req, res, next) => { // Update user session let udata = await API.User.get(req.session.user.id) + + // Update IP address + await API.User.update(udata, {ip_address: req.realIP}) setSession(req, udata) } } diff --git a/src/script/admin.js b/src/script/admin.js index 25cedc5..c728cf6 100644 --- a/src/script/admin.js +++ b/src/script/admin.js @@ -25,6 +25,67 @@ function paginationButton (pages) { return html } +function banUser (id) { + window.Dialog.openTemplate('Ban User', 'banNew', {id: id}) + $('#fnsubmit').submit(function (e) { + e.preventDefault() + $.post({ + url: '/admin/api/ban', + data: $(this).serialize(), + success: function (data) { + window.Dialog.close() + loadBans(1) + }, + error: function (e) { + if (e.responseJSON && e.responseJSON.error) { + $('form .message').show() + $('form .message').text(e.responseJSON.error) + } + } + }) + }) +} + +function loadBans (page) { + $.ajax({ + type: 'get', + url: '/admin/api/bans', + data: {page: page}, + success: function (data) { + $('#banlist').html('') + if (data.error) { + $('#banlist').html('
' + data.error + '
') + return + } + + var pgbtn = paginationButton(data.page) + $('#banlist').append(pgbtn) + $('#banlist .pgn .button').click(function (e) { + var pgnum = $(this).data('page') + if (pgnum == null) return + loadBans(parseInt(pgnum)) + }) + + for (var u in data.bans) { + var ban = data.bans[u] + ban.created_at = new Date(ban.created_at) + ban.expires_at = ban.expires_at === null ? 'Never' : new Date(ban.expires_at) + var tmp = buildTemplateScript('ban', ban) + $('#banlist').append(tmp) + } + + $('#banlist .remove').click(function (e) { + $.post({ + url: '/admin/api/ban/pardon/' + parseInt($(this).data('id')), + success: function (data) { + loadBans(1) + } + }) + }) + } + }) +} + function loadUsers (page) { $.ajax({ type: 'get', @@ -39,7 +100,7 @@ function loadUsers (page) { var pgbtn = paginationButton(data.page) $('#userlist').append(pgbtn) - $('.pgn .button').click(function (e) { + $('#userlist .pgn .button').click(function (e) { var pgnum = $(this).data('page') if (pgnum == null) return loadUsers(parseInt(pgnum)) @@ -51,6 +112,10 @@ function loadUsers (page) { var tmp = buildTemplateScript('user', user) $('#userlist').append(tmp) } + + $('#userlist .ban').click(function (e) { + banUser(parseInt($(this).data('id'))) + }) } }) } @@ -110,7 +175,7 @@ function loadClients (page) { var pgbtn = paginationButton(data.page) $('#clientlist').append(pgbtn) - $('.pgn .button').click(function (e) { + $('#clientlist .pgn .button').click(function (e) { var pgnum = $(this).data('page') if (pgnum == null) return loadClients(parseInt(pgnum)) @@ -123,17 +188,17 @@ function loadClients (page) { $('#clientlist').append(tmp) } - $('.edit').click(function (e) { + $('#clientlist .edit').click(function (e) { var client = $(this).data('client') editClient(parseInt(client)) }) - $('.delete').click(function (e) { + $('#clientlist .delete').click(function (e) { var client = $(this).data('client') deleteClient(parseInt(client)) }) - $('.newsecret').click(function (e) { + $('#clientlist .newsecret').click(function (e) { var client = $(this).data('client') $.post({ url: '/admin/api/client/new_secret/' + parseInt(client), @@ -175,6 +240,10 @@ $(document).ready(function () { loadUsers(1) } + if ($('#banlist').length) { + loadBans(1) + } + if ($('#clientlist').length) { loadClients(1) diff --git a/src/style/admin.styl b/src/style/admin.styl index 9106411..0602e58 100644 --- a/src/style/admin.styl +++ b/src/style/admin.styl @@ -40,6 +40,8 @@ nav display: inline-block .right float: right + .stamps + float: right .user min-height: 180px diff --git a/views/admin/index.pug b/views/admin/index.pug index 3b325d0..771c86c 100644 --- a/views/admin/index.pug +++ b/views/admin/index.pug @@ -8,6 +8,10 @@ block body .users h3 Registered Users #userlist + .right + .users + h3 Bans + #banlist .templates script(type="x-tmpl-mustache" id="user").
@@ -22,7 +26,7 @@ block body
{{^activated}} -
+
{{/activated}}
{{display_name}}
@@ -33,5 +37,33 @@ block body {{^password}}
Used external login
{{/password}} + {{#bannable}} +
Ban User
+ {{/bannable}}
+ script(type="x-tmpl-mustache" id="ban"). +
+
+ {{#expired}} +
+ {{/expired}} +
+
User: {{user.display_name}}
+
Admin: {{admin.display_name}}
+
Reason: {{reason}}
+
Placed {{created_at}}
+
Expires {{expires_at}}
+
Pardon
+
+ script(type="x-tmpl-mustache" id="banNew"). +
+
+ + + + + + + +
diff --git a/views/admin/layout.pug b/views/admin/layout.pug index 83d277f..c4c6adb 100644 --- a/views/admin/layout.pug +++ b/views/admin/layout.pug @@ -5,6 +5,7 @@ html 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}') diff --git a/views/document.pug b/views/document.pug index 4159a25..4a3fbeb 100644 --- a/views/document.pug +++ b/views/document.pug @@ -1,7 +1,7 @@ extends layout.pug block title - |Icy Network - Legal Notice + |Icy Network block body .document