More updates, implemented basic login and article view
This commit is contained in:
parent
6d7b63eb47
commit
4e0ed81b55
29 changed files with 824 additions and 19 deletions
|
@ -7,7 +7,8 @@
|
|||
}
|
||||
},
|
||||
"globals": {
|
||||
"FroalaEditor": "readonly"
|
||||
"FroalaEditor": "readonly",
|
||||
"gapi": "readonly"
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"env": {
|
||||
|
|
|
@ -13,7 +13,7 @@ export default class ArticleRoutes {
|
|||
async getAllArticles(ctx) {
|
||||
await this.security.ensureIncludes(ctx)
|
||||
|
||||
ctx.body = await this.Article.getAll(ctx, { }, ctx.state.filter.includes)
|
||||
ctx.body = await this.Article.getAll(ctx, { }, ctx.state.filter.includes, ctx.query.sort || '-id')
|
||||
}
|
||||
|
||||
/** GET: /api/pages/:pageId/articles */
|
||||
|
|
35
api/authentication/helper.mjs
Normal file
35
api/authentication/helper.mjs
Normal file
|
@ -0,0 +1,35 @@
|
|||
import Staff from '../staff/model.mjs'
|
||||
import Jwt from '../jwt.mjs'
|
||||
|
||||
export default class AuthHelper {
|
||||
constructor(opts = {}) {
|
||||
Object.assign(this, {
|
||||
Staff: opts.Staff || Staff,
|
||||
jwt: opts.jwt || new Jwt(),
|
||||
})
|
||||
}
|
||||
|
||||
async loginStaff(ctx) {
|
||||
let staff
|
||||
|
||||
try {
|
||||
staff = await this.Staff
|
||||
.query(qb => {
|
||||
qb.where({ email: ctx.request.body.username })
|
||||
qb.select('*')
|
||||
})
|
||||
.fetch({ require: true })
|
||||
|
||||
console.log(ctx.request.body.password, staff.get('password'))
|
||||
|
||||
await this.Staff.compare(ctx.request.body.password, staff.get('password'))
|
||||
} catch (err) {
|
||||
if (err.message === 'EmptyResponse' || err.message === 'PasswordMismatch') {
|
||||
ctx.throw(422, 'The email or password did not match')
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
return this.jwt.createToken(staff.get('email'), staff.get('level'))
|
||||
}
|
||||
}
|
|
@ -1,13 +1,17 @@
|
|||
import Staff from '../staff/model.mjs'
|
||||
import Jwt from '../jwt.mjs'
|
||||
import * as google from './google.mjs'
|
||||
import * as security from './security.mjs'
|
||||
import AuthHelper from './helper.mjs'
|
||||
|
||||
export default class AuthRoutes {
|
||||
constructor(opts = {}) {
|
||||
Object.assign(this, {
|
||||
helper: opts.helper || new AuthHelper(),
|
||||
Staff: opts.Staff || Staff,
|
||||
jwt: opts.jwt || new Jwt(),
|
||||
google: opts.google || google,
|
||||
security: opts.security || security,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -36,4 +40,21 @@ export default class AuthRoutes {
|
|||
|
||||
ctx.body = { token: this.jwt.createToken(output.email, level) }
|
||||
}
|
||||
|
||||
/*
|
||||
* POST /api/login/user - Authenticate a user using password login
|
||||
*
|
||||
* @body {string} username - Username
|
||||
* @body {string} password - Password
|
||||
* @returns
|
||||
*
|
||||
* { token: 'Authentication token' }
|
||||
*/
|
||||
async loginUser(ctx) {
|
||||
this.security.isValidLogin(ctx, ctx.request.body)
|
||||
|
||||
let token = await this.helper.loginStaff(ctx)
|
||||
|
||||
ctx.body = { token }
|
||||
}
|
||||
}
|
||||
|
|
18
api/authentication/security.mjs
Normal file
18
api/authentication/security.mjs
Normal file
|
@ -0,0 +1,18 @@
|
|||
|
||||
export function isValidLogin(ctx, body) {
|
||||
if (!body.username) {
|
||||
ctx.throw(422, 'Body was missing property username')
|
||||
}
|
||||
|
||||
if (!body.password) {
|
||||
ctx.throw(422, 'Body was missing property password')
|
||||
}
|
||||
|
||||
if (typeof body.password !== 'string') {
|
||||
ctx.throw(422, 'Property password must be a string')
|
||||
}
|
||||
|
||||
if (typeof body.username !== 'string') {
|
||||
ctx.throw(422, 'Property username must be a string')
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import MediaRoutes from './media/routes.mjs'
|
|||
import FileRoutes from './file/routes.mjs'
|
||||
import PageRoutes from './page/routes.mjs'
|
||||
import ArticleRoutes from './article/routes.mjs'
|
||||
import StaffRoutes from './staff/routes.mjs'
|
||||
import { restrict } from './access/middleware.mjs'
|
||||
|
||||
const router = new Router()
|
||||
|
@ -14,6 +15,7 @@ const router = new Router()
|
|||
// API Authentication
|
||||
const authentication = new AuthRoutes()
|
||||
router.post('/api/login', authentication.login.bind(authentication))
|
||||
router.post('/api/login/user', authentication.loginUser.bind(authentication))
|
||||
|
||||
// API Media
|
||||
const media = new MediaRoutes()
|
||||
|
@ -42,4 +44,11 @@ router.post('/api/articles', restrict(access.Manager), article.createArticle.bin
|
|||
router.put('/api/articles/:id', restrict(access.Manager), article.updateArticle.bind(article))
|
||||
router.del('/api/articles/:id', restrict(access.Manager), article.removeArticle.bind(article))
|
||||
|
||||
const staff = new StaffRoutes()
|
||||
router.get('/api/staff', restrict(access.Admin), staff.getAllStaff.bind(staff))
|
||||
router.get('/api/staff/:id', restrict(access.Admin), staff.getSingleStaff.bind(staff))
|
||||
router.post('/api/staff', restrict(access.Admin), staff.createStaff.bind(staff))
|
||||
router.put('/api/staff/:id', restrict(access.Admin), staff.updateStaff.bind(staff))
|
||||
router.del('/api/staff/:id', restrict(access.Admin), staff.removeStaff.bind(staff))
|
||||
|
||||
export default router
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import bookshelf from '../bookshelf.mjs'
|
||||
import bcrypt from 'bcrypt'
|
||||
import config from '../config.mjs'
|
||||
|
||||
/* Staff model:
|
||||
{
|
||||
|
@ -16,13 +18,57 @@ import bookshelf from '../bookshelf.mjs'
|
|||
|
||||
const Staff = bookshelf.createModel({
|
||||
tableName: 'staff',
|
||||
|
||||
privateFields: bookshelf.safeColumns([
|
||||
'fullname',
|
||||
'email',
|
||||
'level',
|
||||
]),
|
||||
}, {
|
||||
// Hide password from any relations and include requests.
|
||||
publicFields: bookshelf.safeColumns([
|
||||
'username',
|
||||
'fullname',
|
||||
'level',
|
||||
]),
|
||||
|
||||
hash(password) {
|
||||
return new Promise((resolve, reject) =>
|
||||
bcrypt.hash(password, config.get('bcrypt'), (err, hashed) => {
|
||||
if (err) return reject(err)
|
||||
|
||||
resolve(hashed)
|
||||
})
|
||||
)
|
||||
},
|
||||
|
||||
compare(password, hashed) {
|
||||
return new Promise((resolve, reject) =>
|
||||
bcrypt.compare(password, hashed, (err, res) => {
|
||||
if (err || !res) return reject(new Error('PasswordMismatch'))
|
||||
resolve()
|
||||
})
|
||||
)
|
||||
},
|
||||
|
||||
getAll(ctx, where = {}, withRelated = [], orderBy = 'id') {
|
||||
return this.query(qb => {
|
||||
this.baseQueryAll(ctx, qb, where, orderBy)
|
||||
qb.select(bookshelf.safeColumns([
|
||||
'fullname',
|
||||
'email',
|
||||
'level',
|
||||
]))
|
||||
})
|
||||
.fetchPage({
|
||||
pageSize: ctx.state.pagination.perPage,
|
||||
page: ctx.state.pagination.page,
|
||||
withRelated,
|
||||
ctx: ctx,
|
||||
})
|
||||
.then(result => {
|
||||
ctx.state.pagination.total = result.pagination.rowCount
|
||||
return result
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export default Staff
|
||||
|
|
52
api/staff/routes.mjs
Normal file
52
api/staff/routes.mjs
Normal file
|
@ -0,0 +1,52 @@
|
|||
import Staff from './model.mjs'
|
||||
import * as security from './security.mjs'
|
||||
|
||||
export default class StaffRoutes {
|
||||
constructor(opts = {}) {
|
||||
Object.assign(this, {
|
||||
Staff: opts.Staff || Staff,
|
||||
security: opts.security || security,
|
||||
})
|
||||
}
|
||||
|
||||
/** GET: /api/articles */
|
||||
async getAllStaff(ctx) {
|
||||
ctx.body = await this.Staff.getAll(ctx, { }, [])
|
||||
}
|
||||
|
||||
/** GET: /api/articles/:id */
|
||||
async getSingleStaff(ctx) {
|
||||
ctx.body = await this.Staff.getSingle(ctx.params.id, [], true, ctx)
|
||||
}
|
||||
|
||||
/** POST: /api/articles */
|
||||
async createStaff(ctx) {
|
||||
await this.security.validUpdate(ctx)
|
||||
|
||||
ctx.body = await this.Staff.create(ctx.request.body)
|
||||
}
|
||||
|
||||
/** PUT: /api/articles/:id */
|
||||
async updateStaff(ctx) {
|
||||
await this.security.validUpdate(ctx)
|
||||
|
||||
let page = await this.Staff.getSingle(ctx.params.id)
|
||||
|
||||
page.set(ctx.request.body)
|
||||
|
||||
await page.save()
|
||||
|
||||
ctx.body = page
|
||||
}
|
||||
|
||||
/** DELETE: /api/articles/:id */
|
||||
async removeStaff(ctx) {
|
||||
let page = await this.Staff.getSingle(ctx.params.id)
|
||||
|
||||
page.set({ is_deleted: true })
|
||||
|
||||
await page.save()
|
||||
|
||||
ctx.status = 204
|
||||
}
|
||||
}
|
21
api/staff/security.mjs
Normal file
21
api/staff/security.mjs
Normal file
|
@ -0,0 +1,21 @@
|
|||
import filter from '../filter.mjs'
|
||||
import Staff from './model.mjs'
|
||||
|
||||
const validFields = [
|
||||
'fullname',
|
||||
'email',
|
||||
'password',
|
||||
'level',
|
||||
]
|
||||
|
||||
export async function validUpdate(ctx) {
|
||||
let out = filter(Object.keys(ctx.request.body), validFields)
|
||||
|
||||
if (out.length > 0) {
|
||||
ctx.throw(422, `Body had following invalid properties: ${out.join(', ')}`)
|
||||
}
|
||||
|
||||
if (ctx.request.body.password) {
|
||||
ctx.request.body.password = await Staff.hash(ctx.request.body.password)
|
||||
}
|
||||
}
|
|
@ -37,3 +37,4 @@
|
|||
|
||||
@import 'pages';
|
||||
@import 'articles';
|
||||
@import 'staff';
|
||||
|
|
151
app/admin/editstaff.js
Normal file
151
app/admin/editstaff.js
Normal file
|
@ -0,0 +1,151 @@
|
|||
const m = require('mithril')
|
||||
|
||||
const { createStaff, updateStaff, getStaff } = require('../api/staff')
|
||||
|
||||
const EditStaff = {
|
||||
oninit: function(vnode) {
|
||||
this.fetchStaff(vnode)
|
||||
},
|
||||
|
||||
onupdate: function(vnode) {
|
||||
if (this.lastid !== m.route.param('id')) {
|
||||
this.fetchStaff(vnode)
|
||||
}
|
||||
},
|
||||
|
||||
fetchStaff: function(vnode) {
|
||||
this.lastid = m.route.param('id')
|
||||
this.loading = this.lastid !== 'add'
|
||||
this.creating = this.lastid === 'add'
|
||||
this.error = ''
|
||||
this.staff = {
|
||||
fullname: '',
|
||||
email: '',
|
||||
password: '',
|
||||
level: 10,
|
||||
}
|
||||
|
||||
if (this.lastid !== 'add') {
|
||||
getStaff(this.lastid)
|
||||
.then(function(result) {
|
||||
vnode.state.editedPath = true
|
||||
vnode.state.staff = result
|
||||
})
|
||||
.catch(function(err) {
|
||||
vnode.state.error = err.message
|
||||
})
|
||||
.then(function() {
|
||||
vnode.state.loading = false
|
||||
m.redraw()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
updateValue: function(fullname, e) {
|
||||
this.staff[fullname] = e.currentTarget.value
|
||||
},
|
||||
|
||||
save: function(vnode, e) {
|
||||
e.preventDefault()
|
||||
if (!this.staff.fullname) {
|
||||
this.error = 'Fullname is missing'
|
||||
} else if (!this.staff.email) {
|
||||
this.error = 'Email is missing'
|
||||
} else {
|
||||
this.error = ''
|
||||
}
|
||||
if (this.error) return
|
||||
|
||||
this.staff.description = vnode.state.froala && vnode.state.froala.html.get() || this.staff.description
|
||||
|
||||
this.loading = true
|
||||
|
||||
let promise
|
||||
|
||||
if (this.staff.id) {
|
||||
promise = updateStaff(this.staff.id, {
|
||||
fullname: this.staff.fullname,
|
||||
email: this.staff.email,
|
||||
level: this.staff.level,
|
||||
password: this.staff.password,
|
||||
})
|
||||
} else {
|
||||
promise = createStaff({
|
||||
fullname: this.staff.fullname,
|
||||
email: this.staff.email,
|
||||
level: this.staff.level,
|
||||
password: this.staff.password,
|
||||
})
|
||||
}
|
||||
|
||||
promise.then(function(res) {
|
||||
m.route.set('/admin/staff')
|
||||
})
|
||||
.catch(function(err) {
|
||||
vnode.state.error = err.message
|
||||
})
|
||||
.then(function() {
|
||||
vnode.state.loading = false
|
||||
m.redraw()
|
||||
})
|
||||
},
|
||||
|
||||
updateLevel: function(e) {
|
||||
this.staff.level = Number(e.currentTarget.value)
|
||||
},
|
||||
|
||||
view: function(vnode) {
|
||||
const levels = [[10, 'Manager'], [100, 'Admin']]
|
||||
return (
|
||||
this.loading ?
|
||||
m('div.loading-spinner')
|
||||
: m('div.admin-wrapper', [
|
||||
m('div.admin-actions', this.staff.id
|
||||
? [
|
||||
m('span', 'Actions:'),
|
||||
m(m.route.Link, { href: '/admin/staff' }, 'Staff list'),
|
||||
]
|
||||
: null),
|
||||
m('article.editstaff', [
|
||||
m('header', m('h1', this.creating ? 'Create Staff' : 'Edit ' + (this.staff.fullname || '(untitled)'))),
|
||||
m('div.error', {
|
||||
hidden: !this.error,
|
||||
onclick: function() { vnode.state.error = '' },
|
||||
}, this.error),
|
||||
m('form.editstaff.content', {
|
||||
onsubmit: this.save.bind(this, vnode),
|
||||
}, [
|
||||
m('label', 'Level'),
|
||||
m('select', {
|
||||
onchange: this.updateLevel.bind(this),
|
||||
}, levels.map(function(level) { return m('option', { value: level[0], selected: level[0] === vnode.state.staff.level }, level[1]) })),
|
||||
m('label', 'Fullname'),
|
||||
m('input', {
|
||||
type: 'text',
|
||||
value: this.staff.fullname,
|
||||
oninput: this.updateValue.bind(this, 'fullname'),
|
||||
}),
|
||||
m('label', 'Email'),
|
||||
m('input', {
|
||||
type: 'text',
|
||||
value: this.staff.email,
|
||||
oninput: this.updateValue.bind(this, 'email'),
|
||||
}),
|
||||
m('label', 'Password (optional)'),
|
||||
m('input', {
|
||||
type: 'text',
|
||||
value: this.staff.password,
|
||||
oninput: this.updateValue.bind(this, 'password'),
|
||||
}),
|
||||
m('input', {
|
||||
type: 'submit',
|
||||
value: 'Save',
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = EditStaff
|
49
app/admin/staff.scss
Normal file
49
app/admin/staff.scss
Normal file
|
@ -0,0 +1,49 @@
|
|||
|
||||
article.editstaff {
|
||||
text-align: center;
|
||||
background: white;
|
||||
padding: 0 0 20px;
|
||||
|
||||
header {
|
||||
padding: 10px;
|
||||
background: $secondary-bg;
|
||||
|
||||
h1 {
|
||||
color: $secondary-fg;
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
margin-left: 10px;
|
||||
color: $secondary-light-fg;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
padding: 0 40px 20px;
|
||||
|
||||
textarea {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
height: 300px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
& > .loading-spinner {
|
||||
width: 240px;
|
||||
height: 50px;
|
||||
position: relative;
|
||||
|
||||
&.full {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
110
app/admin/stafflist.js
Normal file
110
app/admin/stafflist.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
const m = require('mithril')
|
||||
|
||||
const { getAllStaff, removeStaff } = require('../api/staff')
|
||||
const Dialogue = require('../widgets/dialogue')
|
||||
const Pages = require('../widgets/pages')
|
||||
|
||||
const AdminStaffList = {
|
||||
oninit: function(vnode) {
|
||||
this.error = ''
|
||||
this.lastpage = m.route.param('page') || '1'
|
||||
this.staff = []
|
||||
this.removeStaff = null
|
||||
|
||||
this.fetchStaffs(vnode)
|
||||
},
|
||||
|
||||
fetchStaffs: function(vnode) {
|
||||
this.loading = true
|
||||
|
||||
return getAllStaff()
|
||||
.then(function(result) {
|
||||
vnode.state.staff = result
|
||||
})
|
||||
.catch(function(err) {
|
||||
vnode.state.error = err.message
|
||||
})
|
||||
.then(function() {
|
||||
vnode.state.loading = false
|
||||
m.redraw()
|
||||
})
|
||||
},
|
||||
|
||||
confirmRemoveStaff: function(vnode) {
|
||||
let removingStaff = this.removeStaff
|
||||
this.removeStaff = null
|
||||
this.loading = true
|
||||
removeStaff(removingStaff.id)
|
||||
.then(this.oninit.bind(this, vnode))
|
||||
.catch(function(err) {
|
||||
vnode.state.error = err.message
|
||||
vnode.state.loading = false
|
||||
m.redraw()
|
||||
})
|
||||
},
|
||||
|
||||
getLevel: function(level) {
|
||||
if (level === 100) {
|
||||
return 'Admin'
|
||||
}
|
||||
return 'Manager'
|
||||
},
|
||||
|
||||
view: function(vnode) {
|
||||
return [
|
||||
m('div.admin-wrapper', [
|
||||
m('div.admin-actions', [
|
||||
m('span', 'Actions:'),
|
||||
m(m.route.Link, { href: '/admin/staff/add' }, 'Create new staff'),
|
||||
]),
|
||||
m('article.editarticle', [
|
||||
m('header', m('h1', 'All staff')),
|
||||
m('div.error', {
|
||||
hidden: !this.error,
|
||||
onclick: function() { vnode.state.error = '' },
|
||||
}, this.error),
|
||||
(this.loading
|
||||
? m('div.loading-spinner.full')
|
||||
: m('table', [
|
||||
m('thead',
|
||||
m('tr', [
|
||||
m('th', 'Fullname'),
|
||||
m('th', 'Email'),
|
||||
m('th', 'Level'),
|
||||
m('th.right', 'Updated'),
|
||||
m('th.right', 'Actions'),
|
||||
])
|
||||
),
|
||||
m('tbody', this.staff.map(function(item) {
|
||||
return m('tr', [
|
||||
m('td', m(m.route.Link, { href: '/admin/staff/' + item.id }, item.fullname)),
|
||||
m('td', item.email),
|
||||
m('td.right', AdminStaffList.getLevel(item.level)),
|
||||
m('td.right', (item.updated_at || '---').replace('T', ' ').split('.')[0]),
|
||||
m('td.right', m('button', { onclick: function() { vnode.state.removeStaff = item } }, 'Remove')),
|
||||
])
|
||||
})),
|
||||
])
|
||||
),
|
||||
m(Pages, {
|
||||
base: '/admin/staff',
|
||||
links: this.links,
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
m(Dialogue, {
|
||||
hidden: vnode.state.removeStaff === null,
|
||||
title: 'Delete ' + (vnode.state.removeStaff ? vnode.state.removeStaff.name : ''),
|
||||
message: 'Are you sure you want to remove "' + (vnode.state.removeStaff ? vnode.state.removeStaff.fullname : '') + '" (' + (vnode.state.removeStaff ? vnode.state.removeStaff.email : '') + ')',
|
||||
yes: 'Remove',
|
||||
yesclass: 'alert',
|
||||
no: 'Cancel',
|
||||
noclass: 'cancel',
|
||||
onyes: this.confirmRemoveStaff.bind(this, vnode),
|
||||
onno: function() { vnode.state.removeStaff = null },
|
||||
}),
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = AdminStaffList
|
|
@ -25,7 +25,11 @@ exports.sendRequest = function(options, isPagination) {
|
|||
data: JSON.parse(xhr.responseText),
|
||||
}
|
||||
} else {
|
||||
out = JSON.parse(xhr.responseText)
|
||||
if (xhr.responseText) {
|
||||
out = JSON.parse(xhr.responseText)
|
||||
} else {
|
||||
out = {}
|
||||
}
|
||||
}
|
||||
if (xhr.status >= 300) {
|
||||
throw out
|
||||
|
|
38
app/api/staff.js
Normal file
38
app/api/staff.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
const { sendRequest } = require('./common')
|
||||
|
||||
exports.createStaff = function(body) {
|
||||
return sendRequest({
|
||||
method: 'POST',
|
||||
url: '/api/staff',
|
||||
body: body,
|
||||
})
|
||||
}
|
||||
|
||||
exports.updateStaff = function(id, body) {
|
||||
return sendRequest({
|
||||
method: 'PUT',
|
||||
url: '/api/staff/' + id,
|
||||
body: body,
|
||||
})
|
||||
}
|
||||
|
||||
exports.getAllStaff = function() {
|
||||
return sendRequest({
|
||||
method: 'GET',
|
||||
url: '/api/staff',
|
||||
})
|
||||
}
|
||||
|
||||
exports.getStaff = function(id) {
|
||||
return sendRequest({
|
||||
method: 'GET',
|
||||
url: '/api/staff/' + id,
|
||||
})
|
||||
}
|
||||
|
||||
exports.removeStaff = function(id) {
|
||||
return sendRequest({
|
||||
method: 'DELETE',
|
||||
url: '/api/staff/' + id,
|
||||
})
|
||||
}
|
|
@ -244,4 +244,5 @@ table {
|
|||
@import 'admin/admin';
|
||||
@import 'widgets/common';
|
||||
@import 'pages/page';
|
||||
@import 'frontpage/frontpage'
|
||||
@import 'article/article';
|
||||
@import 'frontpage/frontpage';
|
||||
|
|
78
app/article/article.js
Normal file
78
app/article/article.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
const m = require('mithril')
|
||||
const { getArticle } = require('../api/article')
|
||||
const Authentication = require('../authentication')
|
||||
const Fileinfo = require('../widgets/fileinfo')
|
||||
|
||||
const Article = {
|
||||
oninit: function(vnode) {
|
||||
this.error = ''
|
||||
this.lastarticle = m.route.param('article') || '1'
|
||||
this.loadingnews = false
|
||||
this.fetchArticle(vnode)
|
||||
},
|
||||
|
||||
fetchArticle: function(vnode) {
|
||||
this.path = m.route.param('id')
|
||||
this.news = []
|
||||
this.newslinks = null
|
||||
this.article = {
|
||||
id: 0,
|
||||
name: '',
|
||||
path: '',
|
||||
description: '',
|
||||
media: null,
|
||||
banner: null,
|
||||
files: [],
|
||||
}
|
||||
this.loading = true
|
||||
|
||||
getArticle(this.path)
|
||||
.then(function(result) {
|
||||
vnode.state.article = result
|
||||
})
|
||||
.catch(function(err) {
|
||||
vnode.state.error = err.message
|
||||
})
|
||||
.then(function() {
|
||||
vnode.state.loading = vnode.state.loadingnews = false
|
||||
m.redraw()
|
||||
})
|
||||
},
|
||||
|
||||
onupdate: function(vnode) {
|
||||
if (this.path !== m.route.param('id')) {
|
||||
this.fetchArticle(vnode)
|
||||
}
|
||||
},
|
||||
|
||||
view: function(vnode) {
|
||||
return (
|
||||
this.loading ?
|
||||
m('div.loading-spinner')
|
||||
: m('article.article', [
|
||||
m('header', m('h1', this.article.name)),
|
||||
m('.fr-view', [
|
||||
this.article.media
|
||||
? m('a.cover', {
|
||||
href: this.article.media.url,
|
||||
}, m('img', { src: this.article.media.medium_url }))
|
||||
: null,
|
||||
this.article.description ? m.trust(this.article.description) : null,
|
||||
(this.article.files && this.article.files.length
|
||||
? this.article.files.map(function(file) {
|
||||
return m(Fileinfo, { file: file })
|
||||
})
|
||||
: null),
|
||||
]),
|
||||
Authentication.currentUser
|
||||
? m('div.admin-actions', [
|
||||
m('span', 'Admin controls:'),
|
||||
m(m.route.Link, { href: '/admin/articles/' + this.article.id }, 'Edit article'),
|
||||
])
|
||||
: null,
|
||||
])
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = Article
|
43
app/article/article.scss
Normal file
43
app/article/article.scss
Normal file
|
@ -0,0 +1,43 @@
|
|||
article.article {
|
||||
background: white;
|
||||
padding: 0 0 20px;
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin: 20px;
|
||||
padding: 10px;
|
||||
background: $secondary-bg;
|
||||
width: 100%;
|
||||
max-width: 1920px;
|
||||
align-self: center;
|
||||
|
||||
h1 {
|
||||
color: $secondary-fg;
|
||||
}
|
||||
}
|
||||
|
||||
.cover {
|
||||
margin: 0 -10px 20px;
|
||||
}
|
||||
|
||||
.admin-actions {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.fr-view {
|
||||
margin: 0 20px;
|
||||
padding: 0 20px;
|
||||
width: calc(100% - 40px);
|
||||
max-width: 800px;
|
||||
flex: 2 0 0;
|
||||
align-self: center;
|
||||
|
||||
a.cover img {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
width: calc(100% - 40px);
|
||||
max-width: 1920px;
|
||||
align-self: center;
|
||||
flex: 0 0 150px;
|
||||
|
@ -14,6 +14,8 @@
|
|||
text-align: right;
|
||||
font-size: 1.6em;
|
||||
padding: 10px 20px;
|
||||
text-decoration: none;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
frontpage {
|
||||
|
@ -29,4 +31,20 @@ frontpage {
|
|||
.loading-spinner {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
newsitem {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px){
|
||||
.frontpage-banner {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
frontpage {
|
||||
padding: 0 10px;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,9 +8,12 @@ const Login = require('./login/login')
|
|||
const Logout = require('./login/logout')
|
||||
const EditPage = require('./admin/editpage')
|
||||
const Page = require('./pages/page')
|
||||
const Article = require('./article/article')
|
||||
const AdminPages = require('./admin/pages')
|
||||
const AdminArticles = require('./admin/articles')
|
||||
const EditArticle = require('./admin/editarticle')
|
||||
const AdminStaffList = require('./admin/stafflist')
|
||||
const EditStaff = require('./admin/editstaff')
|
||||
|
||||
const menuRoot = document.getElementById('nav')
|
||||
const mainRoot = document.getElementById('main')
|
||||
|
@ -20,9 +23,12 @@ m.route(mainRoot, '/', {
|
|||
'/login': Login,
|
||||
'/logout': Logout,
|
||||
'/page/:id': Page,
|
||||
'/article/:id': Article,
|
||||
'/admin/pages': AdminPages,
|
||||
'/admin/pages/:key': EditPage,
|
||||
'/admin/articles': AdminArticles,
|
||||
'/admin/articles/:id': EditArticle,
|
||||
'/admin/staff': AdminStaffList,
|
||||
'/admin/staff/:id': EditStaff,
|
||||
})
|
||||
m.mount(menuRoot, Menu)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const m = require('mithril')
|
||||
const Authentication = require('../authentication')
|
||||
const { sendRequest } = require('../api/common')
|
||||
|
||||
const Login = {
|
||||
loadedGoogle: false,
|
||||
|
@ -45,7 +46,6 @@ const Login = {
|
|||
|
||||
onGoogleFailure: function(error) {
|
||||
if (error.error !== 'popup_closed_by_user' && error.error !== 'popup_blocked_by_browser') {
|
||||
console.error(error)
|
||||
Login.error = 'Error while logging into Google: ' + error.error
|
||||
m.redraw()
|
||||
}
|
||||
|
@ -55,6 +55,9 @@ const Login = {
|
|||
Login.redirect = vnode.attrs.redirect || ''
|
||||
if (Authentication.currentUser) return m.route.set('/')
|
||||
Login.error = ''
|
||||
|
||||
this.username = ''
|
||||
this.password = ''
|
||||
},
|
||||
|
||||
oncreate: function() {
|
||||
|
@ -65,7 +68,42 @@ const Login = {
|
|||
})
|
||||
},
|
||||
|
||||
view: function() {
|
||||
loginuser: function(vnode, e) {
|
||||
e.preventDefault()
|
||||
if (!this.username) {
|
||||
Login.error = 'Email is missing'
|
||||
} else if (!this.password) {
|
||||
Login.error = 'Password is missing'
|
||||
} else {
|
||||
Login.error = ''
|
||||
}
|
||||
if (Login.error) return
|
||||
|
||||
Login.loading = true
|
||||
|
||||
sendRequest({
|
||||
method: 'POST',
|
||||
url: '/api/login/user',
|
||||
body: {
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
},
|
||||
})
|
||||
.then(function(result) {
|
||||
Authentication.updateToken(result.token)
|
||||
m.route.set(Login.redirect || '/')
|
||||
})
|
||||
.catch(function(error) {
|
||||
Login.error = 'Error while logging into NFP! ' + error.message
|
||||
vnode.state.password = ''
|
||||
})
|
||||
.then(function () {
|
||||
Login.loading = false
|
||||
m.redraw()
|
||||
})
|
||||
},
|
||||
|
||||
view: function(vnode) {
|
||||
return [
|
||||
m('div.login-wrapper', [
|
||||
m('article.login', [
|
||||
|
@ -73,9 +111,31 @@ const Login = {
|
|||
m('h1', 'NFP.moe login'),
|
||||
]),
|
||||
m('div.content', [
|
||||
m('h5', 'Please login using google to access restricted area'),
|
||||
m('h5', 'Please login to access restricted area'),
|
||||
Login.error ? m('div.error', Login.error) : null,
|
||||
Login.loading ? m('div.loading-spinner') : null,
|
||||
m('form', {
|
||||
hidden: Login.loading,
|
||||
onsubmit: this.loginuser.bind(this, vnode),
|
||||
}, [
|
||||
m('label', 'Email'),
|
||||
m('input', {
|
||||
type: 'text',
|
||||
value: this.username,
|
||||
oninput: function(e) { vnode.state.username = e.currentTarget.value },
|
||||
}),
|
||||
m('label', 'Password'),
|
||||
m('input', {
|
||||
type: 'password',
|
||||
value: this.password,
|
||||
oninput: function(e) { vnode.state.password = e.currentTarget.value },
|
||||
}),
|
||||
m('input', {
|
||||
type: 'submit',
|
||||
value: 'Login',
|
||||
}),
|
||||
]),
|
||||
m('h5', { hidden: Login.loading }, 'Alternative login'),
|
||||
m('div#googlesignin', { hidden: Login.loading }, m('div.loading-spinner')),
|
||||
]),
|
||||
]),
|
||||
|
|
|
@ -30,4 +30,9 @@ article.login {
|
|||
height: 50px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
form {
|
||||
align-self: stretch;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,12 +45,13 @@ const Menu = {
|
|||
'Welcome ' + Authentication.currentUser.email,
|
||||
m(m.route.Link, { href: '/logout' }, 'Logout'),
|
||||
]),
|
||||
(Authentication.currentUser.level >= 100
|
||||
? [
|
||||
m(m.route.Link, { href: '/admin/pages' }, 'Pages'),
|
||||
m(m.route.Link, { href: '/admin/articles' }, 'Articles'),
|
||||
m(m.route.Link, { href: '/admin/articles/add' }, 'Create article'),
|
||||
]
|
||||
(Authentication.currentUser.level >= 10
|
||||
? m('div.adminlinks', [
|
||||
m(m.route.Link, { href: '/admin/articles/add' }, 'Create article'),
|
||||
m(m.route.Link, { href: '/admin/articles' }, 'Articles'),
|
||||
m(m.route.Link, { href: '/admin/pages' }, 'Pages'),
|
||||
m(m.route.Link, { hidden: Authentication.currentUser.level < 100, href: '/admin/staff' }, 'Staff'),
|
||||
])
|
||||
: null
|
||||
),
|
||||
] : [
|
||||
|
|
|
@ -49,6 +49,18 @@
|
|||
line-height: 1.4em;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.adminlinks {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-width: 200px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
a {
|
||||
padding: 3px 5px;
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -319,3 +319,26 @@ pages {
|
|||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px){
|
||||
newsitem a.cover {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 639px){
|
||||
newsitem {
|
||||
a.cover {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
|
||||
img {
|
||||
max-height: max-content;
|
||||
}
|
||||
}
|
||||
|
||||
.newsitemcontent {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ const Newsentry = {
|
|||
return m('newsentry', [
|
||||
vnode.attrs.media
|
||||
? m('a.cover', {
|
||||
href: vnode.attrs.media.large_url,
|
||||
href: '/article/' + vnode.attrs.path,
|
||||
}, m('img', { src: vnode.attrs.media.small_url }))
|
||||
: m('a.cover.nobg'),
|
||||
m('div.entrycontent', [
|
||||
|
|
|
@ -11,8 +11,8 @@ const Newsitem = {
|
|||
m('div.newsitemcontent', [
|
||||
vnode.attrs.media
|
||||
? m('a.cover', {
|
||||
href: vnode.attrs.media.large_url,
|
||||
}, m('img', { src: vnode.attrs.media.small_url }))
|
||||
href: '/article/' + vnode.attrs.path,
|
||||
}, m('img', { src: vnode.attrs.media.medium_url }))
|
||||
: m('a.cover.nobg'),
|
||||
m('div.entrycontent', [
|
||||
(vnode.attrs.description
|
||||
|
|
|
@ -6,6 +6,7 @@ exports.up = function up(knex, Promise) {
|
|||
table.increments()
|
||||
table.text('email')
|
||||
table.text('fullname')
|
||||
table.text('password')
|
||||
table.boolean('is_deleted')
|
||||
.notNullable()
|
||||
.default(false)
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
"homepage": "https://github.com/nfp-projects/nfp_moe",
|
||||
"dependencies": {
|
||||
"@koa/cors": "^2.2.3",
|
||||
"bcrypt": "^3.0.0",
|
||||
"bookshelf": "^0.15.1",
|
||||
"bunyan-lite": "^1.0.1",
|
||||
"format-link-header": "^2.1.0",
|
||||
|
|
Loading…
Reference in a new issue