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 = {
|
const API = {
|
||||||
getAllUsers: async function (page) {
|
getAllUsers: async function (page) {
|
||||||
let count = await Models.User.query().count('id as ids')
|
let count = await Models.User.query().count('id as ids')
|
||||||
@ -41,6 +62,103 @@ const API = {
|
|||||||
page: paginated,
|
page: paginated,
|
||||||
users: users
|
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()) {
|
if (!req.session.accesstime || req.session.accesstime < Date.now()) {
|
||||||
return res.status(401).jsonp({error: 'Access expired'})
|
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
|
// Ensure that the admin panel is not kept open for prolonged time
|
||||||
router.use(wrap(async (req, res, next) => {
|
router.use(wrap(async (req, res, next) => {
|
||||||
if (req.session.accesstime) {
|
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
|
delete req.session.accesstime
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,6 +100,88 @@ apiRouter.get('/users', wrap(async (req, res) => {
|
|||||||
res.jsonp(users)
|
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)
|
router.use('/api', apiRouter)
|
||||||
|
|
||||||
module.exports = router
|
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 () {
|
$(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) {
|
if ($('#userlist').length) {
|
||||||
loadUsers(1)
|
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 () {
|
setInterval(function () {
|
||||||
$.get({
|
$.get({
|
||||||
url: '/admin/api/access',
|
url: '/admin/access',
|
||||||
success: function (data) {
|
success: function (data) {
|
||||||
if (data && data.access) return
|
if (data && data.access) return
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
|
@ -35,6 +35,11 @@ nav
|
|||||||
padding: 10px
|
padding: 10px
|
||||||
background-color: #fff
|
background-color: #fff
|
||||||
min-height: 100vh
|
min-height: 100vh
|
||||||
|
.left, .right
|
||||||
|
width: 48%
|
||||||
|
display: inline-block
|
||||||
|
.right
|
||||||
|
float: right
|
||||||
|
|
||||||
.user
|
.user
|
||||||
min-height: 180px
|
min-height: 180px
|
||||||
@ -47,3 +52,18 @@ nav
|
|||||||
font-size: 120%
|
font-size: 120%
|
||||||
.username
|
.username
|
||||||
font-size: 80%
|
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
|
.users
|
||||||
h3 Registered Users
|
h3 Registered Users
|
||||||
#userlist
|
#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
|
ul.right
|
||||||
li
|
li
|
||||||
a(href="/user/manage") #{user.display_name}
|
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
|
.wrapper
|
||||||
block body
|
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
|
.container
|
||||||
.content
|
.content
|
||||||
h1 Manage OAuth2 Clients
|
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