add simple oauth2 client management to admin
This commit is contained in:
parent
70cbbecec2
commit
de3f8498c2
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Reference in New Issue
Block a user