Finished initial release-worthy version, added appveyor and starting testing appveyor auto building
continuous-integration/appveyor/branch AppVeyor build failed Details

This commit is contained in:
Jonatan Nilsson 2022-08-18 09:59:48 +00:00
parent 17882b457d
commit 71b1725655
199 changed files with 779 additions and 12287 deletions

View File

@ -1,48 +0,0 @@
version: 2
jobs:
build:
docker:
- image: docker:latest
environment:
- di: "nfpis/nfpmoe"
- dtag: "latest"
- service_name: "nfpmoe"
- target_port: "7030" # The public port
- service_port: "4030" # Container port
working_directory: ~/nfpmoe
steps:
- run:
name: Update and install SSH & Git & sed
command: apk update && apk upgrade && apk add --no-cache bash git openssh sed
- checkout
- setup_remote_docker
- run:
name: Replace version in config
command: |
sed -i "s/circleci_version_number/${CIRCLE_BUILD_NUM}/g" config/config.default.json
- run:
name: Build docker image
command: docker build -t ${di}:build_${CIRCLE_BUILD_NUM} -t ${di}:${CIRCLE_SHA1} -t ${di}:${dtag} .
- run:
name: Push to docker
command: |
docker login -u $DOCKER_USER -p $DOCKER_PASS
docker push ${di} --all-tags
- deploy:
name: Deploy to production
command: |
if [ "${CIRCLE_BRANCH}" != "master" ]; then
echo Not running on master. Exiting.
exit 0
fi
echo "$MASTER_HOST" | base64 -d > ~/.ssh/master_host
echo "$MASTER_KEY" | base64 -d > ~/.ssh/master_key
chmod 600 ~/.ssh/master_key
ssh -p 51120 -i ~/.ssh/master_key -o "UserKnownHostsFile ~/.ssh/master_host" root@212.30.212.2 "docker ${service_name} ${di}:build_${CIRCLE_BUILD_NUM} ${target_port} ${service_port}"
workflows:
version: 2
build_deploy:
jobs:
- build:
context: org-global

1
.npmrc
View File

@ -1 +0,0 @@
package-lock=false

View File

@ -1 +1 @@
# nfp_moe
# nfp_sites

82
appveyor.yml Normal file
View File

@ -0,0 +1,82 @@
# version format
version: '{build}'
deploy: on
# branches to build
branches:
# whitelist
only:
- master
# Do not build on tags (GitHub, Bitbucket, GitLab, Gitea)
skip_tags: true
# Maximum number of concurrent jobs for the project
max_jobs: 1
clone_depth: 1
# Build worker image (VM template)
build_cloud: Docker
environment:
docker_image: node:16-alpine
npm_config_cache: /appveyor/projects/cache
build_script:
- sh: |
chown -R node:node /appveyor/projects
chmod -R 777 /appveyor/projects
apk add curl jq
if [ $? -eq 0 ]; then
echo "Finished installling curl and jq"
else
exit 1
fi
for f in *; do
[ ! -d "$f" ] || [ -L "$f" ] || [ "$f" = "base" ] && continue;
echo "checking $f";
cd $f
CURR_VER="$(cat package.json | jq -r .name)_v$(cat package.json | jq -r .version)"
CURR_NAME="$(cat package.json | jq -r .name) v$(cat package.json | jq -r .version)"
echo "Checking https://git.nfp.is/api/v1/repos/$APPVEYOR_REPO_NAME/releases for name ${CURR_NAME}"
curl -s -X GET -H "Authorization: token $deploytoken" https://git.nfp.is/api/v1/repos/$APPVEYOR_REPO_NAME/releases | grep -o "\"name\"\:\"${CURR_NAME}\"" > /dev/null
if [ $? -eq 0 ] ; then
echo "Skipping $f since $CURR_NAME already exists";
else
rm base
cp -Rf ../base ./base
npm install
npm run build
./7zas a -xr!*.xcf -mx9 "${CURR_VER}_build-sc.7z" package.json index.mjs api base public
echo "Creating release on gitea"
RELEASE_RESULT=$(curl \
-X POST \
-H "Authorization: token $deploytoken" \
-H "Content-Type: application/json" \
https://git.nfp.is/api/v1/repos/$APPVEYOR_REPO_NAME/releases \
-d "{\"tag_name\":\"v${CURR_VER}\",\"name\":\"v${CURR_NAME}\",\"body\":\"Automatic release from Appveyor from ${APPVEYOR_REPO_COMMIT} :\n\n${APPVEYOR_REPO_COMMIT_MESSAGE}\"}")
RELEASE_ID=$(echo $RELEASE_RESULT | jq -r .id)
echo "Adding ${CURR_VER}_build-sc.7z to release ${RELEASE_ID}"
curl \
-X POST \
-H "Authorization: token $deploytoken" \
-F "attachment=@${CURR_VER}_build-sc.7z" \
https://git.nfp.is/api/v1/repos/$APPVEYOR_REPO_NAME/releases/$RELEASE_ID/assets
echo "Deplying to production"
MAN_PORT=$(cat package.json | jq -r .port)
MAN_NAME=$(cat package.json | jq -r .name)
Echo "curl -X POST http://192.168.93.50:$MAN_PORT/update/$MAN_NAME"
curl -X POST http://192.168.93.50:$MAN_PORT/update/$MAN_NAME
fi
cd..
done
# on build failure
on_failure:
- sh: echo on_failure

