add simple oauth2 client management to admin

This commit is contained in:
Evert Prants 2017-08-28 20:32:54 +03:00
parent 70cbbecec2
commit de3f8498c2
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
7 changed files with 472 additions and 31 deletions

View File

@ -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
}
}

View File

@ -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

View File

@ -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('<div class="message error">' + data.error + '</div>')
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 = '<div class="pad">' + content + '</div>'
}
$('#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()

View File

@ -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

View File

@ -8,4 +8,30 @@ block body
.users
h3 Registered Users
#userlist
.right
.templates
script(type="x-tmpl-mustache" id="user").
<div class="user" id="user-{{id}}">
<div class="avatar">
{{#avatar_file}}
<img src="/usercontent/images/{{avatar_file}}">
{{/avatar_file}}
{{^avatar_file}}
<img src="/static/image/avatar.png">
{{/avatar_file}}
</div>
<div class="info">
<div class="stamps">
{{^activated}}
<div class="noactive"><i class="fa fa-fw fa-envelope"></i></div>
{{/activated}}
</div>
<div class="display_name">{{display_name}}</div>
<div class="username">{{id}} - {{username}}</div>
<div class="email">{{email}}</div>
<div class="privilege">Privilege: {{nw_privilege}} points</div>
<div class="timestamp">{{created_at}}</div>
{{^password}}
<div class="external"><b>Used external login</b></div>
{{/password}}
</div>
</div>

View File

@ -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").
<div class="user" id="user-{{id}}">
<div class="avatar">
{{#avatar_file}}
<img src="/usercontent/images/{{avatar_file}}">
{{/avatar_file}}
{{^avatar_file}}
<img src="/static/image/avatar.png">
{{/avatar_file}}
</div>
<div class="info">
<div class="stamps">
{{^activated}}
<div class="noactive"><i class="fa fa-fw fa-envelope"></i></div>
{{/activated}}
</div>
<div class="display_name">{{display_name}}</div>
<div class="username">{{username}}</div>
<div class="email">{{email}}</div>
<div class="privilege">Privilege: {{nw_privilege}} points</div>
<div class="timestamp">{{created_at}}</div>
{{^password}}
<div class="external"><b>Used external login</b></div>
{{/password}}
</div>
</div>

View File

@ -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").
<div class="application" id="client-{{id}}">
<div class="picture">
{{#icon}}
<img src="/usercontent/images/{{icon}}">
{{/icon}}
{{^icon}}
<div class="noicon"><i class="fa fa-fw fa-gears"></i></div>
{{/icon}}
</div>
<div class="info">
<div class="stamps">
{{#verified}}
<div class="verified"><i class="fa fa-fw fa-check"></i></div>
{{/verified}}
</div>
<div class="name">{{title}}</div>
<div class="description">{{description}}</div>
<a class="url" href="{{url}}" target="_blank" rel="nofollow">{{url}}</a>
<div class="scope">Scopes: {{scope}}</div>
<div class="redirect_url">Redirect: {{redirect_url}}</div>
<div class="id">Client ID: {{id}}</div>
<div class="secret">Client Secret: <div id="hiddensecret">{{secret}}</div>
<div class="link" id="showbutton" onclick="$(this).parent().find('#hiddensecret').toggleClass('shown')">Show</div>
</div>
<div class="button edit" data-client="{{id}}">Edit</div>
<div class="button delete" data-client="{{id}}">Delete</div>
<div class="button newsecret" data-client="{{id}}">New Secret</div>
</div>
</div>
script(type="x-tmpl-mustache" id="clientEdit").
<form id="ffsubmit">
<div class="message error"></div>
<input type="hidden" name="id" value="{{id}}">
<input type="hidden" name="csrf" value="#{csrf}">
<label for="title">Title</label>
<input type="text" id="title" name="title" value="{{title}}">
<label for="description">Description</label>
<input type="text" id="description" name="description" value="{{description}}">
<label for="url">URL</label>
<input type="text" id="url" name="url" value="{{url}}">
<label for="scope">Scope</label>
<input type="text" id="scope" name="scope" value="{{scope}}">
<label for="redirect_url">Redirect</label>
<input type="text" id="redirect_url" name="redirect_url" value="{{redirect_url}}">
<input type="submit" value="Edit">
</form>
script(type="x-tmpl-mustache" id="clientNew").
<form id="fnsubmit">
<div class="message error"></div>
<input type="hidden" name="csrf" value="#{csrf}">
<label for="title">Title</label>
<input type="text" id="title" name="title">
<label for="description">Description</label>
<input type="text" id="description" name="description">
<label for="url">URL</label>
<input type="text" id="url" name="url">
<label for="scope">Scope</label>
<input type="text" id="scope" name="scope">
<label for="redirect_url">Redirect</label>
<input type="text" id="redirect_url" name="redirect_url">
<input type="submit" value="Create">
</form>
script(type="x-tmpl-mustache" id="clientRemove").
<p>Are you sure?</p>
<div class="button" onclick="window.Dialog.close()">No</div>
<div class="button" id="fremove">Yes, I'm sure</div>