many updates
This commit is contained in:
parent
beb65dcc07
commit
18c7c25eed
32 changed files with 887 additions and 26 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -60,5 +60,7 @@ typings/
|
|||
# Local development config file
|
||||
config/config.json
|
||||
package-lock.json
|
||||
public/*
|
||||
public/assets/app.js
|
||||
public/assets/app.css
|
||||
public/assets/app.css.map
|
||||
|
||||
|
|
14
api/authentication/google.mjs
Normal file
14
api/authentication/google.mjs
Normal file
|
@ -0,0 +1,14 @@
|
|||
import google from 'googleapis'
|
||||
import googleauth from 'google-auth-library'
|
||||
import config from '../config'
|
||||
|
||||
const oauth2Client = new googleauth.OAuth2Client(config.get('googleid'))
|
||||
|
||||
// This is hard to have always running as it requires a
|
||||
// test access token which always expire.
|
||||
|
||||
/* istanbul ignore next */
|
||||
export function getProfile(token) {
|
||||
return oauth2Client.getTokenInfo(token)
|
||||
}
|
||||
|
39
api/authentication/routes.mjs
Normal file
39
api/authentication/routes.mjs
Normal file
|
@ -0,0 +1,39 @@
|
|||
import Staff from '../staff/model'
|
||||
import Jwt from '../jwt'
|
||||
import * as google from './google'
|
||||
|
||||
export default class AuthRoutes {
|
||||
constructor(opts = {}) {
|
||||
Object.assign(this, {
|
||||
Staff: opts.Staff || Staff,
|
||||
jwt: opts.jwt || new Jwt(),
|
||||
google: opts.google || google,
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* POST /api/login - Authenticate a user using social login
|
||||
*
|
||||
* @body {string} token - The google token to authenticate
|
||||
* @returns
|
||||
*
|
||||
* { token: 'Authentication token' }
|
||||
*/
|
||||
async login(ctx) {
|
||||
let output = await google.getProfile(ctx.request.body.token)
|
||||
|
||||
if (output.email_verified !== 'true') ctx.throw(422, 'Email was not verified with google')
|
||||
if (!output.email) ctx.throw(422, 'Email was missing from google response')
|
||||
|
||||
let level = 1
|
||||
let staff = await this.Staff
|
||||
.query({ where: { email: output.email }})
|
||||
.fetch({ require: false })
|
||||
|
||||
if (staff && staff.id && staff.get('level')) {
|
||||
level = staff.get('level')
|
||||
}
|
||||
|
||||
ctx.body = { token: this.jwt.createToken(output.email, level) }
|
||||
}
|
||||
}
|
13
api/jwt.mjs
13
api/jwt.mjs
|
@ -43,16 +43,11 @@ export default class Jwt {
|
|||
return this.jwt.decode(token)
|
||||
}
|
||||
|
||||
createStaffToken(staff, opts) {
|
||||
createToken(email, level, opts) {
|
||||
return this.sign({
|
||||
id: staff.id,
|
||||
level: staff.get('level'),
|
||||
}, staff.get('password'), opts)
|
||||
}
|
||||
|
||||
async getUserSecret(header, payload) {
|
||||
let staff = await this.Staff.getSingle(payload.id)
|
||||
return staff.id
|
||||
email: email,
|
||||
level: level,
|
||||
}, email, opts)
|
||||
}
|
||||
|
||||
static jwtMiddleware() {
|
||||
|
|
56
api/media/model.mjs
Normal file
56
api/media/model.mjs
Normal file
|
@ -0,0 +1,56 @@
|
|||
import path from 'path'
|
||||
import bookshelf from '../bookshelf'
|
||||
|
||||
/*
|
||||
|
||||
Media model:
|
||||
{
|
||||
filename,
|
||||
filetype,
|
||||
small_image,
|
||||
medium_image,
|
||||
large_image,
|
||||
*small_url,
|
||||
*medium_url,
|
||||
*large_url,
|
||||
size,
|
||||
staff_id,
|
||||
is_deleted,
|
||||
created_at,
|
||||
updated_at,
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
const Media = bookshelf.createModel({
|
||||
tableName: 'media',
|
||||
|
||||
virtuals: {
|
||||
small_url() {
|
||||
return `${Media.baseUrl}${this.get('small_image')}`
|
||||
},
|
||||
|
||||
medium_url() {
|
||||
return `${Media.baseUrl}${this.get('medium_image')}`
|
||||
},
|
||||
|
||||
large_url() {
|
||||
return `${Media.baseUrl}${this.get('large_image')}`
|
||||
},
|
||||
},
|
||||
}, {
|
||||
baseUrl: 'https://cdn-nfp.global.ssl.fastly.net',
|
||||
|
||||
getSubUrl(input, size) {
|
||||
if (!input) return input
|
||||
|
||||
let output = input
|
||||
if (path.extname(input)) {
|
||||
let ext = path.extname(input).toLowerCase()
|
||||
output = input.slice(0, -ext.length)
|
||||
}
|
||||
return `${output}.${size}.jpg`
|
||||
},
|
||||
})
|
||||
|
||||
export default Media
|
15
api/media/multer.mjs
Normal file
15
api/media/multer.mjs
Normal file
|
@ -0,0 +1,15 @@
|
|||
import multer from 'multer'
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
filename: (req, file, cb) => cb(null, file.originalname),
|
||||
})
|
||||
const upload = multer({ storage: storage })
|
||||
|
||||
export function processBody(ctx) {
|
||||
return new Promise((res, rej) => {
|
||||
upload.single('file')(ctx.req, ctx.res, (err) => {
|
||||
if (err) return rej(err)
|
||||
return res(ctx.req.file)
|
||||
})
|
||||
})
|
||||
}
|
38
api/media/resize.mjs
Normal file
38
api/media/resize.mjs
Normal file
|
@ -0,0 +1,38 @@
|
|||
import sharp from 'sharp'
|
||||
import Media from './model'
|
||||
|
||||
export default class Resizer {
|
||||
constructor(opts = {}) {
|
||||
Object.assign(this, {
|
||||
Media: opts.Media || Media,
|
||||
sharp: opts.sharp || sharp,
|
||||
})
|
||||
}
|
||||
|
||||
createSmall(input) {
|
||||
let output = this.Media.getSubUrl(input, 'small')
|
||||
|
||||
return this.sharp(input)
|
||||
.resize(300, 300)
|
||||
.toFile(output)
|
||||
.then(() => output)
|
||||
}
|
||||
|
||||
createMedium(input) {
|
||||
let output = this.Media.getSubUrl(input, 'medium')
|
||||
|
||||
return this.sharp(input)
|
||||
.resize(700, 700)
|
||||
.toFile(output)
|
||||
.then(() => output)
|
||||
}
|
||||
|
||||
autoRotate(input) {
|
||||
const output = `${input}_2.jpg`
|
||||
|
||||
return this.sharp(input)
|
||||
.rotate()
|
||||
.toFile(output)
|
||||
.then(() => output)
|
||||
}
|
||||
}
|
43
api/media/routes.mjs
Normal file
43
api/media/routes.mjs
Normal file
|
@ -0,0 +1,43 @@
|
|||
import config from '../config'
|
||||
import Media from './model'
|
||||
import * as multer from './multer'
|
||||
import Resizer from './resize'
|
||||
import { uploadFile } from './upload'
|
||||
import Jwt from '../jwt'
|
||||
|
||||
export default class MediaRoutes {
|
||||
constructor(opts = {}) {
|
||||
Object.assign(this, {
|
||||
Media: opts.Media || Media,
|
||||
multer: opts.multer || multer,
|
||||
resize: opts.resize || new Resizer(),
|
||||
jwt: opts.jwt || new Jwt(),
|
||||
uploadFile: opts.uploadFile || uploadFile,
|
||||
})
|
||||
}
|
||||
|
||||
async upload(ctx) {
|
||||
let result = await this.multer.processBody(ctx)
|
||||
|
||||
let smallPath = await this.resize.createSmall(result.path)
|
||||
let mediumPath = await this.resize.createMedium(result.path)
|
||||
|
||||
let token = this.jwt.signDirect({ site: config.get('upload:name') }, config.get('upload:secret'))
|
||||
|
||||
let [large, small, medium] = await Promise.all([
|
||||
this.uploadFile(token, result.path),
|
||||
this.uploadFile(token, smallPath),
|
||||
this.uploadFile(token, mediumPath),
|
||||
])
|
||||
|
||||
ctx.body = await this.Media.create({
|
||||
filename: result.originalname,
|
||||
filetype: result.mimetype,
|
||||
small_image: small.path,
|
||||
medium_image: medium.path,
|
||||
large_image: large.path,
|
||||
size: result.size,
|
||||
staff_id: ctx.state.user.id,
|
||||
})
|
||||
}
|
||||
}
|
73
api/media/upload.mjs
Normal file
73
api/media/upload.mjs
Normal file
|
@ -0,0 +1,73 @@
|
|||
import http from 'http'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
|
||||
let stub
|
||||
|
||||
export function uploadFile(token, file) {
|
||||
// For testing
|
||||
if (stub) return stub(token, file)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(file, (err, data) => {
|
||||
if (err) return reject(err)
|
||||
|
||||
const crlf = '\r\n'
|
||||
const filename = path.basename(file)
|
||||
const boundary = `--${Math.random().toString(16)}`
|
||||
const headers = [
|
||||
`Content-Disposition: form-data; name="file"; filename="${filename}"` + crlf,
|
||||
]
|
||||
const multipartBody = Buffer.concat([
|
||||
new Buffer(
|
||||
`${crlf}--${boundary}${crlf}` +
|
||||
headers.join('') + crlf
|
||||
),
|
||||
data,
|
||||
new Buffer(
|
||||
`${crlf}--${boundary}--`
|
||||
),
|
||||
])
|
||||
|
||||
const options = {
|
||||
port: 2111,
|
||||
hostname: 'storage01.nfp.is',
|
||||
method: 'POST',
|
||||
path: '/media?token=' + token,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data; boundary=' + boundary,
|
||||
'Content-Length': multipartBody.length,
|
||||
},
|
||||
}
|
||||
|
||||
const req = http.request(options)
|
||||
|
||||
req.write(multipartBody)
|
||||
req.end()
|
||||
|
||||
req.on('error', reject)
|
||||
|
||||
req.on('response', res => {
|
||||
res.setEncoding('utf8')
|
||||
let output = ''
|
||||
|
||||
res.on('data', function (chunk) {
|
||||
output += chunk.toString()
|
||||
})
|
||||
|
||||
res.on('end', function () {
|
||||
try {
|
||||
output = JSON.parse(output)
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
resolve(output)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function overrideStub(newStub) {
|
||||
stub = newStub
|
||||
}
|
|
@ -1,14 +1,19 @@
|
|||
/* eslint max-len: 0 */
|
||||
import Router from 'koa-router'
|
||||
|
||||
// import access from './access'
|
||||
// import AuthRoutes from './authentication/routes'
|
||||
// import { restrict } from './access/middleware'
|
||||
import access from './access'
|
||||
import AuthRoutes from './authentication/routes'
|
||||
import MediaRoutes from './media/routes'
|
||||
import { restrict } from './access/middleware'
|
||||
|
||||
const router = new Router()
|
||||
|
||||
// API Authentication
|
||||
// const authentication = new AuthRoutes()
|
||||
// router.post('/api/login', authentication.login.bind(authentication))
|
||||
const authentication = new AuthRoutes()
|
||||
router.post('/api/login', authentication.login.bind(authentication))
|
||||
|
||||
// API Media
|
||||
const media = new MediaRoutes()
|
||||
router.post('/api/media', restrict(access.Manager), media.upload.bind(media))
|
||||
|
||||
export default router
|
||||
|
|
17
app/_common.scss
Normal file
17
app/_common.scss
Normal file
|
@ -0,0 +1,17 @@
|
|||
$primary-bg: #01579b;
|
||||
$primary-fg: white;
|
||||
$primary-light-bg: #4f83cc;
|
||||
$primary-light-fg: white;
|
||||
$primary-dark-bg: #002f6c;
|
||||
$primary-dark-fg: white;
|
||||
|
||||
$secondary-bg: #f57c00;
|
||||
$secondary-fg: black;
|
||||
$secondary-light-bg: #ffad42;
|
||||
$secondary-light-fg: black;
|
||||
$secondary-dark-bg: #bb4d00;
|
||||
$secondary-dark-fg: white;
|
||||
|
||||
$border: #ccc;
|
||||
$title-fg: #555;
|
||||
$meta-fg: #999;
|
41
app/admin/admin.scss
Normal file
41
app/admin/admin.scss
Normal file
|
@ -0,0 +1,41 @@
|
|||
|
||||
.admin-wrapper {
|
||||
flex-grow: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: $primary-bg;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
article.editcat {
|
||||
text-align: center;
|
||||
background: white;
|
||||
padding: 0 0 20px;
|
||||
|
||||
header {
|
||||
padding: 10px;
|
||||
background: $secondary-bg;
|
||||
|
||||
h1 {
|
||||
color: $secondary-fg;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
padding: 20px 40px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 240px;
|
||||
height: 50px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
@import 'editcat'
|
27
app/admin/editcat.js
Normal file
27
app/admin/editcat.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
const m = require('mithril')
|
||||
const Authentication = require('../authentication')
|
||||
|
||||
const EditCategory = {
|
||||
loading: true,
|
||||
|
||||
oninit: function(vnode) {
|
||||
console.log(vnode.attrs)
|
||||
EditCategory.loading = !!m.route.param('id')
|
||||
},
|
||||
|
||||
view: function() {
|
||||
return (
|
||||
EditCategory.loading ?
|
||||
m('div.loading-spinner')
|
||||
: m('div.admin-wrapper',
|
||||
m('article.editcat', [
|
||||
m('header', m('h1', 'Edit category')),
|
||||
m('form.editcat', [
|
||||
])
|
||||
])
|
||||
)
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = EditCategory
|
0
app/admin/editcat.scss
Normal file
0
app/admin/editcat.scss
Normal file
|
@ -1,4 +0,0 @@
|
|||
|
||||
body
|
||||
margin: 0
|
||||
padding: 0
|
112
app/app.scss
Normal file
112
app/app.scss
Normal file
|
@ -0,0 +1,112 @@
|
|||
@import './_common';
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
*, *:before, *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body, h1, h2, h3, h4, h5, h6, p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@keyframes spinner-loader {
|
||||
to {transform: rotate(360deg);}
|
||||
}
|
||||
|
||||
.loading-spinner:before {
|
||||
content: '';
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: -10px;
|
||||
margin-left: -10px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #ccc;
|
||||
border-top-color: #333;
|
||||
animation: spinner-loader .6s linear infinite;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
.error {
|
||||
font-size: 0.8em;
|
||||
color: $secondary-dark-bg;
|
||||
font-weight: bold;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
[hidden] { display: none !important; }
|
||||
|
||||
article {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 2;
|
||||
padding: 20px;
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h1 {
|
||||
font-size: 1.4em;
|
||||
color: $title-fg;
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 0.8em;
|
||||
color: $meta-fg;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
h5 {
|
||||
font-size: 1.0em;
|
||||
font-weight: bold;
|
||||
color: $title-fg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@import 'menu/menu';
|
||||
@import 'login/login';
|
||||
@import 'admin/admin';
|
56
app/authentication.js
Normal file
56
app/authentication.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
const m = require('mithril')
|
||||
const jwt = require('jsonwebtoken')
|
||||
|
||||
const storageName = 'logintoken'
|
||||
const loadingListeners = []
|
||||
|
||||
window.googleLoaded = function() {
|
||||
Authentication.loadedGoogle = true
|
||||
while (Authentication.loadingListeners.length) {
|
||||
Authentication.loadingListeners.pop()()
|
||||
}
|
||||
}
|
||||
|
||||
const Authentication = {
|
||||
currentUser: null,
|
||||
loadedGoogle: false,
|
||||
loadingGoogle: false,
|
||||
loadingListeners: [],
|
||||
|
||||
updateToken: function(token) {
|
||||
if (!token) return Authentication.clearToken()
|
||||
localStorage.setItem(storageName, token)
|
||||
Authentication.currentUser = jwt.decode(token)
|
||||
},
|
||||
|
||||
clearToken: function() {
|
||||
Authentication.currentUser = null
|
||||
localStorage.removeItem(storageName)
|
||||
},
|
||||
|
||||
createGoogleScript: function() {
|
||||
if (Authentication.loadedGoogle) return Promise.resolve()
|
||||
return new Promise(function (res) {
|
||||
if (Authentication.loadedGoogle) return res()
|
||||
Authentication.loadingListeners.push(res)
|
||||
|
||||
if (Authentication.loadingGoogle) return
|
||||
Authentication.loadingGoogle = true
|
||||
|
||||
let gscript = document.createElement('script')
|
||||
gscript.type = 'text/javascript'
|
||||
gscript.async = true
|
||||
gscript.defer = true
|
||||
gscript.src = 'https://apis.google.com/js/platform.js?onload=googleLoaded'
|
||||
document.body.appendChild(gscript)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Authentication.updateToken(localStorage.getItem(storageName))
|
||||
|
||||
if (Authentication.currentUser) {
|
||||
// Authentication.createGoogleScript()
|
||||
}
|
||||
|
||||
module.exports = Authentication
|
12
app/frontpage/frontpage.js
Normal file
12
app/frontpage/frontpage.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
const m = require('mithril')
|
||||
|
||||
module.exports = {
|
||||
view: function() {
|
||||
return m('article', [
|
||||
m('header', [
|
||||
m('h1', 'Welcome to NFP Moe'),
|
||||
m('span.meta', 'Last updated many years ago'),
|
||||
]),
|
||||
])
|
||||
}
|
||||
}
|
22
app/index.js
22
app/index.js
|
@ -1 +1,21 @@
|
|||
console.log('Success')
|
||||
const m = require('mithril')
|
||||
|
||||
m.route.prefix('')
|
||||
|
||||
const Authentication = require('./authentication')
|
||||
const Menu = require('./menu/menu')
|
||||
const Frontpage = require('./frontpage/frontpage')
|
||||
const Login = require('./login/login')
|
||||
const Logout = require('./login/logout')
|
||||
const EditCategory = require('./admin/editcat')
|
||||
|
||||
const menuRoot = document.getElementById('nav')
|
||||
const mainRoot = document.getElementById('main')
|
||||
|
||||
m.route(mainRoot, '/', {
|
||||
'/': Frontpage,
|
||||
'/login': Login,
|
||||
'/logout': Logout,
|
||||
'/admin/addcat': EditCategory,
|
||||
})
|
||||
m.mount(menuRoot, Menu)
|
||||
|
|
83
app/login/login.js
Normal file
83
app/login/login.js
Normal file
|
@ -0,0 +1,83 @@
|
|||
const m = require('mithril')
|
||||
const Authentication = require('../authentication')
|
||||
|
||||
const Login = {
|
||||
loadedGoogle: false,
|
||||
loading: false,
|
||||
error: '',
|
||||
|
||||
initGoogleButton: function() {
|
||||
gapi.signin2.render('googlesignin', {
|
||||
'scope': 'email',
|
||||
'width': 240,
|
||||
'height': 50,
|
||||
'longtitle': true,
|
||||
'theme': 'dark',
|
||||
'onsuccess': Login.onGoogleSuccess,
|
||||
'onfailure': Login.onGoogleFailure,
|
||||
});
|
||||
},
|
||||
|
||||
onGoogleSuccess: function(googleUser) {
|
||||
Login.loading = true
|
||||
m.redraw()
|
||||
|
||||
m.request({
|
||||
method: 'POST',
|
||||
url: '/api/login',
|
||||
data: { token: googleUser.Zi.access_token },
|
||||
})
|
||||
.then(function(result) {
|
||||
Authentication.updateToken(result.token)
|
||||
m.route.set('/')
|
||||
})
|
||||
.catch(function(error) {
|
||||
Login.error = 'Error while logging into NFP! ' + error.code + ': ' + error.response.message
|
||||
let auth2 = gapi.auth2.getAuthInstance();
|
||||
return auth2.signOut()
|
||||
})
|
||||
.then(function () {
|
||||
Login.loading = false
|
||||
m.redraw()
|
||||
})
|
||||
},
|
||||
|
||||
onGoogleFailure: function(error) {
|
||||
Login.error = 'Error while logging into Google: ' + error
|
||||
m.redraw()
|
||||
Authentication.createGoogleScript()
|
||||
},
|
||||
|
||||
oninit: function() {
|
||||
if (Authentication.currentUser) return m.route.set('/')
|
||||
Login.error = ''
|
||||
},
|
||||
|
||||
oncreate: function() {
|
||||
if (Authentication.currentUser) return
|
||||
Authentication.createGoogleScript()
|
||||
.then(function() {
|
||||
Login.initGoogleButton()
|
||||
})
|
||||
},
|
||||
|
||||
view: function() {
|
||||
return [
|
||||
m('div.login-wrapper', [
|
||||
m('article.login', [
|
||||
m('header', [
|
||||
m('h1', 'NFP.moe login'),
|
||||
]),
|
||||
m('div.content', [
|
||||
m('h5', 'Please login using google to access restricted area'),
|
||||
Login.error ? m('div.error', Login.error) : null,
|
||||
Login.loading ? m('div.loading-spinner') : null,
|
||||
m('div#googlesignin', { hidden: Login.loading }, m('div.loading-spinner')),
|
||||
])
|
||||
]),
|
||||
]),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Login
|
33
app/login/login.scss
Normal file
33
app/login/login.scss
Normal file
|
@ -0,0 +1,33 @@
|
|||
@import '../_common';
|
||||
|
||||
.login-wrapper {
|
||||
flex-grow: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
background: $border;
|
||||
}
|
||||
|
||||
article.login {
|
||||
text-align: center;
|
||||
flex-grow: 0;
|
||||
border: 1px solid $title-fg;
|
||||
background: white;
|
||||
align-self: center;
|
||||
|
||||
.content {
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
padding: 20px 40px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 240px;
|
||||
height: 50px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
28
app/login/logout.js
Normal file
28
app/login/logout.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
const m = require('mithril')
|
||||
const Authentication = require('../authentication')
|
||||
|
||||
const Logout = {
|
||||
oninit: function() {
|
||||
Authentication.createGoogleScript()
|
||||
.then(function() {
|
||||
return new Promise(function (res) {
|
||||
gapi.load('auth2', res);
|
||||
})
|
||||
})
|
||||
.then(function() { return gapi.auth2.init() })
|
||||
.then(function() {
|
||||
let auth2 = gapi.auth2.getAuthInstance();
|
||||
return auth2.signOut()
|
||||
})
|
||||
.then(function() {
|
||||
Authentication.clearToken()
|
||||
m.route.set('/')
|
||||
})
|
||||
},
|
||||
|
||||
view: function() {
|
||||
return m('div.loading-spinner')
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = Logout
|
47
app/menu/menu.js
Normal file
47
app/menu/menu.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
const m = require('mithril')
|
||||
const Authentication = require('../authentication')
|
||||
|
||||
const Menu = {
|
||||
currentActive: 'home',
|
||||
|
||||
onbeforeupdate: function() {
|
||||
let currentPath = m.route.get()
|
||||
if (currentPath === '/') Menu.currentActive = 'home'
|
||||
else if (currentPath === '/login') Menu.currentActive = 'login'
|
||||
else Menu.currentActive = 'none'
|
||||
},
|
||||
|
||||
oninit: function() {
|
||||
Menu.onbeforeupdate()
|
||||
},
|
||||
|
||||
view: function() {
|
||||
return [
|
||||
m('div.top', [
|
||||
m('h2', 'NFP Moe'),
|
||||
m('aside', Authentication.currentUser ? [
|
||||
m('p', 'Welcome ' + Authentication.currentUser.email),
|
||||
(Authentication.currentUser.level >= 100 ?
|
||||
m('a[href=/admin/addcat]', { oncreate: m.route.link }, 'Create category')
|
||||
: null
|
||||
),
|
||||
m('a[href=/logout]', { oncreate: m.route.link }, 'Logout')
|
||||
] : [
|
||||
m('a[href=/login]', { oncreate: m.route.link }, 'Login')
|
||||
])
|
||||
]),
|
||||
m('nav', [
|
||||
m('a[href=/]', {
|
||||
class: Menu.currentActive === 'home' ? 'active' : '',
|
||||
oncreate: m.route.link
|
||||
}, 'Home'),
|
||||
m('a[href=/articles]', {
|
||||
class: Menu.currentActive === 'articles' ? 'active' : '',
|
||||
oncreate: m.route.link
|
||||
}, 'Articles'),
|
||||
]),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Menu
|
65
app/menu/menu.scss
Normal file
65
app/menu/menu.scss
Normal file
|
@ -0,0 +1,65 @@
|
|||
@import '../_common';
|
||||
|
||||
#nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.top {
|
||||
background: url('./img/logo.png') 25px center no-repeat $primary-dark-bg;
|
||||
color: $primary-dark-fg;
|
||||
padding: 0 10px 0 120px;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
|
||||
h2 {
|
||||
flex-grow: 2;
|
||||
align-self: center;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
aside {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
|
||||
p {
|
||||
font-size: 0.8em;
|
||||
color: $meta-fg;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
a, a:visited {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
color: $secondary-light-bg;
|
||||
font-size: 0.8em;
|
||||
line-height: 1.4em;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
background: $primary-light-bg;
|
||||
color: $primary-light-fg;
|
||||
|
||||
a, a:visited {
|
||||
flex-grow: 2;
|
||||
flex-basis: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
color: $primary-light-fg;
|
||||
padding: 10px;
|
||||
font-size: 0.9em;
|
||||
text-decoration: none;
|
||||
|
||||
&.active {
|
||||
border-bottom: 3px solid $secondary-bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
0
app/widgets/common.scss
Normal file
0
app/widgets/common.scss
Normal file
26
app/widgets/fileupload.js
Normal file
26
app/widgets/fileupload.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
const m = require('mithril')
|
||||
|
||||
const Login = {
|
||||
oninit: function(vnode) {
|
||||
vnode.state.media = null
|
||||
vnode.state.error = ''
|
||||
},
|
||||
|
||||
view: function(vnode) {
|
||||
let media = vnode.state.media
|
||||
|
||||
return m('fileupload', [
|
||||
(media ?
|
||||
m('a', {
|
||||
href: media.large_url,
|
||||
style: {
|
||||
'background-image': 'url(' + media.medium_url + ')',
|
||||
}
|
||||
}) :
|
||||
m('div.empty')
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Login
|
|
@ -32,6 +32,7 @@
|
|||
"expiresIn": 604800
|
||||
}
|
||||
},
|
||||
"googleid": "1076074914074-3no1difo1jq3dfug3glfb25pn1t8idud.apps.googleusercontent.com",
|
||||
"sessionsecret": "this-is-session-secret-lol",
|
||||
"bcrypt": 5,
|
||||
"fileSize": 524288000,
|
||||
|
|
3
nodemon.json
Normal file
3
nodemon.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"ignore": ["app/**", "public/**"]
|
||||
}
|
|
@ -9,11 +9,11 @@
|
|||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"start": "node --experimental-modules index.mjs",
|
||||
"build": "sass app/app.sass public/assets/app.css && browserify -d app/index.js -o public/assets/app.js",
|
||||
"build": "sass app/app.scss public/assets/app.css && browserify -d app/index.js -o public/assets/app.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"watch:api": "nodemon --experimental-modules index.mjs | bunyan",
|
||||
"watch:app": "watchify app/index.js -o public/assets/app.js",
|
||||
"watch:sass": "sass --watch app/app.sass public/assets/app.css",
|
||||
"watch:app": "watchify -d app/index.js -o public/assets/app.js",
|
||||
"watch:sass": "sass --watch app/app.scss public/assets/app.css",
|
||||
"dev": "run-p watch:api watch:app watch:sass",
|
||||
"prod": "npm run build && npm start"
|
||||
},
|
||||
|
@ -32,6 +32,7 @@
|
|||
"bookshelf": "^0.14.2",
|
||||
"bunyan-lite": "^1.0.1",
|
||||
"format-link-header": "^2.1.0",
|
||||
"googleapis": "^37.2.0",
|
||||
"http-errors": "^1.7.2",
|
||||
"jsonwebtoken": "^8.4.0",
|
||||
"knex": "^0.16.3",
|
||||
|
|
BIN
public/assets/img/favicon.png
Normal file
BIN
public/assets/img/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
BIN
public/assets/img/logo.png
Normal file
BIN
public/assets/img/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
|
@ -1,6 +1,19 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>NFP Moe</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/png" href="/assets/img/favicon.png">
|
||||
<link rel="Stylesheet" href="/assets/app.css" type="text/css" />
|
||||
<meta name="google-signin-client_id" content="1076074914074-3no1difo1jq3dfug3glfb25pn1t8idud.apps.googleusercontent.com">
|
||||
</head>
|
||||
<body>
|
||||
Works
|
||||
<div class="container">
|
||||
<div id="nav"></div>
|
||||
<main id="main"></main>
|
||||
</div>
|
||||
<script type="text/javascript" src="/assets/app.js?v=1"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
Loading…
Reference in a new issue