View File

@ -1,5 +1,5 @@
import { parseArticles, parseArticle } from './util.mjs'
import { uploadMedia, uploadFile } from '../media/upload.mjs'
import { uploadMedia, uploadFile, deleteFile } from '../media/upload.mjs'
import { mediaToDatabase } from '../media/util.mjs'
export default class ArticleRoutes {
@ -7,6 +7,7 @@ export default class ArticleRoutes {
Object.assign(this, {
uploadMedia: uploadMedia,
uploadFile: uploadFile,
deleteFile: deleteFile,
})
}
@ -69,7 +70,6 @@ export default class ArticleRoutes {
params = params.concat(mediaToDatabase(banner, body.remove_banner === 'true'))
params = params.concat(mediaToDatabase(media, body.remove_media === 'true'))
}
console.log(params)
let res = await ctx.db.safeCallProc('article_auth_get_update_create', params)
ctx.body = this.private_getUpdateArticle_resOutput(res)
@ -89,8 +89,6 @@ export default class ArticleRoutes {
/** PUT: /api/auth/articles/:id */
async auth_updateCreateSingleArticle(ctx) {
console.log(ctx.req.body)
let newBanner = null
let newMedia = null

View File

@ -3,7 +3,7 @@ import { HttpError } from 'flaska'
import { decode, encode } from '../util.mjs'
import config from '../config.mjs'
const levels = {
export const RankLevels = {
Normal: 1,
Manager: 10,
Admin: 100,
@ -12,7 +12,48 @@ const levels = {
const issuer = config.get('mssql:connectionUser')
const secret = config.get('jwtsecret')
export function authenticate(minLevel = levels.Manager) {
export function verifyValidToken(parts, minLevel) {
if (parts.length !== 4) {
throw new HttpError(401, 'Authentication token invalid')
}
const hmac = crypto.createHmac('sha256', secret)
hmac.update([parts[0], parts[1], parts[2]].join('.'))
let apiSignature = encode(hmac.digest())
if (apiSignature !== parts[3]) {
throw new HttpError(401, 'Authentication token invalid signature')
}
let header
let body
try {
header = JSON.parse(decode(parts[0]).toString('utf8'))
body = JSON.parse(decode(parts[1]).toString('utf8'))
} catch (err) {
throw new HttpError(401, 'Authentication token invalid json')
}
if (header.alg !== 'HS256') {
throw new HttpError(401, 'Authentication token invalid alg')
}
let unixNow = Math.floor(Date.now() / 1000)
// Validate token, add a little skew support for issued_at
if (body.iss !== issuer || !body.iat || !body.exp
|| body.iat > unixNow + 300 || body.exp <= unixNow) {
throw new HttpError(403, 'Authentication token expired or invalid')
}
if (body.rank < minLevel) {
throw new HttpError(401, 'User does not have access to this resource')
}
return body
}
export function authenticate(minLevel = RankLevels.Manager) {
return function(ctx) {
if (!ctx.req.headers.authorization) {
throw new HttpError(401, 'Authentication token missing')
@ -23,40 +64,7 @@ export function authenticate(minLevel = levels.Manager) {
let parts = ctx.req.headers.authorization.slice(7).split('.')
if (parts.length !== 4) {
throw new HttpError(401, 'Authentication token invalid')
}
const hmac = crypto.createHmac('sha256', secret)
const token = [parts[0], parts[1], parts[2]].join('.')
hmac.update(token)
let apiSignature = encode(hmac.digest())
if (apiSignature !== parts[3]) {
throw new HttpError(401, 'Authentication token invalid signature')
}
let header
let body
try {
header = JSON.parse(decode(parts[0]).toString('utf8'))
body = JSON.parse(decode(parts[1]).toString('utf8'))
} catch (err) {
throw new HttpError(401, 'Authentication token invalid json')
}
if (header.alg !== 'HS256') {
throw new HttpError(401, 'Authentication token invalid alg')
}
let unixNow = Math.floor(Date.now() / 1000)
// Validate token, add a little skew support for issued_at
if (body.iss !== issuer || !body.iat || !body.exp
|| body.iat > unixNow + 300 || body.exp <= unixNow) {
throw new HttpError(403, 'Authentication token expired or invalid')
}
ctx.state.auth_user = body
ctx.state.auth_token = token
ctx.state.auth_user = verifyValidToken(parts, minLevel)
ctx.state.auth_token = [parts[0], parts[1], parts[2]].join('.')
}
}

View File

@ -1,5 +1,6 @@
import _ from 'lodash'
import nconf from 'nconf-lite'
import Nconf from 'nconf-lite'
const nconf = new Nconf()
// Helper method for global usage.
nconf.inTest = () => nconf.get('NODE_ENV') === 'test'
@ -16,30 +17,15 @@ nconf.inTest = () => nconf.get('NODE_ENV') === 'test'
nconf.env({
separator: '__',
whitelist: [
'DATABASE_URL',
'NODE_ENV',
'server__port',
'server__host',
'knex__connection__host',
'knex__connection__user',
'knex__connection__database',
'knex__connection__password',
'knex__connectionslave__host',
'knex__connectionslave__user',
'knex__connectionslave__database',
'knex__connectionslave__password',
'upload__baseurl',
'upload__port',
'upload__host',
'upload__name',
'upload__secret',
'bunyan__name',
'mssql__connectionString',
'media__secret',
'media__iss',
'media__path',
'media__filePath',
'media__removePath',
'frontend__url',
'jwt__secret',
'sessionsecret',
'bcrypt',
'name',
'NODE_VERSION',
'jwtsecret',
],
parseValues: true,
})
@ -67,6 +53,7 @@ nconf.defaults({
"iss": "dev",
"path": "https://media.nfp.is/media/resize",
"filePath": "https://media.nfp.is/media",
"removePath": "https://media.nfp.is/media/",
"preview": {
"out": "base64",
"format": "avif",
@ -171,14 +158,7 @@ nconf.defaults({
}
},
},
"fileSize": 524288000,
"upload": {
"baseurl": "https://cdn.nfp.is",
"port": "2111",
"host": "storage01.nfp.is",
"name": "nfpmoe-dev",
"secret": "nfpmoe-dev"
}
"fileSize": 524288000
})

View File

@ -10,31 +10,6 @@ export function initPool(core, config) {
core.log.info('MSSQL connection open')
})
let waiting = false
/*pool.on('error', function(error) {
if (error.length) {
let msg = 'Error in MSSQL pool\n => ' + error[0].message.trim()
for (let i = 1; i < error.length; i++) {
msg += '\n => ' + error[i].message.trim()
}
core.log.error(msg)
} else {
core.log.error('Error in MSSQL pool')
core.log.error(error)
}
if (waiting) { return }
core.log.warn('Attempting to connect again in 5 seconds')
waiting = true
setTimeout(function() {
waiting = false
console.log('opening')
pool.open()
console.log('done')
}, 5000)
})*/
core.log.info('Attempting to connect to MSSQL server')
pool.open()

View File

@ -48,10 +48,8 @@ export default class Client {
reject(err)
})
req.on('timeout', function() {
console.log("req.on('timeout')")
req.destroy()
let d2 = new Date()
console.log((d2 - d1))
reject(new Error(`Request ${method} ${path} timed out`))
})
req.on('response', res => {
@ -141,6 +139,16 @@ export default class Client {
},
})
}
delete(url, body) {
let parsed = JSON.stringify(body)
return this.customRequest('DELETE', url, parsed, {
headers: {
'Content-Type': 'application/json',
'Content-Length': parsed.length,
},
})
}
upload(url, files, method = 'POST', body = {}) {
const boundary = `---------${this.random(32)}`

View File

@ -71,4 +71,13 @@ export function uploadFile(file) {
type: file.type,
}
})
}
}
export function deleteFile(filename) {
const media = config.get('media')
const client = new Client()
let token = client.createJwt({ iss: media.iss }, media.secret)
return client.delete(media.removePath + filename + '?token=' + token, { })
}

View File

@ -4,6 +4,6 @@
"flaska": "^1.3.1",
"formidable": "^1.2.6",
"msnodesqlv8": "^2.4.7",
"nconf-lite": "^1.0.1"
"nconf-lite": "^2.0.0"
}
}

View File

@ -1,4 +1,4 @@
import { parsePage, parsePagesToTree } from './util.mjs'
import { parsePage, parsePages, parsePagesToTree } from './util.mjs'
import { uploadMedia, uploadFile } from '../media/upload.mjs'
import { parseArticle, parseArticles } from '../article/util.mjs'
import { mediaToDatabase } from '../media/util.mjs'
@ -36,13 +36,17 @@ export default class PageRoutes {
}
/** GET: /api/pages/[path] */
async getPage(ctx) {
async getPage(ctx, onlyReturn = false) {
let res = await ctx.db.safeCallProc('pages_get_single', [
ctx.params.path || null,
Math.max(ctx.query.get('page') || 1, 1),
Math.min(ctx.query.get('per_page') || 10, 25),
])
if (onlyReturn) {
return this.getPage_resOut(res)
}
ctx.body = this.getPage_resOut(res)
}
@ -61,7 +65,7 @@ export default class PageRoutes {
ctx.state.auth_token
])
ctx.body = parsePagesToTree(res.first)
ctx.body = parsePagesToTree(parsePages(res.first))
}
async private_getUpdatePage(ctx, body = null, banner = null, media = null) {
@ -80,7 +84,6 @@ export default class PageRoutes {
params = params.concat(mediaToDatabase(banner, body.remove_banner === 'true'))
params = params.concat(mediaToDatabase(media, body.remove_media === 'true'))
}
console.log(params)
let res = await ctx.db.safeCallProc('pages_auth_get_update_create', params)
let out = {
@ -98,8 +101,6 @@ export default class PageRoutes {
/** PUT: /api/auth/pages/:id */
async auth_updateCreateSinglePage(ctx) {
console.log(ctx.req.body)
let newBanner = null
let newMedia = null

View File

@ -24,6 +24,13 @@ export function parsePagesToTree(pages) {
}
}
export function parsePages(pages) {
for (let i = 0; i < pages.length; i++) {
parsePage(pages[i])
}
return pages
}
export function parsePage(page) {
if (!page) {
return null

View File

@ -3,21 +3,26 @@ import dot from 'dot'
import { FileResponse, HttpError } from 'flaska'
import fs from 'fs/promises'
import fsSync from 'fs'
import { RankLevels, verifyValidToken } from './authentication/security.mjs'
export default class ServeHandler {
constructor(opts = {}) {
Object.assign(this, {
pageRoutes: opts.pageRoutes,
fs: opts.fs || fs,
fsSync: opts.fsSync || fsSync,
root: opts.root,
template: null,
frontend: opts.frontend || 'http://localhost:4000',
version: opts.version || 'version',
})
Object.assign(this, opts)
if (!opts.fs) {
this.fs = fs
}
if (!opts.fsSync) {
this.fsSync = fsSync
}
if (!opts.frontend) {
this.frontend = 'http://localhost:4000'
}
if (!opts.version) {
this.version = 'version'
}
let indexFile = fsSync.readFileSync(path.join(this.root, 'index.html'))
this.template = dot.template(indexFile.toString(), { argName: ['headerDescription', 'headerImage', 'headerTitle', 'headerUrl', 'payloadData', 'payloadLinks', 'payloadTree', 'version', 'nonce'] })
this.template = dot.template(indexFile.toString(), { argName: ['headerDescription', 'headerImage', 'headerTitle', 'headerUrl', 'payloadData', 'payloadTree', 'version', 'nonce'] })
// console.log(indexFile.toString())
}
@ -28,23 +33,35 @@ export default class ServeHandler {
/** GET: /::file */
serve(ctx) {
if (ctx.params.file.startsWith('api/')) {
throw new HttpError(404, 'Not Found: ' + ctx.params.file, { status: 404, message: 'Not Found: ' + ctx.params.file })
return this.serveIndex(ctx)
}
let file = path.resolve(path.join(this.root, ctx.params.file ? ctx.params.file : 'index.html'))
if (!ctx.params.file || ctx.params.file === 'index.html') {
if (!ctx.params.file
|| ctx.params.file === 'index.html'
|| ctx.params.file.startsWith('/page')
|| ctx.params.file.startsWith('/article')
|| ctx.params.file.startsWith('/admin')) {
return this.serveIndex(ctx)
}
if (!file.startsWith(this.root)) {
ctx.status = 404
ctx.body = 'HTTP 404 Error'
return
return this.serveIndex(ctx)
}
if (file.indexOf('admin') >= 0
&& (file.indexOf('.js') >= 0 || file.indexOf('.css') >= 0)) {
verifyValidToken((ctx.query.get('token') || '').split('.'), RankLevels.Manager)
}
return this.fs.stat(file)
.then(function(stat) {
if (file.indexOf('admin') === -1) {
ctx.headers['Cache-Control'] = 'max-age=2592000'
} else {
ctx.headers['Cache-Control'] = 'no-store'
}
ctx.body = new FileResponse(file, stat)
})
.catch((err) => {
@ -56,25 +73,7 @@ export default class ServeHandler {
}
async serveIndex(ctx) {
let payload = {
headerDescription: 'Small fansubbing and scanlation group translating and encoding our favourite shows from Japan.',
headerImage: this.frontend + '/assets/img/heart.png',
headerTitle: 'NFP Moe - Anime/Manga translation group',
headerUrl: this.frontend + ctx.url,
payloadData: null,
payloadLinks: null,
payloadTree: null,
version: this.version,
nonce: ctx.state.nonce,
}
try {
payload.payloadTree = JSON.stringify(await this.pageRoutes.getPageTree(ctx, true))
} catch (e) {
ctx.log.error(e)
}
ctx.body = this.template(payload)
ctx.body = this.template({})
ctx.type = 'text/html; charset=utf-8'
}

View File

@ -51,7 +51,6 @@ export default class ArticleRoutes extends Parent {
}
}
let file = await this.uploadFile(ctx.req.files.file)
console.log(file)
let params = [
ctx.state.auth_token,
@ -79,10 +78,10 @@ export default class ArticleRoutes extends Parent {
null,
null,
ctx.params.fileId,
0,
1,
]
let res = await ctx.db.safeCallProc('article_auth_file_create_delete', params)
console.log(res)
await this.deleteFile(res.first[0].deleted_filename)
}
}

34
nfp_moe/api/serve.mjs Normal file
View File

@ -0,0 +1,34 @@
import Parent from '../base/serve.mjs'
export default class ServeHandler extends Parent {
async serveIndex(ctx) {
let payload = {
headerDescription: 'Small fansubbing and scanlation group translating and encoding our favourite shows from Japan.',
headerImage: this.frontend + '/assets/img/heart.png',
headerTitle: 'NFP Moe - Anime/Manga translation group',
headerUrl: this.frontend + ctx.url,
payloadData: null,
payloadTree: null,
version: this.version,
nonce: ctx.state.nonce,
}
try {
payload.payloadTree = JSON.stringify(await this.pageRoutes.getPageTree(ctx, true))
if (ctx.url === '/' || (ctx.url.startsWith('/page/') && ctx.url.lastIndexOf('/') === 5)) {
ctx.params.path = null
if (ctx.url.lastIndexOf('/') === 5) {
ctx.params.path = ctx.url.slice(ctx.url.lastIndexOf('/') + 1)
}
payload.payloadData = JSON.stringify(await this.pageRoutes.getPage(ctx, true))
}
console.log('url', ctx.url)
} catch (e) {
ctx.log.error(e)
}
ctx.body = this.template(payload)
ctx.type = 'text/html; charset=utf-8'
}
}

View File

@ -1,6 +1,6 @@
import config from '../base/config.mjs'
import Parent from '../base/server.mjs'
import ServeHandler from '../base/serve.mjs'
import ServeHandler from './serve.mjs'
import ArticleRoutes from './article_routes.mjs'
import PageRoutes from './page_routes.mjs'
@ -14,6 +14,7 @@ export default class Server extends Parent {
this.routes.page = new PageRoutes()
this.routes.serve = new ServeHandler({
pageRoutes: this.routes.page,
articleRoutes: this.routes.article,
root: localUtil.getPathFromRoot('../public'),
version: this.core.app.running,
frontend: config.get('frontend:url'),

View File

@ -24,6 +24,17 @@ const FileUpload = {
vnode.attrs.onfile(out)
},
fileRemoved: function(vnode) {
if (this.preview) {
this.preview.clear()
this.preview = null
vnode.attrs.onfile(null)
}
if (vnode.attrs.media) {
vnode.attrs.ondelete(vnode.attrs.media)
}
},
oninit: function(vnode) {
this.loading = false
this.preview = null
@ -66,10 +77,10 @@ const FileUpload = {
type: 'file',
onchange: this.fileChanged.bind(this, vnode),
}),
/*imageLink && vnode.attrs.ondelete
? m('button.remove', { onclick: vnode.attrs.ondelete })
imageLink && vnode.attrs.ondelete
? m('button.remove', { onclick: this.fileRemoved.bind(this, vnode), title: 'Remove image' }, m.trust('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.1.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M144 400C144 408.8 136.8 416 128 416C119.2 416 112 408.8 112 400V176C112 167.2 119.2 160 128 160C136.8 160 144 167.2 144 176V400zM240 400C240 408.8 232.8 416 224 416C215.2 416 208 408.8 208 400V176C208 167.2 215.2 160 224 160C232.8 160 240 167.2 240 176V400zM336 400C336 408.8 328.8 416 320 416C311.2 416 304 408.8 304 400V176C304 167.2 311.2 160 320 160C328.8 160 336 167.2 336 176V400zM310.1 22.56L336.9 64H432C440.8 64 448 71.16 448 80C448 88.84 440.8 96 432 96H416V432C416 476.2 380.2 512 336 512H112C67.82 512 32 476.2 32 432V96H16C7.164 96 0 88.84 0 80C0 71.16 7.164 64 16 64H111.1L137 22.56C145.8 8.526 161.2 0 177.7 0H270.3C286.8 0 302.2 8.526 310.1 22.56V22.56zM148.9 64H299.1L283.8 39.52C280.9 34.84 275.8 32 270.3 32H177.7C172.2 32 167.1 34.84 164.2 39.52L148.9 64zM64 432C64 458.5 85.49 480 112 480H336C362.5 480 384 458.5 384 432V96H64V432z"/></svg>'))
: null,
this.loading
/*this.loading
? m('div.loading-spinner')
: null,*/
])

View File

@ -108,6 +108,12 @@ const AdminArticles = {
? 'rowfeatured'
: ''
}, [
m('td.nopadding', article.banner_alt_prefix
? m('a', { href: article.banner_path, target: '_blank' }, m('img', { src: article.banner_alt_prefix + '_small.avif' }))
: m.trust('&nbsp;') ),
m('td.nopadding', article.media_alt_prefix
? m('a', { href: article.media_path, target: '_blank' }, m('img', { src: article.media_alt_prefix + '_small.avif' }))
: m.trust('&nbsp;') ),
m('td', m(m.route.Link, { href: '/admin/articles/' + article.id }, article.name)),
m('td', m(m.route.Link, { href: '/article/' + article.path }, 'View')),
m('td', m(m.route.Link, { href: article.page_path }, article.page_name)),
@ -138,6 +144,8 @@ const AdminArticles = {
: m('table', [
m('thead',
m('tr', [
m('th', 'Banner'),
m('th', 'Cover'),
m('th', 'Title'),
m('th', 'Path'),
m('th', 'Page'),

View File

@ -19,6 +19,8 @@ const EditArticle = {
this.pages = [{id: null, name: 'Frontpage'}]
this.pages = this.pages.concat(PageTree.getFlatTree())
this.removeBanner = false
this.removeMedia = false
this.newBanner = null
this.newMedia = null
this.dateInstance = null
@ -93,7 +95,13 @@ const EditArticle = {
if (name === 'path') {
this.editedPath = true
} else if (name === 'name' && !this.editedPath) {
this.data.article.path = this.data.article.name.toLowerCase().replace(/ /g, '-')
this.data.article.path = this.data.article.name
.normalize("NFD").replace(/[\u0300-\u036f]/g, '')
.toLocaleLowerCase()
.replace(/[^a-z0-9]/g, '-')
.replace(/\-{2,}/g, '-')
.replace(/^-+/, '')
.replace(/-+$/, '')
}
},
@ -108,14 +116,20 @@ const EditArticle = {
mediaUploaded: function(type, file) {
if (type === 'banner') {
this.newBanner = file
this.removeBanner = false
} else {
this.newMedia = file
this.removeMedia = false
}
},
mediaRemoved: function(type) {
this.data.article['remove_' + type] = true
this.data.article[type + '_prefix'] = null
this.data.article[type + '_alt_prefix'] = null
if (type === 'banner') {
this.removeBanner = true
} else {
this.removeMedia = true
}
},
save: function(vnode, e) {
@ -146,8 +160,8 @@ const EditArticle = {
formData.append('path', this.data.article.path)
formData.append('page_id', this.data.article.page_id || null)
formData.append('publish_at', this.dateInstance.inputElem.value.replace(', ', 'T') + 'Z')
formData.append('remove_banner', this.data.article.remove_banner ? true : false)
formData.append('remove_media', this.data.article.remove_media ? true : false)
formData.append('remove_banner', this.removeBanner ? true : false)
formData.append('remove_media', this.removeMedia ? true : false)
this.loading = true
@ -192,6 +206,9 @@ const EditArticle = {
},
refreshFiles: function(vnode, prom) {
this.loading = true
m.redraw()
prom.then(() => {
return api.sendRequest({
method: 'GET',
@ -204,21 +221,16 @@ const EditArticle = {
this.error = err.message
})
.then(() => {
this.loading = false
m.redraw()
})
},
confirmRemoveFile: function(vnode, file) {
console.log(file)
/*Dialogue.showDialogue(
'Delete file',
'Are you sure you want to remove "' + file.filename + '"',
'Delete',
'alert',
'Don\'t delete',
'',
page,
this.confirmRemovePage.bind(this, vnode))*/
return this.refreshFiles(vnode, api.sendRequest({
method: 'DELETE',
url: '/api/auth/articles/' + this.lastid + '/files/' + file.id,
}))
},
askConfirmRemoveFile: function(vnode, file) {

View File

@ -66,6 +66,12 @@ const AdminPages = {
drawPage: function(vnode, page) {
return [
m('tr', [
m('td.nopadding', page.banner_alt_prefix
? m('a', { href: page.banner_path, target: '_blank' }, m('img', { src: page.banner_alt_prefix + '_small.avif' }))
: m.trust('&nbsp;') ),
m('td.nopadding', page.media_alt_prefix
? m('a', { href: page.media_path, target: '_blank' }, m('img', { src: page.media_alt_prefix + '_small.avif' }))
: m.trust('&nbsp;') ),
m('td', [
page.parent_id ? m('span.subpage', ' - ') : null,
m(m.route.Link, { href: '/admin/pages/' + page.id }, page.name),
@ -97,6 +103,8 @@ const AdminPages = {
: m('table', [
m('thead',
m('tr', [
m('th', 'Banner'),
m('th', 'Cover'),
m('th', 'Title'),
m('th', 'Path'),
m('th.right', 'Updated'),

View File

@ -14,8 +14,8 @@ const Article = {
if (this.lastId !== article.id) {
this.lastId = article.id
let pictureCover = '(max-width: 639px) calc(100vw - 40px), '
+ '(max-width: 1000px) 300px, '
let pictureCover = '(max-width: 639px) calc(100vw - 10px), '
+ '(max-width: 1000px) calc(100vw - 265px), '
+ '400px'
if (vnode.attrs.full) {
pictureCover = '(max-width: 1280) calc(100vw - 2rem), '

View File

@ -9,29 +9,33 @@ const Footer = {
view: function() {
return [
m('span', 'Sitemap'),
m(m.route.Link, { class: 'root', href: '/' }, 'Home'),
PageTree.Tree.map(function(page) {
return [
m(m.route.Link, { class: 'root', href: '/page/' + page.path }, page.name),
(page.children
? m('ul', page.children.map(function(subpage) {
return m('li', m(m.route.Link, { class: 'child', href: '/page/' + subpage.path }, subpage.name))
}))
: null),
]
}),
!Authentication.currentUser
? m(m.route.Link, { class: 'root', href: '/login' }, 'Login')
: null,
m('div.meta', [
'©'
+ this.year
+ ' NFP Encodes - nfp@nfp.moe - ',
m('a', { rel: 'noopener', href: 'https://www.iubenda.com/privacy-policy/31076050', target: '_blank' }, 'Privacy Policy'),
' (Fuck EU)',
m('div.first'),
m('div.middle', [
m('span', 'Sitemap'),
m(m.route.Link, { class: 'root', href: '/' }, 'Home'),
PageTree.Tree.map(function(page) {
return [
m(m.route.Link, { class: 'root', href: '/page/' + page.path }, page.name),
(page.children
? m('ul', page.children.map(function(subpage) {
return m('li', m(m.route.Link, { class: 'child', href: '/page/' + subpage.path }, subpage.name))
}))
: null),
]
}),
!Authentication.currentUser
? m(m.route.Link, { class: 'root', href: '/login' }, 'Login')
: null,
m('div.meta', [
'©'
+ this.year
+ ' NFP Encodes - nfp@nfp.moe - ',
m('a', { rel: 'noopener', href: 'https://www.iubenda.com/privacy-policy/31076050', target: '_blank' }, 'Privacy Policy'),
' (Fuck EU)',
]),
]),
m('div.asuna.spritesheet'),
]
},
}

View File

@ -54,7 +54,7 @@ const Menu = {
} else {
localStorage.removeItem(DarkModeStorageName)
}
document.body.className = (this.darkIsOn ? 'darkmode ' : 'daymode ')
document.body.className = (this.darkIsOn ? 'nightmode ' : 'daymode ')
+ (window.supportsavif ? 'avifsupport' : 'jpegonly')
},
@ -63,8 +63,11 @@ const Menu = {
m('header', [
m('div.inside', [
m(m.route.Link,
{ href: '/', class: 'logo' },
m('h1', 'NFP Moe')
{ href: '/', class: 'title' },
[
m('div.logo.spritesheet'),
m('h1', 'NFP Moe'),
]
),
m('aside', [
Authentication.currentUser
@ -77,7 +80,7 @@ const Menu = {
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.rank < 100, href: '/admin/staff' }, 'Staff'),
// m(m.route.Link, { hidden: Authentication.currentUser.rank < 100, href: '/admin/staff' }, 'Staff'),
])
]
: null,

View File

@ -6,6 +6,7 @@ const Footer = require('./footer')
const Login = require('./site_login')
const SitePage = require('./site_page')
const SiteArticle = require('./site_article')
const NotFoundView = require('./site_404')
window.m = m
m.route.setOrig = m.route.set
@ -29,6 +30,7 @@ const allRoutes = {
'/article/:id': SiteArticle,
'/admin/:path': AdminResolver,
'/admin/:path/:id': AdminResolver,
'/:404...': NotFoundView,
}
// Wait until we finish checking avif support, some views render immediately and will ask for this immediately before the callback gets called.

View File

@ -20,7 +20,7 @@ const Paginator = {
),
}, 'Previous'),
]
: m('div'),
: [ m('div'), m('div')],
m('div', 'Page ' + currentPage),
currentPage < maxPage
? [
@ -31,7 +31,7 @@ const Paginator = {
href: vnode.attrs.base + '?page=' + maxPage,
}, 'Last')
]
: m('div'),
: [ m('div'), m('div')],
])
},
}

20
nfp_moe/app/site_404.js Normal file
View File

@ -0,0 +1,20 @@
const m = require('mithril')
const NotFoundView = {
oninit: function(vnode) {
},
oncreate: function() {
},
view: function(vnode) {
return [
m('div.wrapper.not_found', [
m('h4.notfound', '404: Page, article or file was not found'),
m('div.asuna.spritesheet'),
]),
]
},
}
module.exports = NotFoundView

View File

@ -2,6 +2,7 @@ const m = require('mithril')
const Article = require('./article')
const api = require('./api')
const media = require('./media')
const NotFoundView = require('./site_404')
window.LoadComments = false
window.HyvorLoaded = false
@ -22,6 +23,7 @@ const SiteArticle = {
this.path = m.route.param('id')
this.data.article = window.__nfpdata
window.__nfpdata = null
this.afterData()
} else {
this.fetchArticle(vnode)
}
@ -58,10 +60,7 @@ const SiteArticle = {
})
.then((result) => {
this.data = result
if (!this.data.article) {
this.error = 'Article not found'
}
this.afterData()
}, (err) => {
this.error = err.message
})
@ -73,6 +72,12 @@ const SiteArticle = {
})
},
afterData: function() {
if (!this.data.article) {
this.error = 'Article not found'
}
},
view: function(vnode) {
let article = this.data.article
@ -80,13 +85,16 @@ const SiteArticle = {
this.loading
? m('div.loading-spinner')
: null,
!this.loading && this.error
!this.loading && this.error === 'Article not found'
? NotFoundView.view()
: null,
!this.loading && this.error && this.error !== 'Article not found'
? m('div.wrapper', m('div.error', {
onclick: () => {
this.error = ''
this.fetchPage(vnode)
},
}, 'Page error: ' + this.error + '. Click here to try again'))
}, 'Article error: ' + this.error + '. Click here to try again'))
: null,
(article
? m('.inside.vertical', [

View File

@ -59,13 +59,14 @@ const Login = {
return [
m('div.wrapper', [
this.loading ? m('div.loading-spinner') : null,
m('div.login--first'),
m('form.inside.login', {
hidden: this.loading,
onsubmit: this.loginuser.bind(this, vnode),
}, [
m('div.title', 'NFP.moe login'),
this.error ? m('div.error', this.error) : null,
m('label', 'Email'),
m('label', 'Email or name'),
m('input', {
type: 'text',
value: this.username,
@ -82,6 +83,7 @@ const Login = {
value: 'Log in',
}),
]),
m('div.login--asuna.spritesheet'),
]),
]
},

View File

@ -7,6 +7,7 @@ const Article = require('./article')
const Articleslim = require('./article_slim')
const media = require('./media')
const EditorBlock = require('./editorblock')
const NotFoundView = require('./site_404')
const ArticlesPerPage = 10
@ -25,12 +26,20 @@ const SitePage = {
this.children = []
this.currentPage = Number(m.route.param('page')) || 1
console.log('test', window.__nfpdata)
if (window.__nfpdata) {
this.path = m.route.param('id')
this.data = window.__nfpdata
this.lastpage = this.currentPage
this.data = window.__nfpdata
window.__nfpdata = null
window.__nfpsubdata = null
this.children = PageTree.Tree
if (this.path) {
this.children = PageTree.TreeMap.get(this.path)
this.children = this.children && this.children.children || []
}
this.afterData()
} else {
this.fetchPage(vnode)
}
@ -76,28 +85,7 @@ const SitePage = {
})
.then((result) => {
this.data = result
if (!this.data.page && this.path) {
this.error = 'Page not found'
return
}
let title = 'Page not found - NFP Moe - Anime/Manga translation group'
if (this.data.page) {
title = this.data.page.name + ' - NFP Moe'
} else if (!this.path) {
title = 'NFP Moe - Anime/Manga translation group'
}
this.picture = media.generatePictureSource(this.data.page,
'(max-width: 840px) calc(100vw - 82px), '
+ '758px')
if (this.lastpage !== 1) {
document.title = 'Page ' + this.lastpage + ' - ' + title
} else {
document.title = title
}
this.afterData()
}, (err) => {
this.error = err.message
})
@ -109,6 +97,30 @@ const SitePage = {
})
},
afterData: function() {
if (!this.data.page && this.path) {
this.error = 'Page not found'
return
}
let title = 'Page not found - NFP Moe - Anime/Manga translation group'
if (this.data.page) {
title = this.data.page.name + ' - NFP Moe'
} else if (!this.path) {
title = 'NFP Moe - Anime/Manga translation group'
}
this.picture = media.generatePictureSource(this.data.page,
'(max-width: 840px) calc(100vw - 82px), '
+ '758px')
if (this.lastpage !== 1) {
document.title = 'Page ' + this.lastpage + ' - ' + title
} else {
document.title = title
}
},
view: function(vnode) {
let page = this.data.page
let featuredBanner = media.getBannerImage(this.data.featured, '/article/')
@ -118,7 +130,10 @@ const SitePage = {
this.loading
? m('div.loading-spinner')
: null,
!this.loading && this.error
!this.loading && this.error === 'Page not found'
? NotFoundView.view()
: null,
!this.loading && this.error && this.error !== 'Page not found'
? m('div.wrapper', m('div.error', {
onclick: () => {
this.error = ''
@ -163,52 +178,57 @@ const SitePage = {
m('h2.title', page.name)
])
: null),
m('.inside', [
this.children.length
? m('aside', [
m('h5', page ? 'View ' + page.name + ':' : 'Categories'),
this.children.map((page) => {
return [
m(m.route.Link, { class: 'root', href: '/page/' + page.path }, page.name),
(page.children && page.children.length
? m('ul', page.children.map(function(subpage) {
return m('li', m(m.route.Link, { class: 'child', href: '/page/' + subpage.path }, subpage.name))
})</