From de3f8498c2b97de1389e92c8fbe73a65da6460ae Mon Sep 17 00:00:00 2001 From: Evert Date: Mon, 28 Aug 2017 20:32:54 +0300 Subject: [PATCH] add simple oauth2 client management to admin --- server/api/admin.js | 118 ++++++++++++++++++++++++++++++++++ server/routes/admin.js | 90 +++++++++++++++++++++++++- src/script/admin.js | 142 ++++++++++++++++++++++++++++++++++++++++- src/style/admin.styl | 20 ++++++ views/admin/index.pug | 28 +++++++- views/admin/layout.pug | 35 +++------- views/admin/oauth2.pug | 70 ++++++++++++++++++++ 7 files changed, 472 insertions(+), 31 deletions(-) diff --git a/server/api/admin.js b/server/api/admin.js index 65ecb56..2866b4d 100644 --- a/server/api/admin.js +++ b/server/api/admin.js @@ -19,6 +19,27 @@ function cleanUserObject (dbe) { } } +async function cleanClientObject (dbe) { + let user = await Users.User.get(dbe.user_id) + return { + id: dbe.id, + title: dbe.title, + description: dbe.description, + url: dbe.url, + redirect_url: dbe.redirect_url, + grants: dbe.grants, + icon: dbe.icon, + user: { + id: user.id, + display_name: user.display_name + }, + scope: dbe.scope, + secret: dbe.secret, + verified: dbe.verified === 1, + created_at: dbe.created_at + } +} + const API = { getAllUsers: async function (page) { let count = await Models.User.query().count('id as ids') @@ -41,6 +62,103 @@ const API = { page: paginated, users: users } + }, + getAllClients: async function (page) { + let count = await Models.OAuth2Client.query().count('id as ids') + if (!count.length || !count[0]['ids'] || isNaN(page)) { + return {error: 'No clients found'} + } + + count = count[0].ids + let paginated = Users.Pagination(perPage, parseInt(count), page) + let raw = await Models.OAuth2Client.query().offset(paginated.offset).limit(perPage) + + let clients = [] + for (let i in raw) { + let entry = raw[i] + + clients.push(await cleanClientObject(entry)) + } + + return { + page: paginated, + clients: clients + } + }, + getClient: async function (id) { + let raw = await Models.OAuth2Client.query().where('id', id) + if (!raw.length) return null + + return cleanClientObject(raw[0]) + }, + updateClient: async function (id, data) { + if (isNaN(id)) return {error: 'Invalid client ID'} + + let fields = [ + 'title', 'description', 'url', 'redirect_url', 'scope' + ] + + for (let i in data) { + if (fields.indexOf(i) === -1) { + delete data[i] + } + } + + for (let i in fields) { + if (!data[fields[i]] && fields[i] !== 'scope') return {error: 'Missing fields'} + } + + try { + await Models.OAuth2Client.query().patchAndFetchById(id, data) + } catch (e) { + return {error: 'No such client'} + } + + return {} + }, + newSecret: async function (id) { + if (isNaN(id)) return {error: 'Invalid client ID'} + let secret = Users.Hash(16) + + try { + await Models.OAuth2Client.query().patchAndFetchById(id, {secret: secret}) + } catch (e) { + return {error: 'No such client'} + } + + return {} + }, + createClient: async function (data, user) { + let fields = [ + 'title', 'description', 'url', 'redirect_url', 'scope' + ] + + for (let i in data) { + if (fields.indexOf(i) === -1) { + delete data[i] + } + } + + for (let i in fields) { + if (!data[fields[i]] && fields[i] !== 'scope') return {error: 'Missing fields'} + } + + let obj = Object.assign({ + secret: Users.Hash(16), + grants: 'authorization_code', + created_at: new Date(), + user_id: user.id + }, data) + + return Models.OAuth2Client.query().insert(obj) + }, + removeClient: async function (id) { + if (isNaN(id)) return {error: 'Invalid number'} + await Models.OAuth2Client.query().delete().where('id', id) + await Models.OAuth2AuthorizedClient.query().delete().where('client_id', id) + await Models.OAuth2AccessToken.query().delete().where('client_id', id) + await Models.OAuth2RefreshToken.query().delete().where('client_id', id) + return true } } diff --git a/server/routes/admin.js b/server/routes/admin.js index 4262ced..ff41940 100644 --- a/server/routes/admin.js +++ b/server/routes/admin.js @@ -30,7 +30,7 @@ router.use(wrap(async (req, res, next) => { * ================ */ -apiRouter.get('/access', (req, res) => { +router.get('/access', (req, res) => { if (!req.session.accesstime || req.session.accesstime < Date.now()) { return res.status(401).jsonp({error: 'Access expired'}) } @@ -61,7 +61,11 @@ router.post('/', wrap(async (req, res, next) => { // Ensure that the admin panel is not kept open for prolonged time router.use(wrap(async (req, res, next) => { if (req.session.accesstime) { - if (req.session.accesstime > Date.now()) return next() + if (req.session.accesstime > Date.now()) { + req.session.accesstime = Date.now() + 300000 + return next() + } + delete req.session.accesstime } @@ -96,6 +100,88 @@ apiRouter.get('/users', wrap(async (req, res) => { res.jsonp(users) })) +apiRouter.get('/clients', wrap(async (req, res) => { + let page = parseInt(req.query.page) + if (isNaN(page) || page < 1) { + page = 1 + } + + let clients = await API.getAllClients(page) + res.jsonp(clients) +})) + +apiRouter.get('/client/:id', wrap(async (req, res) => { + let id = parseInt(req.params.id) + if (isNaN(id)) { + return res.status(400).jsonp({error: 'Invalid number'}) + } + + let client = await API.getClient(id) + if (!client) return res.status(400).jsonp({error: 'Invalid client'}) + + res.jsonp(client) +})) + +apiRouter.post('/client/new', wrap(async (req, res) => { + if (req.body.csrf !== req.session.csrf) { + return res.status(400).jsonp({error: 'Invalid session'}) + } + + let update = await API.createClient(req.body, req.session.user) + if (update.error) { + return res.status(400).jsonp({error: update.error}) + } + + res.status(204).end() +})) + +apiRouter.post('/client/update', wrap(async (req, res) => { + if (!req.body.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 update = await API.updateClient(parseInt(req.body.id), req.body) + if (update.error) { + return res.status(400).jsonp({error: update.error}) + } + + res.status(204).end() +})) + +apiRouter.post('/client/new_secret/:id', wrap(async (req, res) => { + let id = parseInt(req.params.id) + if (isNaN(id)) { + return res.status(400).jsonp({error: 'Invalid number'}) + } + + let client = await API.newSecret(id) + if (client.error) { + return res.status(400).jsonp({error: client.error}) + } + + res.jsonp(client) +})) + +apiRouter.post('/client/delete/:id', wrap(async (req, res) => { + let id = parseInt(req.params.id) + if (isNaN(id)) { + return res.status(400).jsonp({error: 'Invalid number'}) + } + + let client = await API.removeClient(id) + if (client.error) { + return res.status(400).jsonp({error: client.error}) + } + + res.jsonp(client) +})) + +apiRouter.use((err, req, res, next) => { + console.error(err) + return res.status(500).jsonp({error: 'Internal server error'}) +}) + router.use('/api', apiRouter) module.exports = router diff --git a/src/script/admin.js b/src/script/admin.js index 732861d..25cedc5 100644 --- a/src/script/admin.js +++ b/src/script/admin.js @@ -55,14 +55,154 @@ function loadUsers (page) { }) } +function editClient (id) { + $.ajax({ + type: 'get', + url: '/admin/api/client/' + id, + success: function (data) { + window.Dialog.openTemplate('Editing client', 'clientEdit', data) + $('#ffsubmit').submit(function (e) { + e.preventDefault() + $.ajax({ + type: 'post', + url: '/admin/api/client/update', + data: $(this).serialize(), + success: function (data) { + window.Dialog.close() + loadClients(1) + }, + error: function (e) { + if (e.responseJSON && e.responseJSON.error) { + $('form .message').show() + $('form .message').text(e.responseJSON.error) + } + } + }) + }) + } + }) +} + +function deleteClient (id) { + window.Dialog.openTemplate('Deleting client', 'clientRemove') + $('#fremove').click(function (e) { + $.post({ + url: '/admin/api/client/delete/' + id, + success: function (data) { + window.Dialog.close() + loadClients(1) + } + }) + }) +} + +function loadClients (page) { + $.ajax({ + type: 'get', + url: '/admin/api/clients', + data: {page: page}, + success: function (data) { + $('#clientlist').html('') + if (data.error) { + $('#clientlist').html('
' + data.error + '
') + return + } + + var pgbtn = paginationButton(data.page) + $('#clientlist').append(pgbtn) + $('.pgn .button').click(function (e) { + var pgnum = $(this).data('page') + if (pgnum == null) return + loadClients(parseInt(pgnum)) + }) + + for (var u in data.clients) { + var client = data.clients[u] + client.created_at = new Date(client.created_at) + var tmp = buildTemplateScript('client', client) + $('#clientlist').append(tmp) + } + + $('.edit').click(function (e) { + var client = $(this).data('client') + editClient(parseInt(client)) + }) + + $('.delete').click(function (e) { + var client = $(this).data('client') + deleteClient(parseInt(client)) + }) + + $('.newsecret').click(function (e) { + var client = $(this).data('client') + $.post({ + url: '/admin/api/client/new_secret/' + parseInt(client), + success: function (e) { + loadClients(1) + } + }) + }) + } + }) +} + $(document).ready(function () { + window.Dialog = $('#dialog') + window.Dialog.open = function (title, content, pad) { + $('#dialog #title').text(title) + if (pad) { + content = '
' + content + '
' + } + $('#dialog #content').html(content) + $('#dialog').fadeIn() + } + + window.Dialog.close = function () { + $('#dialog').fadeOut('fast', function () { + $('#dialog #content').html('') + }) + } + + window.Dialog.openTemplate = function (title, template, data = {}) { + window.Dialog.open(title, buildTemplateScript(template, data), true) + } + + $('#dialog #close').click(function (e) { + window.Dialog.close() + }) + if ($('#userlist').length) { loadUsers(1) } + if ($('#clientlist').length) { + loadClients(1) + + $('#new').click(function (e) { + window.Dialog.openTemplate('New Client', 'clientNew') + $('#fnsubmit').submit(function (e) { + e.preventDefault() + $.post({ + url: '/admin/api/client/new', + data: $(this).serialize(), + success: function (data) { + window.Dialog.close() + loadClients(1) + }, + error: function (e) { + if (e.responseJSON && e.responseJSON.error) { + $('form .message').show() + $('form .message').text(e.responseJSON.error) + } + } + }) + }) + }) + } + setInterval(function () { $.get({ - url: '/admin/api/access', + url: '/admin/access', success: function (data) { if (data && data.access) return window.location.reload() diff --git a/src/style/admin.styl b/src/style/admin.styl index cacc95b..9106411 100644 --- a/src/style/admin.styl +++ b/src/style/admin.styl @@ -35,6 +35,11 @@ nav padding: 10px background-color: #fff min-height: 100vh + .left, .right + width: 48% + display: inline-block + .right + float: right .user min-height: 180px @@ -47,3 +52,18 @@ nav font-size: 120% .username font-size: 80% + +.application + height: 200px + #hiddensecret + display: none + &.shown + display: inline-block + .link + color: green + cursor: pointer + display: inline-block + +form + .message + display: none diff --git a/views/admin/index.pug b/views/admin/index.pug index 4b901dd..3b325d0 100644 --- a/views/admin/index.pug +++ b/views/admin/index.pug @@ -8,4 +8,30 @@ block body .users h3 Registered Users #userlist - .right + .templates + script(type="x-tmpl-mustache" id="user"). +
+
+ {{#avatar_file}} + + {{/avatar_file}} + {{^avatar_file}} + + {{/avatar_file}} +
+
+
+ {{^activated}} +
+ {{/activated}} +
+
{{display_name}}
+
{{id}} - {{username}}
+ +
Privilege: {{nw_privilege}} points
+
{{created_at}}
+ {{^password}} +
Used external login
+ {{/password}} +
+
diff --git a/views/admin/layout.pug b/views/admin/layout.pug index 096dec7..83d277f 100644 --- a/views/admin/layout.pug +++ b/views/admin/layout.pug @@ -26,32 +26,13 @@ html ul.right li a(href="/user/manage") #{user.display_name} + block dialog + .dialog-drop#dialog + .dialog + .head + #title + #close + i.fa.fa-fw.fa-times + .content#content .wrapper block body - .templates - script(type="x-tmpl-mustache" id="user"). -
-
- {{#avatar_file}} - - {{/avatar_file}} - {{^avatar_file}} - - {{/avatar_file}} -
-
-
- {{^activated}} -
- {{/activated}} -
-
{{display_name}}
-
{{username}}
- -
Privilege: {{nw_privilege}} points
-
{{created_at}}
- {{^password}} -
Used external login
- {{/password}} -
-
diff --git a/views/admin/oauth2.pug b/views/admin/oauth2.pug index 06156c2..91f30fb 100644 --- a/views/admin/oauth2.pug +++ b/views/admin/oauth2.pug @@ -4,3 +4,73 @@ block body .container .content h1 Manage OAuth2 Clients + .button(id="new") New Client + #clientlist + .templates + script(type="x-tmpl-mustache" id="client"). +
+
+ {{#icon}} + + {{/icon}} + {{^icon}} +
+ {{/icon}} +
+
+
+ {{#verified}} +
+ {{/verified}} +
+
{{title}}
+
{{description}}
+ {{url}} +
Scopes: {{scope}}
+
Redirect: {{redirect_url}}
+
Client ID: {{id}}
+
Client Secret:
{{secret}}
+ +
+
Edit
+
Delete
+
New Secret
+
+
+ script(type="x-tmpl-mustache" id="clientEdit"). +
+
+ + + + + + + + + + + + + +
+ script(type="x-tmpl-mustache" id="clientNew"). +
+
+ + + + + + + + + + + + +
+ script(type="x-tmpl-mustache" id="clientRemove"). +

Are you sure?

+
No
+
Yes, I'm sure