ban management and other changes
This commit is contained in:
parent
de3f8498c2
commit
2899f48d95
|
@ -26,8 +26,8 @@ console.warn = function () {
|
||||||
|
|
||||||
const realConsoleError = console.error
|
const realConsoleError = console.error
|
||||||
console.error = function () {
|
console.error = function () {
|
||||||
process.stdout.write('\x1b[2K\r')
|
process.stderr.write('\x1b[2K\r')
|
||||||
process.stdout.write('[ err] [' + dateFormat(new Date()) + '] ')
|
process.stderr.write('[ err] [' + dateFormat(new Date()) + '] ')
|
||||||
realConsoleError.apply(this, arguments)
|
realConsoleError.apply(this, arguments)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import Models from './models'
|
||||||
|
|
||||||
const perPage = 6
|
const perPage = 6
|
||||||
|
|
||||||
function cleanUserObject (dbe) {
|
function cleanUserObject (dbe, admin) {
|
||||||
return {
|
return {
|
||||||
id: dbe.id,
|
id: dbe.id,
|
||||||
username: dbe.username,
|
username: dbe.username,
|
||||||
|
@ -12,10 +12,11 @@ function cleanUserObject (dbe) {
|
||||||
avatar_file: dbe.avatar_file,
|
avatar_file: dbe.avatar_file,
|
||||||
activated: dbe.activated === 1,
|
activated: dbe.activated === 1,
|
||||||
locked: dbe.locked === 1,
|
locked: dbe.locked === 1,
|
||||||
ip_addess: dbe.ip_addess,
|
ip_address: dbe.ip_address,
|
||||||
password: dbe.password !== null,
|
password: dbe.password !== null,
|
||||||
nw_privilege: dbe.nw_privilege,
|
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 = {
|
const API = {
|
||||||
getAllUsers: async function (page) {
|
getAllUsers: async function (page, adminId) {
|
||||||
let count = await Models.User.query().count('id as ids')
|
let count = await Models.User.query().count('id as ids')
|
||||||
if (!count.length || !count[0]['ids'] || isNaN(page)) {
|
if (!count.length || !count[0]['ids'] || isNaN(page)) {
|
||||||
return {error: 'No users found'}
|
return {error: 'No users found'}
|
||||||
|
@ -50,12 +72,13 @@ const API = {
|
||||||
count = count[0].ids
|
count = count[0].ids
|
||||||
let paginated = Users.Pagination(perPage, parseInt(count), page)
|
let paginated = Users.Pagination(perPage, parseInt(count), page)
|
||||||
let raw = await Models.User.query().offset(paginated.offset).limit(perPage)
|
let raw = await Models.User.query().offset(paginated.offset).limit(perPage)
|
||||||
|
let admin = await Users.User.get(adminId)
|
||||||
|
|
||||||
let users = []
|
let users = []
|
||||||
for (let i in raw) {
|
for (let i in raw) {
|
||||||
let entry = raw[i]
|
let entry = raw[i]
|
||||||
|
|
||||||
users.push(cleanUserObject(entry))
|
users.push(cleanUserObject(entry, admin))
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -110,6 +133,7 @@ const API = {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Models.OAuth2Client.query().patchAndFetchById(id, data)
|
await Models.OAuth2Client.query().patchAndFetchById(id, data)
|
||||||
|
await Models.OAuth2AuthorizedClient.query().delete().where('client_id', id)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {error: 'No such client'}
|
return {error: 'No such client'}
|
||||||
}
|
}
|
||||||
|
@ -159,6 +183,54 @@ const API = {
|
||||||
await Models.OAuth2AccessToken.query().delete().where('client_id', id)
|
await Models.OAuth2AccessToken.query().delete().where('client_id', id)
|
||||||
await Models.OAuth2RefreshToken.query().delete().where('client_id', id)
|
await Models.OAuth2RefreshToken.query().delete().where('client_id', id)
|
||||||
return true
|
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 {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ router.post('/', wrap(async (req, res, next) => {
|
||||||
|
|
||||||
let passReady = await User.Login.password(req.session.user, req.body.password)
|
let passReady = await User.Login.password(req.session.user, req.body.password)
|
||||||
if (passReady) {
|
if (passReady) {
|
||||||
req.session.accesstime = Date.now() + 300000 // 5 minutes
|
req.session.accesstime = Date.now() + 600000 // 10 minutes
|
||||||
return res.redirect('/admin')
|
return res.redirect('/admin')
|
||||||
} else {
|
} else {
|
||||||
req.flash('message', {error: true, text: 'Invalid password'})
|
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) => {
|
router.use(wrap(async (req, res, next) => {
|
||||||
if (req.session.accesstime) {
|
if (req.session.accesstime) {
|
||||||
if (req.session.accesstime > Date.now()) {
|
if (req.session.accesstime > Date.now()) {
|
||||||
req.session.accesstime = Date.now() + 300000
|
req.session.accesstime = Date.now() + 600000
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,10 +96,14 @@ apiRouter.get('/users', wrap(async (req, res) => {
|
||||||
page = 1
|
page = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
let users = await API.getAllUsers(page)
|
let users = await API.getAllUsers(page, req.session.user.id)
|
||||||
res.jsonp(users)
|
res.jsonp(users)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
/* ===============
|
||||||
|
* OAuth2 Data
|
||||||
|
* ===============
|
||||||
|
*/
|
||||||
apiRouter.get('/clients', wrap(async (req, res) => {
|
apiRouter.get('/clients', wrap(async (req, res) => {
|
||||||
let page = parseInt(req.query.page)
|
let page = parseInt(req.query.page)
|
||||||
if (isNaN(page) || page < 1) {
|
if (isNaN(page) || page < 1) {
|
||||||
|
@ -177,6 +181,49 @@ apiRouter.post('/client/delete/:id', wrap(async (req, res) => {
|
||||||
res.jsonp(client)
|
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) => {
|
apiRouter.use((err, req, res, next) => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
return res.status(500).jsonp({error: 'Internal server error'})
|
return res.status(500).jsonp({error: 'Internal server error'})
|
||||||
|
|
|
@ -53,7 +53,9 @@ router.use(wrap(async (req, res, next) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.session.user.session_refresh < Date.now()) {
|
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)
|
let banStatus = await API.User.getBanStatus(req.session.user.id)
|
||||||
|
|
||||||
if (banStatus.length) {
|
if (banStatus.length) {
|
||||||
|
@ -63,6 +65,9 @@ router.use(wrap(async (req, res, next) => {
|
||||||
|
|
||||||
// Update user session
|
// Update user session
|
||||||
let udata = await API.User.get(req.session.user.id)
|
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)
|
setSession(req, udata)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,67 @@ function paginationButton (pages) {
|
||||||
return html
|
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('<div class="message">' + data.error + '</div>')
|
||||||
|
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) {
|
function loadUsers (page) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: 'get',
|
type: 'get',
|
||||||
|
@ -39,7 +100,7 @@ function loadUsers (page) {
|
||||||
|
|
||||||
var pgbtn = paginationButton(data.page)
|
var pgbtn = paginationButton(data.page)
|
||||||
$('#userlist').append(pgbtn)
|
$('#userlist').append(pgbtn)
|
||||||
$('.pgn .button').click(function (e) {
|
$('#userlist .pgn .button').click(function (e) {
|
||||||
var pgnum = $(this).data('page')
|
var pgnum = $(this).data('page')
|
||||||
if (pgnum == null) return
|
if (pgnum == null) return
|
||||||
loadUsers(parseInt(pgnum))
|
loadUsers(parseInt(pgnum))
|
||||||
|
@ -51,6 +112,10 @@ function loadUsers (page) {
|
||||||
var tmp = buildTemplateScript('user', user)
|
var tmp = buildTemplateScript('user', user)
|
||||||
$('#userlist').append(tmp)
|
$('#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)
|
var pgbtn = paginationButton(data.page)
|
||||||
$('#clientlist').append(pgbtn)
|
$('#clientlist').append(pgbtn)
|
||||||
$('.pgn .button').click(function (e) {
|
$('#clientlist .pgn .button').click(function (e) {
|
||||||
var pgnum = $(this).data('page')
|
var pgnum = $(this).data('page')
|
||||||
if (pgnum == null) return
|
if (pgnum == null) return
|
||||||
loadClients(parseInt(pgnum))
|
loadClients(parseInt(pgnum))
|
||||||
|
@ -123,17 +188,17 @@ function loadClients (page) {
|
||||||
$('#clientlist').append(tmp)
|
$('#clientlist').append(tmp)
|
||||||
}
|
}
|
||||||
|
|
||||||
$('.edit').click(function (e) {
|
$('#clientlist .edit').click(function (e) {
|
||||||
var client = $(this).data('client')
|
var client = $(this).data('client')
|
||||||
editClient(parseInt(client))
|
editClient(parseInt(client))
|
||||||
})
|
})
|
||||||
|
|
||||||
$('.delete').click(function (e) {
|
$('#clientlist .delete').click(function (e) {
|
||||||
var client = $(this).data('client')
|
var client = $(this).data('client')
|
||||||
deleteClient(parseInt(client))
|
deleteClient(parseInt(client))
|
||||||
})
|
})
|
||||||
|
|
||||||
$('.newsecret').click(function (e) {
|
$('#clientlist .newsecret').click(function (e) {
|
||||||
var client = $(this).data('client')
|
var client = $(this).data('client')
|
||||||
$.post({
|
$.post({
|
||||||
url: '/admin/api/client/new_secret/' + parseInt(client),
|
url: '/admin/api/client/new_secret/' + parseInt(client),
|
||||||
|
@ -175,6 +240,10 @@ $(document).ready(function () {
|
||||||
loadUsers(1)
|
loadUsers(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($('#banlist').length) {
|
||||||
|
loadBans(1)
|
||||||
|
}
|
||||||
|
|
||||||
if ($('#clientlist').length) {
|
if ($('#clientlist').length) {
|
||||||
loadClients(1)
|
loadClients(1)
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,8 @@ nav
|
||||||
display: inline-block
|
display: inline-block
|
||||||
.right
|
.right
|
||||||
float: right
|
float: right
|
||||||
|
.stamps
|
||||||
|
float: right
|
||||||
|
|
||||||
.user
|
.user
|
||||||
min-height: 180px
|
min-height: 180px
|
||||||
|
|
|
@ -8,6 +8,10 @@ block body
|
||||||
.users
|
.users
|
||||||
h3 Registered Users
|
h3 Registered Users
|
||||||
#userlist
|
#userlist
|
||||||
|
.right
|
||||||
|
.users
|
||||||
|
h3 Bans
|
||||||
|
#banlist
|
||||||
.templates
|
.templates
|
||||||
script(type="x-tmpl-mustache" id="user").
|
script(type="x-tmpl-mustache" id="user").
|
||||||
<div class="user" id="user-{{id}}">
|
<div class="user" id="user-{{id}}">
|
||||||
|
@ -22,7 +26,7 @@ block body
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="stamps">
|
<div class="stamps">
|
||||||
{{^activated}}
|
{{^activated}}
|
||||||
<div class="noactive"><i class="fa fa-fw fa-envelope"></i></div>
|
<div class="noactive" title="Not activated"><i class="fa fa-fw fa-envelope"></i></div>
|
||||||
{{/activated}}
|
{{/activated}}
|
||||||
</div>
|
</div>
|
||||||
<div class="display_name">{{display_name}}</div>
|
<div class="display_name">{{display_name}}</div>
|
||||||
|
@ -33,5 +37,33 @@ block body
|
||||||
{{^password}}
|
{{^password}}
|
||||||
<div class="external"><b>Used external login</b></div>
|
<div class="external"><b>Used external login</b></div>
|
||||||
{{/password}}
|
{{/password}}
|
||||||
|
{{#bannable}}
|
||||||
|
<div class="button ban" data-id="{{id}}"><i class="fa fa-fw fa-ban"></i>Ban User</div>
|
||||||
|
{{/bannable}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
script(type="x-tmpl-mustache" id="ban").
|
||||||
|
<div class="ban" id="ban-{{user.id}}">
|
||||||
|
<div class="stamps">
|
||||||
|
{{#expired}}
|
||||||
|
<div class="noactive" title="Expired"><i class="fa fa-fw fa-ban"></i></div>
|
||||||
|
{{/expired}}
|
||||||
|
</div>
|
||||||
|
<div class="display_name">User: {{user.display_name}}</div>
|
||||||
|
<div class="display_name">Admin: {{admin.display_name}}</div>
|
||||||
|
<div class="description">Reason: {{reason}}</div>
|
||||||
|
<div class="timestamp">Placed {{created_at}}</div>
|
||||||
|
<div class="timestamp">Expires {{expires_at}}</div>
|
||||||
|
<div class="button remove" data-id="{{id}}">Pardon</div>
|
||||||
|
</div>
|
||||||
|
script(type="x-tmpl-mustache" id="banNew").
|
||||||
|
<form id="fnsubmit">
|
||||||
|
<div class="message error"></div>
|
||||||
|
<input type="hidden" name="csrf" value="#{csrf}">
|
||||||
|
<input type="hidden" name="user_id" value="{{id}}">
|
||||||
|
<label for="reason">Reason</label>
|
||||||
|
<input type="text" id="reason" name="reason">
|
||||||
|
<label for="expires_at">Expires</label>
|
||||||
|
<input type="date" id="expires_at" name="expires_at">
|
||||||
|
<input type="submit" value="Create">
|
||||||
|
</form>
|
||||||
|
|
|
@ -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="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/main.css")
|
||||||
link(rel="stylesheet", type="text/css", href="/style/admin.css")
|
link(rel="stylesheet", type="text/css", href="/style/admin.css")
|
||||||
|
block links
|
||||||
script.
|
script.
|
||||||
window.variables = {
|
window.variables = {
|
||||||
server_time: parseInt('#{server_time}')
|
server_time: parseInt('#{server_time}')
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
extends layout.pug
|
extends layout.pug
|
||||||
|
|
||||||
block title
|
block title
|
||||||
|Icy Network - Legal Notice
|
|Icy Network
|
||||||
|
|
||||||
block body
|
block body
|
||||||
.document
|
.document
|
||||||
|
|
Reference in New Issue