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": {
|
"globals": {
|
||||||
"FroalaEditor": "readonly"
|
"FroalaEditor": "readonly",
|
||||||
|
"gapi": "readonly"
|
||||||
},
|
},
|
||||||
"extends": "eslint:recommended",
|
"extends": "eslint:recommended",
|
||||||
"env": {
|
"env": {
|
||||||
|
|
|
@ -13,7 +13,7 @@ export default class ArticleRoutes {
|
||||||
async getAllArticles(ctx) {
|
async getAllArticles(ctx) {
|
||||||
await this.security.ensureIncludes(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 */
|
/** 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 Staff from '../staff/model.mjs'
|
||||||
import Jwt from '../jwt.mjs'
|
import Jwt from '../jwt.mjs'
|
||||||
import * as google from './google.mjs'
|
import * as google from './google.mjs'
|
||||||
|
import * as security from './security.mjs'
|
||||||
|
import AuthHelper from './helper.mjs'
|
||||||
|
|
||||||
export default class AuthRoutes {
|
export default class AuthRoutes {
|
||||||
constructor(opts = {}) {
|
constructor(opts = {}) {
|
||||||
Object.assign(this, {
|
Object.assign(this, {
|
||||||
|
helper: opts.helper || new AuthHelper(),
|
||||||
Staff: opts.Staff || Staff,
|
Staff: opts.Staff || Staff,
|
||||||
jwt: opts.jwt || new Jwt(),
|
jwt: opts.jwt || new Jwt(),
|
||||||
google: opts.google || google,
|
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) }
|
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 FileRoutes from './file/routes.mjs'
|
||||||
import PageRoutes from './page/routes.mjs'
|
import PageRoutes from './page/routes.mjs'
|
||||||
import ArticleRoutes from './article/routes.mjs'
|
import ArticleRoutes from './article/routes.mjs'
|
||||||
|
import StaffRoutes from './staff/routes.mjs'
|
||||||
import { restrict } from './access/middleware.mjs'
|
import { restrict } from './access/middleware.mjs'
|
||||||
|
|
||||||
const router = new Router()
|
const router = new Router()
|
||||||
|
@ -14,6 +15,7 @@ const router = new Router()
|
||||||
// API Authentication
|
// API Authentication
|
||||||
const authentication = new AuthRoutes()
|
const authentication = new AuthRoutes()
|
||||||
router.post('/api/login', authentication.login.bind(authentication))
|
router.post('/api/login', authentication.login.bind(authentication))
|
||||||
|
router.post('/api/login/user', authentication.loginUser.bind(authentication))
|
||||||
|
|
||||||
// API Media
|
// API Media
|
||||||
const media = new MediaRoutes()
|
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.put('/api/articles/:id', restrict(access.Manager), article.updateArticle.bind(article))
|
||||||
router.del('/api/articles/:id', restrict(access.Manager), article.removeArticle.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
|
export default router
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import bookshelf from '../bookshelf.mjs'
|
import bookshelf from '../bookshelf.mjs'
|
||||||
|
import bcrypt from 'bcrypt'
|
||||||
|
import config from '../config.mjs'
|
||||||
|
|
||||||
/* Staff model:
|
/* Staff model:
|
||||||
{
|
{
|
||||||
|
@ -16,13 +18,57 @@ import bookshelf from '../bookshelf.mjs'
|
||||||
|
|
||||||
const Staff = bookshelf.createModel({
|
const Staff = bookshelf.createModel({
|
||||||
tableName: 'staff',
|
tableName: 'staff',
|
||||||
|
|
||||||
|
privateFields: bookshelf.safeColumns([
|
||||||
|
'fullname',
|
||||||
|
'email',
|
||||||
|
'level',
|
||||||
|
]),
|
||||||
}, {
|
}, {
|
||||||
// Hide password from any relations and include requests.
|
// Hide password from any relations and include requests.
|
||||||
publicFields: bookshelf.safeColumns([
|
publicFields: bookshelf.safeColumns([
|
||||||
'username',
|
|
||||||
'fullname',
|
'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
|
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 'pages';
|
||||||
@import 'articles';
|
@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),
|
data: JSON.parse(xhr.responseText),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (xhr.responseText) {
|
||||||
out = JSON.parse(xhr.responseText)
|
out = JSON.parse(xhr.responseText)
|
||||||
|
} else {
|
||||||
|
out = {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (xhr.status >= 300) {
|
if (xhr.status >= 300) {
|
||||||
throw out
|
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 'admin/admin';
|
||||||
@import 'widgets/common';
|
@import 'widgets/common';
|
||||||
@import 'pages/page';
|
@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-repeat: no-repeat;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
width: 100%;
|
width: calc(100% - 40px);
|
||||||
max-width: 1920px;
|
max-width: 1920px;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
flex: 0 0 150px;
|
flex: 0 0 150px;
|
||||||
|
@ -14,6 +14,8 @@
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-size: 1.6em;
|
font-size: 1.6em;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
|
text-decoration: none;
|
||||||
|
margin: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
frontpage {
|
frontpage {
|
||||||
|
@ -29,4 +31,20 @@ frontpage {
|
||||||
.loading-spinner {
|
.loading-spinner {
|
||||||
height: 100px;
|
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 Logout = require('./login/logout')
|
||||||
const EditPage = require('./admin/editpage')
|
const EditPage = require('./admin/editpage')
|
||||||
const Page = require('./pages/page')
|
const Page = require('./pages/page')
|
||||||
|
const Article = require('./article/article')
|
||||||
const AdminPages = require('./admin/pages')
|
const AdminPages = require('./admin/pages')
|
||||||
const AdminArticles = require('./admin/articles')
|
const AdminArticles = require('./admin/articles')
|
||||||
const EditArticle = require('./admin/editarticle')
|
const EditArticle = require('./admin/editarticle')
|
||||||
|
const AdminStaffList = require('./admin/stafflist')
|
||||||
|
const EditStaff = require('./admin/editstaff')
|
||||||
|
|
||||||
const menuRoot = document.getElementById('nav')
|
const menuRoot = document.getElementById('nav')
|
||||||
const mainRoot = document.getElementById('main')
|
const mainRoot = document.getElementById('main')
|
||||||
|
@ -20,9 +23,12 @@ m.route(mainRoot, '/', {
|
||||||
'/login': Login,
|
'/login': Login,
|
||||||
'/logout': Logout,
|
'/logout': Logout,
|
||||||
'/page/:id': Page,
|
'/page/:id': Page,
|
||||||
|
'/article/:id': Article,
|
||||||
'/admin/pages': AdminPages,
|
'/admin/pages': AdminPages,
|
||||||
'/admin/pages/:key': EditPage,
|
'/admin/pages/:key': EditPage,
|
||||||
'/admin/articles': AdminArticles,
|
'/admin/articles': AdminArticles,
|
||||||
'/admin/articles/:id': EditArticle,
|
'/admin/articles/:id': EditArticle,
|
||||||
|
'/admin/staff': AdminStaffList,
|
||||||
|
'/admin/staff/:id': EditStaff,
|
||||||
})
|
})
|
||||||
m.mount(menuRoot, Menu)
|
m.mount(menuRoot, Menu)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const m = require('mithril')
|
const m = require('mithril')
|
||||||
const Authentication = require('../authentication')
|
const Authentication = require('../authentication')
|
||||||
|
const { sendRequest } = require('../api/common')
|
||||||
|
|
||||||
const Login = {
|
const Login = {
|
||||||
loadedGoogle: false,
|
loadedGoogle: false,
|
||||||
|
@ -45,7 +46,6 @@ const Login = {
|
||||||
|
|
||||||
onGoogleFailure: function(error) {
|
onGoogleFailure: function(error) {
|
||||||
if (error.error !== 'popup_closed_by_user' && error.error !== 'popup_blocked_by_browser') {
|
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
|
Login.error = 'Error while logging into Google: ' + error.error
|
||||||
m.redraw()
|
m.redraw()
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,9 @@ const Login = {
|
||||||
Login.redirect = vnode.attrs.redirect || ''
|
Login.redirect = vnode.attrs.redirect || ''
|
||||||
if (Authentication.currentUser) return m.route.set('/')
|
if (Authentication.currentUser) return m.route.set('/')
|
||||||
Login.error = ''
|
Login.error = ''
|
||||||
|
|
||||||
|
this.username = ''
|
||||||
|
this.password = ''
|
||||||
},
|
},
|
||||||
|
|
||||||
oncreate: function() {
|
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 [
|
return [
|
||||||
m('div.login-wrapper', [
|
m('div.login-wrapper', [
|
||||||
m('article.login', [
|
m('article.login', [
|
||||||
|
@ -73,9 +111,31 @@ const Login = {
|
||||||
m('h1', 'NFP.moe login'),
|
m('h1', 'NFP.moe login'),
|
||||||
]),
|
]),
|
||||||
m('div.content', [
|
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.error ? m('div.error', Login.error) : null,
|
||||||
Login.loading ? m('div.loading-spinner') : 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')),
|
m('div#googlesignin', { hidden: Login.loading }, m('div.loading-spinner')),
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
|
|
|
@ -30,4 +30,9 @@ article.login {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
align-self: stretch;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,12 +45,13 @@ const Menu = {
|
||||||
'Welcome ' + Authentication.currentUser.email,
|
'Welcome ' + Authentication.currentUser.email,
|
||||||
m(m.route.Link, { href: '/logout' }, 'Logout'),
|
m(m.route.Link, { href: '/logout' }, 'Logout'),
|
||||||
]),
|
]),
|
||||||
(Authentication.currentUser.level >= 100
|
(Authentication.currentUser.level >= 10
|
||||||
? [
|
? m('div.adminlinks', [
|
||||||
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'),
|
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
|
: null
|
||||||
),
|
),
|
||||||
] : [
|
] : [
|
||||||
|
|
|
@ -49,6 +49,18 @@
|
||||||
line-height: 1.4em;
|
line-height: 1.4em;
|
||||||
text-decoration: none;
|
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;
|
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', [
|
return m('newsentry', [
|
||||||
vnode.attrs.media
|
vnode.attrs.media
|
||||||
? m('a.cover', {
|
? m('a.cover', {
|
||||||
href: vnode.attrs.media.large_url,
|
href: '/article/' + vnode.attrs.path,
|
||||||
}, m('img', { src: vnode.attrs.media.small_url }))
|
}, m('img', { src: vnode.attrs.media.small_url }))
|
||||||
: m('a.cover.nobg'),
|
: m('a.cover.nobg'),
|
||||||
m('div.entrycontent', [
|
m('div.entrycontent', [
|
||||||
|
|
|
@ -11,8 +11,8 @@ const Newsitem = {
|
||||||
m('div.newsitemcontent', [
|
m('div.newsitemcontent', [
|
||||||
vnode.attrs.media
|
vnode.attrs.media
|
||||||
? m('a.cover', {
|
? m('a.cover', {
|
||||||
href: vnode.attrs.media.large_url,
|
href: '/article/' + vnode.attrs.path,
|
||||||
}, m('img', { src: vnode.attrs.media.small_url }))
|
}, m('img', { src: vnode.attrs.media.medium_url }))
|
||||||
: m('a.cover.nobg'),
|
: m('a.cover.nobg'),
|
||||||
m('div.entrycontent', [
|
m('div.entrycontent', [
|
||||||
(vnode.attrs.description
|
(vnode.attrs.description
|
||||||
|
|
|
@ -6,6 +6,7 @@ exports.up = function up(knex, Promise) {
|
||||||
table.increments()
|
table.increments()
|
||||||
table.text('email')
|
table.text('email')
|
||||||
table.text('fullname')
|
table.text('fullname')
|
||||||
|
table.text('password')
|
||||||
table.boolean('is_deleted')
|
table.boolean('is_deleted')
|
||||||
.notNullable()
|
.notNullable()
|
||||||
.default(false)
|
.default(false)
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
"homepage": "https://github.com/nfp-projects/nfp_moe",
|
"homepage": "https://github.com/nfp-projects/nfp_moe",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@koa/cors": "^2.2.3",
|
"@koa/cors": "^2.2.3",
|
||||||
|
"bcrypt": "^3.0.0",
|
||||||
"bookshelf": "^0.15.1",
|
"bookshelf": "^0.15.1",
|
||||||
"bunyan-lite": "^1.0.1",
|
"bunyan-lite": "^1.0.1",
|
||||||
"format-link-header": "^2.1.0",
|
"format-link-header": "^2.1.0",
|
||||||
|
|
Loading…
Reference in a new issue