├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── api ├── controllers │ ├── api │ │ └── fileController.js │ ├── client │ │ ├── entryController.js │ │ └── fileController.js │ └── loginController.js └── policies │ ├── isLocal.js │ ├── isLoggedIn.js │ └── isLoggedInOrLocal.js ├── app.js ├── client ├── app.html ├── layouts │ ├── default.vue │ └── login.vue ├── middleware │ ├── noLogin.js │ └── requiresPublicationKey.js ├── pages │ ├── login │ │ └── index.vue │ └── publications │ │ ├── _publication_key │ │ ├── accessKeys │ │ │ ├── _accessKey_key │ │ │ │ └── edit.vue │ │ │ ├── create.vue │ │ │ └── index.vue │ │ ├── contentTypes │ │ │ ├── _contentType_key │ │ │ │ ├── edit.vue │ │ │ │ └── entries │ │ │ │ │ ├── _entry_key │ │ │ │ │ └── edit.vue │ │ │ │ │ ├── create.vue │ │ │ │ │ └── index.vue │ │ │ ├── create.vue │ │ │ └── index.vue │ │ ├── edit.vue │ │ └── files │ │ │ ├── _file_key │ │ │ └── edit.vue │ │ │ ├── create.vue │ │ │ └── index.vue │ │ ├── create.vue │ │ └── index.vue ├── plugins │ ├── fontAwesomeSolid.js │ ├── graphClient.js │ └── startup.js ├── store │ ├── editors │ │ ├── accessKey.js │ │ ├── contentType.js │ │ ├── entry.js │ │ └── publication.js │ ├── index.js │ └── settings.js ├── ui │ ├── components │ │ ├── design │ │ │ ├── designBooleanField.vue │ │ │ ├── designCodeField.vue │ │ │ ├── designColumnLayout.vue │ │ │ ├── designDateField.vue │ │ │ ├── designFileField.vue │ │ │ ├── designGroupLayout.vue │ │ │ ├── designLinkField.vue │ │ │ ├── designMultiLineTextbox.vue │ │ │ ├── designReferenceField.vue │ │ │ ├── designRichTextEditor.vue │ │ │ ├── designSelectField.vue │ │ │ └── designSingleLineTextbox.vue │ │ ├── edit │ │ │ ├── archive │ │ │ │ ├── editReferenceMultiField.vue │ │ │ │ └── editReferenceSingleField.vue │ │ │ ├── editBooleanField.vue │ │ │ ├── editCodeField.vue │ │ │ ├── editColumnLayout.vue │ │ │ ├── editDateField.vue │ │ │ ├── editFileField.vue │ │ │ ├── editGroupLayout.vue │ │ │ ├── editLinkField.vue │ │ │ ├── editMultiLineTextbox.vue │ │ │ ├── editReferenceField.vue │ │ │ ├── editRichTextEditor.vue │ │ │ ├── editSelectField.vue │ │ │ └── editSingleLineTextbox.vue │ │ └── view │ │ │ └── viewSingleLineTextbox.vue │ ├── dialogs │ │ └── filesDialog.vue │ ├── editors │ │ ├── linkDialog.vue │ │ └── richtext.vue │ ├── fieldComponents │ │ ├── fieldsEditor.vue │ │ ├── fieldsList.vue │ │ ├── optionEditors │ │ │ ├── archive │ │ │ │ ├── contentTypesOption.vue │ │ │ │ ├── genericOptions.vue │ │ │ │ ├── groupOptions.vue │ │ │ │ ├── selectOptions.vue │ │ │ │ └── textOptions.vue │ │ │ ├── choicesOption.vue │ │ │ ├── contentTypesOption.vue │ │ │ ├── groupModeOption.vue │ │ │ ├── hintOption.vue │ │ │ ├── imageSizeOption.vue │ │ │ ├── requiredOption.vue │ │ │ └── selectModeOption.vue │ │ └── optionsEditor.vue │ ├── filePreview.vue │ ├── forms │ │ ├── accessKeyForm.vue │ │ ├── blockForm.vue │ │ ├── componentForm.vue │ │ ├── contentTypeForm.vue │ │ ├── entryForm.vue │ │ ├── fileForm.vue │ │ └── publicationForm.vue │ ├── grids │ │ └── fileGrid.vue │ ├── imageColors.vue │ └── lists │ │ ├── accessKeyList.vue │ │ ├── contentTypeList.vue │ │ ├── entryList.vue │ │ ├── fileList.vue │ │ └── publicationList.vue └── vuetify.options.js ├── config ├── config.js ├── policies.js └── routes.js ├── example-config.js ├── graphql ├── index.js ├── schema │ ├── _scalers │ │ ├── dateTime.js │ │ └── json.js │ ├── accessKey.js │ ├── contentType.js │ ├── entry.js │ ├── file.js │ ├── publication.js │ └── user.js └── utils.js ├── jsconfig.json ├── lib ├── colorPalette.js ├── cycle.js ├── fieldTypes.js ├── format.js ├── mobileDocAtoms.js ├── mobileDocCards.js └── utils.js ├── nuxt.config.js ├── package.json ├── scripts ├── imageData.js └── imageResize.js ├── static ├── contentEditor.png ├── files.png └── typeEditor.png ├── system ├── middleware │ ├── nuxtRender.js │ └── session.js └── services │ ├── arango.js │ ├── arangofs.js │ ├── bcrypt.js │ ├── contentMigration.js │ ├── contentResolver.js │ ├── contentTypeValidator.js │ ├── ejs.js │ ├── images.js │ ├── init.js │ ├── mailer.js │ ├── nuxt.js │ ├── schema.js │ ├── sessionStorage.js │ ├── settings.js │ └── utils.js ├── users.js └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | // "browser": true, 4 | // "es6": true, 5 | // "node": true 6 | }, 7 | "extends": [ 8 | "plugin:vue/essential", 9 | "standard" 10 | ], 11 | "globals": { 12 | "Atomics": "readonly", 13 | "SharedArrayBuffer": "readonly" 14 | }, 15 | "parserOptions": { 16 | // "ecmaVersion": 2018, 17 | // "sourceType": "module" 18 | }, 19 | "plugins": [ 20 | "html", 21 | "import", 22 | "node", 23 | "promise", 24 | "standard", 25 | "vue" 26 | ], 27 | "rules": { 28 | "quotes": [1, "backtick"] 29 | }, 30 | "parser": "vue-eslint-parser", 31 | "parserOptions": { 32 | "parser": "babel-eslint", 33 | "sourceType": "module" 34 | } 35 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nuxt 2 | node_modules 3 | npm-debug.log 4 | logs 5 | fileStore 6 | cache 7 | config/env/dev.js 8 | config/env/prod.js 9 | deploy.sh 10 | private 11 | config.js -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PageFlo 2 | 3 | PageFlo is an open source headless CMS that gives you the flexibility to host almost any content you can imagine. 4 | 5 | ### Features 6 | 7 | - Handles multiple publications, apps or websites. 8 | - Create custom content types. 9 | - File storage. 10 | - Image resizing. 11 | - Video streaming. 12 | - Simple JSON api 13 | 14 | ### Run a dev server in minutes 15 | 16 | > note: It is assumed you are using nvm. 17 | 18 | #### 1. Get a DB server running. 19 | 20 | PageFlo uses [ArangoDB](https://www.arangodb.com/) as its datastore. There are many options for running it. 21 | 22 | - [Get a DB hosted for you.](https://cloud.arangodb.com/home) 23 | - [Run an enterprise edition.](https://www.arangodb.com/download-arangodb-enterprise/) 24 | - [Run the (awesome!) community version on your own computer.](https://www.arangodb.com/download-major/) 25 | 26 | #### 2. Get the code. 27 | 28 | ``` 29 | git clone https://github.com/internalfx/pageflo.git 30 | ``` 31 | 32 | #### 3. Get dependencies 33 | 34 | ``` 35 | nvm exec yarn 36 | ``` 37 | 38 | #### 4. Copy example config file 39 | 40 | ``` 41 | cp example-config.js config.js 42 | ``` 43 | 44 | #### 5. modify config to suite your needs 45 | 46 | ``` 47 | nano config.js 48 | ``` 49 | 50 | #### 6. start your dev server! 51 | 52 | ``` 53 | nvm run app.js --build 54 | ``` 55 | 56 | ## Poorly Made demonstration video! 57 | 58 | [Watch Now!](https://pageflo2.aam.site/api/client/file/download/1582317980692X1vcs7xx9) 59 | 60 | ## Screenshots 61 | 62 | ![Content Editor](https://github.com/internalfx/pageflo/raw/master/static/contentEditor.png) 63 | ![Files](https://github.com/internalfx/pageflo/raw/master/static/files.png) 64 | ![Type Editor](https://github.com/internalfx/pageflo/raw/master/static/typeEditor.png) -------------------------------------------------------------------------------- /api/controllers/client/entryController.js: -------------------------------------------------------------------------------- 1 | const substruct = require('@internalfx/substruct') 2 | const { arango, aql } = substruct.services.arango 3 | const { resolveEntry } = substruct.services.contentResolver 4 | const Promise = require('bluebird') 5 | // const _ = require('lodash') 6 | 7 | module.exports = { 8 | list: async function (ctx) { 9 | ctx.set('Access-Control-Allow-Origin', '*') 10 | ctx.set('Access-Control-Allow-Headers', '*') 11 | 12 | if (ctx.method === 'OPTIONS') { 13 | ctx.body = null 14 | return 15 | } 16 | 17 | const contentTypeSlug = ctx.state.params.content_type || null 18 | const apikey = ctx.headers.apikey || null 19 | 20 | const accessKey = await arango.qNext(aql` 21 | FOR item IN accessKeys 22 | FILTER item.apikey == ${apikey} 23 | RETURN item 24 | `) 25 | 26 | if (accessKey == null) { 27 | return ctx.throw(403) 28 | } 29 | 30 | const env = accessKey.environment 31 | const publicationKey = accessKey.publication_key 32 | 33 | const contentType = await arango.qNext(aql` 34 | FOR item IN contentTypes 35 | FILTER item.publication_key == ${publicationKey} 36 | FILTER item.slug == ${contentTypeSlug} 37 | RETURN item 38 | `) 39 | 40 | if (contentType == null) { 41 | return ctx.throw(400, 'Content Type not found') 42 | } 43 | 44 | let entryNumbers = [] 45 | 46 | entryNumbers = await arango.qAll(aql` 47 | FOR item IN entries 48 | FILTER item.contentType_key == ${contentType._key} 49 | SORT ${env === 'dev' ? 'item.createdAt' : 'item.publishDate'} DESC 50 | COLLECT num = item.number INTO entryGroup 51 | RETURN FIRST(entryGroup).item.number 52 | `) 53 | 54 | const entries = await Promise.map(entryNumbers, async function (entryNumber) { 55 | return resolveEntry(entryNumber, { env }) 56 | }) 57 | 58 | ctx.body = entries 59 | }, 60 | 61 | show: async function (ctx) { 62 | ctx.set('Access-Control-Allow-Origin', '*') 63 | ctx.set('Access-Control-Allow-Headers', '*') 64 | 65 | if (ctx.method === 'OPTIONS') { 66 | ctx.body = null 67 | return 68 | } 69 | 70 | const contentTypeSlug = ctx.state.params.content_type || null 71 | let entryNumber = isNaN(ctx.state.params.number) ? null : parseInt(ctx.state.params.number, 10) 72 | const apikey = ctx.headers.apikey || null 73 | 74 | if (entryNumber == null) { 75 | return ctx.throw(403, 'number is required') 76 | } 77 | 78 | const accessKey = await arango.qNext(aql` 79 | FOR item IN accessKeys 80 | FILTER item.apikey == ${apikey} 81 | RETURN item 82 | `) 83 | 84 | if (accessKey == null) { 85 | return ctx.throw(403) 86 | } 87 | 88 | const env = accessKey.environment 89 | const publicationKey = accessKey.publication_key 90 | 91 | const contentType = await arango.qNext(aql` 92 | FOR item IN contentTypes 93 | FILTER item.publication_key == ${publicationKey} 94 | FILTER item.slug == ${contentTypeSlug} 95 | RETURN item 96 | `) 97 | 98 | if (contentType == null) { 99 | return ctx.throw(400, 'Content Type not found') 100 | } 101 | 102 | entryNumber = await arango.qNext(aql` 103 | FOR entry IN entries 104 | FILTER entry.contentType_key == ${contentType._key} && entry.number == ${entryNumber} 105 | SORT ${env === 'dev' ? 'entry.createdAt' : 'entry.publishDate'} DESC 106 | LIMIT 1 107 | RETURN entry.number 108 | `) 109 | 110 | ctx.body = await resolveEntry(entryNumber, { env }) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /api/controllers/client/fileController.js: -------------------------------------------------------------------------------- 1 | const substruct = require('@internalfx/substruct') 2 | 3 | const { arango, aql } = substruct.services.arango 4 | const arangofs = substruct.services.arangofs 5 | const images = substruct.services.images 6 | 7 | const _ = require('lodash') 8 | const mime = require('mime-types') 9 | 10 | const defaultImageOpts = { 11 | width: null, 12 | height: null, 13 | sizing: 'cover', 14 | background: null, 15 | format: null, 16 | enlarge: true 17 | } 18 | 19 | module.exports = { 20 | download: async function (ctx) { 21 | const { filename } = ctx.state.params 22 | let imageOpts = { ...defaultImageOpts, ...ctx.state.params } 23 | imageOpts = _.pick(imageOpts, ['width', 'height', 'sizing', 'background', 'format', 'enlarge']) 24 | 25 | const returnOriginal = _.isEqual(imageOpts, defaultImageOpts) 26 | 27 | const file = await arango.qNext(aql` 28 | FOR file IN files 29 | FILTER file.filename == ${filename} 30 | RETURN file 31 | `) 32 | 33 | if (file == null) { 34 | ctx.throw(404) 35 | } 36 | 37 | ctx.set('Content-Type', file.mimeType) 38 | ctx.set('Cache-Control', 'max-age=3600') 39 | ctx.set('Content-Disposition', `inline; filename="${file.uploadedFilename}"`) 40 | 41 | // IF IMAGE 42 | if (file.mimeClass === 'image') { 43 | if (returnOriginal) { 44 | const gridFile = await arangofs.getFile({ filename: file.filename }) 45 | ctx.body = await arangofs.createReadStream({ _id: gridFile._id }) 46 | } else { 47 | imageOpts.width = imageOpts.width ? parseInt(imageOpts.width, 10) : null 48 | imageOpts.height = imageOpts.height ? parseInt(imageOpts.height, 10) : null 49 | 50 | if (imageOpts.format != null) { 51 | const newMime = mime.lookup(imageOpts.format) 52 | ctx.set('Content-Type', newMime) 53 | } 54 | 55 | ctx.body = await images.process({ ...imageOpts, file }) 56 | } 57 | // IF VIDEO 58 | } else if (['audio', 'video'].includes(file.mimeClass)) { 59 | ctx.set('Accept-Ranges', 'bytes') 60 | 61 | const gridFile = await arangofs.getFile({ filename: filename }) 62 | const range = ctx.header.range 63 | const size = gridFile.size 64 | const maxEnd = size - 1 65 | let contentLength = size 66 | let start = null 67 | let end = null 68 | 69 | if (range) { 70 | const parts = range.replace(/bytes=/, '').split('-') 71 | const partialstart = parts[0] 72 | const partialend = parts[1] 73 | 74 | start = parseInt(partialstart, 10) 75 | end = partialend ? parseInt(partialend, 10) : maxEnd 76 | if (end > maxEnd) { 77 | ctx.throw(416) 78 | } 79 | contentLength = (end - start) + 1 80 | 81 | ctx.status = 206 82 | ctx.set('Content-Range', `bytes ${start}-${end}/${size}`) 83 | } 84 | 85 | ctx.set('Content-Length', contentLength) 86 | 87 | ctx.body = await arangofs.createReadStream({ _id: gridFile._id, seekStart: start, seekEnd: end }) 88 | } else { 89 | const gridFile = await arangofs.getFile({ filename: filename }) 90 | ctx.body = await arangofs.createReadStream({ _id: gridFile._id }) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /api/controllers/loginController.js: -------------------------------------------------------------------------------- 1 | 2 | const substruct = require('@internalfx/substruct') 3 | const bcrypt = substruct.services.bcrypt 4 | const { arango, aql } = substruct.services.arango 5 | 6 | module.exports = { 7 | 8 | login: async function (ctx) { 9 | const { password, email } = ctx.request.body || {} 10 | 11 | if (email == null) { 12 | ctx.throw(400, 'Invalid login.') 13 | } 14 | 15 | const user = await arango.qNext(aql` 16 | FOR user IN users 17 | FILTER user.email == ${email.toLowerCase()} 18 | RETURN user 19 | `) 20 | 21 | if (user == null) { 22 | ctx.throw(400, 'Invalid password or email.') 23 | } 24 | 25 | if (password == null) { 26 | ctx.throw(400, 'Password is required.') 27 | } 28 | 29 | const check = await bcrypt.checkPassword(password, user.passwordHash) 30 | 31 | if (check.result !== true) { 32 | ctx.throw(400, 'Invalid password or email.') 33 | } 34 | 35 | const payload = { 36 | lastLoginAt: new Date() 37 | } 38 | 39 | if (check.newHash) { 40 | payload.passwordHash = check.newHash 41 | } 42 | 43 | await arango.q(aql` 44 | UPDATE { _key: ${user._key} } WITH ${payload} IN users 45 | `) 46 | 47 | ctx.state.session.userKey = user._key 48 | 49 | ctx.body = { token: ctx.state.token } 50 | }, 51 | 52 | logout: async function (ctx) { 53 | ctx.state.session = {} 54 | ctx.body = { 55 | success: true 56 | } 57 | }, 58 | 59 | user: async function (ctx) { 60 | const userKey = ctx.state.session.userKey 61 | 62 | if (userKey == null) { 63 | ctx.status = 200 64 | ctx.body = {} 65 | return 66 | } 67 | 68 | const user = await arango.qNext(aql` 69 | FOR user IN users 70 | FILTER user._key == ${userKey} 71 | RETURN user 72 | `) 73 | 74 | ctx.body = { user } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /api/policies/isLocal.js: -------------------------------------------------------------------------------- 1 | const ipaddr = require('ipaddr.js') 2 | const ip = require('ip') 3 | 4 | module.exports = async function (ctx) { 5 | const addr = ipaddr.process(ctx.request.ip).toString() 6 | 7 | if (ip.isLoopback(addr) === false) { 8 | ctx.throw(403) 9 | return 10 | } 11 | 12 | return true 13 | } 14 | -------------------------------------------------------------------------------- /api/policies/isLoggedIn.js: -------------------------------------------------------------------------------- 1 | module.exports = async function (ctx) { 2 | const session = ctx.state.session 3 | 4 | if (session.userKey == null) { // Check if user is logged in somehow 5 | ctx.throw(403) // Throw error if false 6 | return 7 | } 8 | 9 | return true // Return true to allow controller method to execute. 10 | } 11 | -------------------------------------------------------------------------------- /api/policies/isLoggedInOrLocal.js: -------------------------------------------------------------------------------- 1 | const ipaddr = require('ipaddr.js') 2 | const ip = require('ip') 3 | 4 | module.exports = async function (ctx) { 5 | const session = ctx.state.session 6 | const addr = ipaddr.process(ctx.request.ip).toString() 7 | 8 | if (session.userKey == null && ip.isLoopback(addr) === false) { 9 | ctx.throw(403) 10 | return 11 | } 12 | 13 | return true 14 | } 15 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | require(`@babel/register`)({ 2 | cwd: __dirname, 3 | plugins: [`@babel/plugin-transform-modules-commonjs`], 4 | only: [ 5 | `./lib/*` 6 | ] 7 | }) 8 | 9 | require(`./lib/cycle.js`) 10 | const substruct = require(`@internalfx/substruct`) 11 | const { ApolloServer, AuthenticationError, UserInputError } = require(`apollo-server-koa`) 12 | const { typeDefs, resolvers } = require(`./graphql/index.js`) 13 | const path = require(`path`) 14 | const numeral = require(`numeral`) 15 | 16 | const configPath = path.join(process.cwd(), `config.js`) 17 | const userConfig = require(configPath) 18 | 19 | substruct.configure({ 20 | ...userConfig, 21 | runDir: process.cwd(), 22 | appDir: __dirname 23 | }) 24 | 25 | const main = async function () { 26 | const apollo = new ApolloServer({ 27 | typeDefs, 28 | resolvers, 29 | formatError: function (error) { 30 | const data = JSON.decycle(error) 31 | console.log(`================================================================== GRAPHQL ERROR`) 32 | console.dir(data, { colors: true, depth: null }) 33 | console.log(`================================================================================`) 34 | return data 35 | }, 36 | context: async function ({ ctx }) { 37 | const session = ctx.state.session 38 | const { arango, aql, getNumber } = substruct.services.arango 39 | const afs = substruct.services.arangofs 40 | const utils = substruct.services.utils 41 | const config = substruct.config 42 | 43 | const user = await arango.qNext(aql` 44 | for u in users 45 | filter u._key == ${session.userKey || null} 46 | return u 47 | `) 48 | 49 | if (user == null) { 50 | throw new AuthenticationError(`You are not logged in`) 51 | } 52 | 53 | const userInputError = function (message, data) { 54 | throw new UserInputError(message, data) 55 | } 56 | 57 | return { 58 | session, 59 | arango, 60 | aql, 61 | getNumber, 62 | afs, 63 | utils, 64 | config, 65 | user, 66 | services: substruct.services, 67 | 68 | userInputError 69 | } 70 | } 71 | }) 72 | 73 | await substruct.load() 74 | await substruct.start() 75 | 76 | apollo.applyMiddleware({ app: substruct.koa, path: `/api/graphql` }) 77 | console.log(`Server Started...`) 78 | // setInterval(function () { 79 | // const stats = { 80 | // rss: numeral(process.memoryUsage().rss).format('0.00b'), 81 | // heapTotal: numeral(process.memoryUsage().heapTotal).format('0.00b'), 82 | // heapUsed: numeral(process.memoryUsage().heapUsed).format('0.00b'), 83 | // external: numeral(process.memoryUsage().external).format('0.00b') 84 | // } 85 | // console.log(stats) 86 | // }, 600000) 87 | } 88 | 89 | main().catch(function (err) { 90 | console.log(err) 91 | }) 92 | -------------------------------------------------------------------------------- /client/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ HEAD }} 5 | 6 | 7 | {{ APP }} 8 | 9 | 10 | -------------------------------------------------------------------------------- /client/layouts/login.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | 23 | -------------------------------------------------------------------------------- /client/middleware/noLogin.js: -------------------------------------------------------------------------------- 1 | 2 | export default function ({ app, redirect }) { 3 | if (app.$auth.loggedIn) { 4 | redirect('/') 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /client/middleware/requiresPublicationKey.js: -------------------------------------------------------------------------------- 1 | 2 | export default function ({ app, redirect, store }) { 3 | if (store.state.publication_key == null) { 4 | redirect('/publications') 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /client/pages/login/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 39 | 40 | 64 | 65 | 67 | -------------------------------------------------------------------------------- /client/pages/publications/_publication_key/accessKeys/_accessKey_key/edit.vue: -------------------------------------------------------------------------------- 1 | 2 | 50 | 51 | 62 | 63 | 65 | 66 | -------------------------------------------------------------------------------- /client/pages/publications/_publication_key/accessKeys/create.vue: -------------------------------------------------------------------------------- 1 | 2 | 53 | 54 | 65 | 66 | 68 | 69 | -------------------------------------------------------------------------------- /client/pages/publications/_publication_key/accessKeys/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 81 | 82 | 105 | 106 | 113 | 114 | -------------------------------------------------------------------------------- /client/pages/publications/_publication_key/contentTypes/_contentType_key/edit.vue: -------------------------------------------------------------------------------- 1 | 2 | 54 | 55 | 66 | 67 | 69 | -------------------------------------------------------------------------------- /client/pages/publications/_publication_key/contentTypes/_contentType_key/entries/_entry_key/edit.vue: -------------------------------------------------------------------------------- 1 | 2 | 70 | 71 | 86 | 87 | 89 | -------------------------------------------------------------------------------- /client/pages/publications/_publication_key/contentTypes/_contentType_key/entries/create.vue: -------------------------------------------------------------------------------- 1 | 2 | 70 | 71 | 86 | 87 | 89 | -------------------------------------------------------------------------------- /client/pages/publications/_publication_key/contentTypes/create.vue: -------------------------------------------------------------------------------- 1 | 2 | 54 | 55 | 66 | 67 | 69 | 70 | -------------------------------------------------------------------------------- /client/pages/publications/_publication_key/contentTypes/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 90 | 91 | 114 | 115 | 122 | 123 | -------------------------------------------------------------------------------- /client/pages/publications/_publication_key/edit.vue: -------------------------------------------------------------------------------- 1 | 2 | 51 | 52 | 63 | 64 | 66 | 67 | -------------------------------------------------------------------------------- /client/pages/publications/_publication_key/files/_file_key/edit.vue: -------------------------------------------------------------------------------- 1 | 2 | 25 | 26 | 35 | 36 | 38 | 39 | -------------------------------------------------------------------------------- /client/pages/publications/create.vue: -------------------------------------------------------------------------------- 1 | 2 | 50 | 51 | 62 | 63 | 65 | 66 | -------------------------------------------------------------------------------- /client/pages/publications/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 76 | 77 | 113 | 114 | 121 | 122 | -------------------------------------------------------------------------------- /client/plugins/fontAwesomeSolid.js: -------------------------------------------------------------------------------- 1 | 2 | import { library } from '@fortawesome/fontawesome-svg-core' 3 | 4 | import { 5 | faAlignCenter, 6 | faAlignJustify, 7 | faAlignLeft, 8 | faAlignRight, 9 | faBars, 10 | faBold, 11 | faCalendarAlt, 12 | faCamera, 13 | faCaretDown, 14 | faCheck, 15 | faCheckCircle, 16 | faCheckSquare, 17 | faChevronDown, 18 | faChevronLeft, 19 | faChevronRight, 20 | faChevronUp, 21 | faCircle, 22 | faCode, 23 | faColumns, 24 | faProjectDiagram, 25 | faDotCircle, 26 | faEnvelope, 27 | faExchangeAlt, 28 | faExclamationCircle, 29 | faExclamationTriangle, 30 | faExternalLinkAlt, 31 | faFile, 32 | faFileAlt, 33 | faFileArchive, 34 | faFileAudio, 35 | faFileCode, 36 | faFileCsv, 37 | faFileExcel, 38 | faFileImage, 39 | faFilePdf, 40 | faFileVideo, 41 | faFileWord, 42 | faGripVertical, 43 | faHeading, 44 | faImage, 45 | faInfoCircle, 46 | faItalic, 47 | faKey, 48 | faKeyboard, 49 | faLink, 50 | faListUl, 51 | faListOl, 52 | faMinus, 53 | faMinusCircle, 54 | faMinusSquare, 55 | faObjectGroup, 56 | faPaperPlane, 57 | faPencilAlt, 58 | faParagraph, 59 | faPhotoVideo, 60 | faPlus, 61 | faPrint, 62 | faQuestionCircle, 63 | faQuoteRight, 64 | faSearch, 65 | faSignature, 66 | faSignOutAlt, 67 | faSquare, 68 | faStrikethrough, 69 | faTasks, 70 | faThLarge, 71 | faThList, 72 | faThumbsUp, 73 | faTimes, 74 | faTimesCircle, 75 | faToggleOn, 76 | faTrashAlt, 77 | faUnderline, 78 | faUpload, 79 | faVideo 80 | } from '@fortawesome/free-solid-svg-icons' 81 | 82 | library.add( 83 | faAlignCenter, 84 | faAlignJustify, 85 | faAlignLeft, 86 | faAlignRight, 87 | faBars, 88 | faBold, 89 | faCalendarAlt, 90 | faCamera, 91 | faCaretDown, 92 | faCheck, 93 | faCheckCircle, 94 | faCheckSquare, 95 | faChevronDown, 96 | faChevronLeft, 97 | faChevronRight, 98 | faChevronUp, 99 | faCircle, 100 | faCode, 101 | faColumns, 102 | faProjectDiagram, 103 | faDotCircle, 104 | faEnvelope, 105 | faExchangeAlt, 106 | faExclamationCircle, 107 | faExclamationTriangle, 108 | faExternalLinkAlt, 109 | faFile, 110 | faFileAlt, 111 | faFileArchive, 112 | faFileAudio, 113 | faFileCode, 114 | faFileCsv, 115 | faFileExcel, 116 | faFileImage, 117 | faFilePdf, 118 | faFileVideo, 119 | faFileWord, 120 | faGripVertical, 121 | faHeading, 122 | faImage, 123 | faInfoCircle, 124 | faItalic, 125 | faKey, 126 | faKeyboard, 127 | faLink, 128 | faListUl, 129 | faListOl, 130 | faMinus, 131 | faMinusCircle, 132 | faMinusSquare, 133 | faObjectGroup, 134 | faPaperPlane, 135 | faPencilAlt, 136 | faParagraph, 137 | faPhotoVideo, 138 | faPlus, 139 | faPrint, 140 | faQuestionCircle, 141 | faQuoteRight, 142 | faSearch, 143 | faSignature, 144 | faSignOutAlt, 145 | faSquare, 146 | faStrikethrough, 147 | faTasks, 148 | faThLarge, 149 | faThList, 150 | faThumbsUp, 151 | faTimes, 152 | faTimesCircle, 153 | faToggleOn, 154 | faTrashAlt, 155 | faUnderline, 156 | faUpload, 157 | faVideo 158 | ) 159 | -------------------------------------------------------------------------------- /client/plugins/graphClient.js: -------------------------------------------------------------------------------- 1 | 2 | export default ({ app, env }, inject) => { 3 | inject('gqlClient', app.apolloProvider.defaultClient) 4 | } 5 | -------------------------------------------------------------------------------- /client/plugins/startup.js: -------------------------------------------------------------------------------- 1 | 2 | import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' 3 | import _ from 'lodash' 4 | import Vue from 'vue' 5 | import * as tagNames from 'mobiledoc-dom-renderer/dist/commonjs/mobiledoc-dom-renderer/utils/tag-names' 6 | import { VALID_ATTRIBUTES, VALID_MARKUP_TAGNAMES } from 'mobiledoc-kit/dist/commonjs/mobiledoc-kit/models/markup' 7 | 8 | Vue.component('font-awesome-icon', FontAwesomeIcon) 9 | Vue.prototype.get = _.get 10 | 11 | _.mixin({ isPresent: _.negate(_.isEmpty) }) 12 | 13 | export default ({ app, env, store }, inject) => { 14 | 15 | app.router.beforeEach((to, from, next) => { 16 | store.commit('set', { 17 | route: to 18 | }) 19 | next() 20 | }) 21 | } 22 | 23 | tagNames.isValidMarkerType = function () { 24 | return true 25 | } 26 | 27 | VALID_ATTRIBUTES.push('style') 28 | VALID_ATTRIBUTES.push('target') 29 | VALID_ATTRIBUTES.push('class') 30 | VALID_ATTRIBUTES.push('alt') 31 | 32 | VALID_MARKUP_TAGNAMES.push('small') 33 | VALID_MARKUP_TAGNAMES.push('div') 34 | -------------------------------------------------------------------------------- /client/store/editors/accessKey.js: -------------------------------------------------------------------------------- 1 | 2 | import { getField, updateField } from 'vuex-map-fields' 3 | import _ from 'lodash' 4 | import gql from 'graphql-tag' 5 | 6 | import { cleanMutation } from '../../../lib/utils.js' 7 | 8 | export const state = function () { 9 | return { 10 | accessKey: null 11 | } 12 | } 13 | 14 | export const mutations = { 15 | updateField, 16 | set: function (state, payload) { 17 | const newState = _.cloneDeep(state) 18 | Object.entries(payload).forEach(function ([key, val]) { 19 | _.set(newState, key, val) 20 | }) 21 | for (const [key, val] of Object.entries(newState)) { 22 | state[key] = val 23 | } 24 | } 25 | } 26 | 27 | export const actions = { 28 | newAccessKey: async function ({ state, rootState, commit, dispatch, getters, rootGetters }) { 29 | const newObj = { 30 | publication_key: rootState.route.params.publication_key, 31 | title: null, 32 | environment: 'prod', 33 | createdAt: null, 34 | updatedAt: null 35 | } 36 | 37 | commit('set', { 38 | accessKey: newObj 39 | }) 40 | }, 41 | loadAccessKey: async function ({ state, rootState, commit, dispatch, getters, rootGetters }, _key) { 42 | commit('set', { 43 | accessKey: null 44 | }) 45 | 46 | const res = await this.$gqlClient.query({ 47 | query: gql` 48 | query getAccessKey ($_key: ID) { 49 | record: getAccessKey (_key: $_key) { 50 | _key 51 | publication_key 52 | title 53 | environment 54 | apikey 55 | createdAt 56 | updatedAt 57 | } 58 | } 59 | `, 60 | variables: { 61 | _key 62 | }, 63 | fetchPolicy: 'network-only' 64 | }) 65 | 66 | const record = cleanMutation(_.get(res, 'data.record')) 67 | 68 | commit('set', { 69 | accessKey: record 70 | }) 71 | }, 72 | saveAccessKey: async function ({ state, rootState, commit, dispatch, getters, rootGetters }) { 73 | const res = await this.$gqlClient.mutate({ 74 | mutation: gql` 75 | mutation ($accessKey: AccessKeyInput!) { 76 | upsertAccessKey (accessKey: $accessKey) { 77 | _key 78 | publication_key 79 | title 80 | environment 81 | apikey 82 | createdAt 83 | updatedAt 84 | } 85 | } 86 | `, 87 | variables: { 88 | accessKey: _.pick(state.accessKey, '_key', 'publication_key', 'title', 'environment') 89 | }, 90 | refetchQueries: ['allAccessKeys'] 91 | }) 92 | 93 | const record = cleanMutation(_.get(res, 'data.upsertAccessKey')) 94 | 95 | commit('set', { 96 | accessKey: record 97 | }) 98 | } 99 | } 100 | 101 | export const getters = { 102 | getField 103 | } 104 | -------------------------------------------------------------------------------- /client/store/index.js: -------------------------------------------------------------------------------- 1 | 2 | import _ from 'lodash' 3 | import Promise from 'bluebird' 4 | import { getField, updateField } from 'vuex-map-fields' 5 | 6 | export const strict = true 7 | 8 | export const state = function () { 9 | return { 10 | publication_key: null, 11 | alert: { 12 | title: null, 13 | body: null, 14 | resolve: null 15 | }, 16 | auth: { 17 | user: null 18 | }, 19 | confirm: { 20 | title: null, 21 | body: null, 22 | resolve: null 23 | }, 24 | snackbar: { 25 | show: false, 26 | color: 'primary', 27 | message: null 28 | } 29 | } 30 | } 31 | 32 | export const mutations = { 33 | updateField, 34 | set: function (state, payload) { 35 | Object.entries(payload).forEach(function ([key, val]) { 36 | _.set(state, key, val) 37 | }) 38 | } 39 | } 40 | 41 | export const actions = { 42 | nuxtServerInit: async function ({ commit }, { app, env }) { 43 | // commit('set', { baseURL: env.baseURL }) 44 | }, 45 | 46 | showAlert: function ({ state, commit }, opts = {}) { 47 | let { title, body } = opts 48 | title = title || '' 49 | body = body || '' 50 | return new Promise(function (resolve) { 51 | commit('set', { 52 | 'alert.title': title, 53 | 'alert.body': body, 54 | 'alert.resolve': resolve 55 | }) 56 | }).then(function (choice) { 57 | commit('set', { 58 | 'alert.title': null, 59 | 'alert.body': null, 60 | 'alert.resolve': null 61 | }) 62 | 63 | return choice 64 | }) 65 | }, 66 | 67 | showConfirm: function ({ state, commit }, opts = {}) { 68 | let { title, body } = opts 69 | title = title || 'Are you sure?' 70 | body = body || '' 71 | return new Promise(function (resolve) { 72 | commit('set', { 73 | 'confirm.title': title, 74 | 'confirm.body': body, 75 | 'confirm.resolve': resolve 76 | }) 77 | }).then(function (choice) { 78 | commit('set', { 79 | 'confirm.title': null, 80 | 'confirm.body': null, 81 | 'confirm.resolve': null 82 | }) 83 | 84 | return choice 85 | }) 86 | }, 87 | 88 | showSnackbar: function ({ state, commit }, payload) { 89 | let message = '' 90 | let color = 'secondary' 91 | if (_.isString(payload)) { 92 | message = payload 93 | } else { 94 | message = payload.message 95 | color = payload.color 96 | } 97 | commit('set', { 98 | 'snackbar.show': true, 99 | 'snackbar.color': color, 100 | 'snackbar.message': message 101 | }) 102 | } 103 | } 104 | 105 | export const getters = { 106 | getField 107 | } 108 | -------------------------------------------------------------------------------- /client/store/settings.js: -------------------------------------------------------------------------------- 1 | 2 | import _ from 'lodash' 3 | import { getField, updateField } from 'vuex-map-fields' 4 | 5 | export const state = function () { 6 | return { 7 | accessKeys: { 8 | viewMode: 'list', 9 | page: 1, 10 | pageSize: 10, 11 | search: '' 12 | }, 13 | publications: { 14 | viewMode: 'list', 15 | page: 1, 16 | pageSize: 10, 17 | search: '' 18 | }, 19 | contentTypes: { 20 | viewMode: 'list', 21 | page: 1, 22 | pageSize: 10, 23 | search: '' 24 | }, 25 | entries: { 26 | viewMode: 'list', 27 | page: 1, 28 | pageSize: 10, 29 | search: '' 30 | }, 31 | files: { 32 | viewMode: 'list', 33 | page: 1, 34 | pageSize: 10, 35 | search: '', 36 | fileType: null 37 | }, 38 | filesDialog: { 39 | viewMode: 'list', 40 | page: 1, 41 | pageSize: 10, 42 | search: '', 43 | fileType: null 44 | } 45 | } 46 | } 47 | 48 | export const mutations = { 49 | updateField, 50 | set: function (state, payload) { 51 | Object.entries(payload).forEach(function ([key, val]) { 52 | _.set(state, key, val) 53 | }) 54 | } 55 | } 56 | 57 | export const actions = { 58 | } 59 | 60 | export const getters = { 61 | getField 62 | } 63 | -------------------------------------------------------------------------------- /client/ui/components/design/designBooleanField.vue: -------------------------------------------------------------------------------- 1 | 2 | 45 | 46 | 62 | 63 | 79 | -------------------------------------------------------------------------------- /client/ui/components/design/designCodeField.vue: -------------------------------------------------------------------------------- 1 | 2 | 45 | 46 | 63 | 64 | 85 | -------------------------------------------------------------------------------- /client/ui/components/design/designColumnLayout.vue: -------------------------------------------------------------------------------- 1 | 2 | 51 | 52 | 84 | 85 | 118 | -------------------------------------------------------------------------------- /client/ui/components/design/designDateField.vue: -------------------------------------------------------------------------------- 1 | 2 | 45 | 46 | 61 | 62 | 78 | -------------------------------------------------------------------------------- /client/ui/components/design/designFileField.vue: -------------------------------------------------------------------------------- 1 | 2 | 48 | 49 | 68 | 69 | 85 | -------------------------------------------------------------------------------- /client/ui/components/design/designLinkField.vue: -------------------------------------------------------------------------------- 1 | 2 | 45 | 46 | 73 | 74 | 90 | -------------------------------------------------------------------------------- /client/ui/components/design/designMultiLineTextbox.vue: -------------------------------------------------------------------------------- 1 | 2 | 45 | 46 | 61 | 62 | 78 | -------------------------------------------------------------------------------- /client/ui/components/design/designReferenceField.vue: -------------------------------------------------------------------------------- 1 | 2 | 45 | 46 | 63 | 64 | 80 | -------------------------------------------------------------------------------- /client/ui/components/design/designRichTextEditor.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 54 | 55 | 71 | -------------------------------------------------------------------------------- /client/ui/components/design/designSelectField.vue: -------------------------------------------------------------------------------- 1 | 2 | 45 | 46 | 61 | 62 | 78 | -------------------------------------------------------------------------------- /client/ui/components/design/designSingleLineTextbox.vue: -------------------------------------------------------------------------------- 1 | 2 | 45 | 46 | 61 | 62 | 78 | -------------------------------------------------------------------------------- /client/ui/components/edit/archive/editReferenceMultiField.vue: -------------------------------------------------------------------------------- 1 | 96 | 97 | 127 | 128 | 130 | -------------------------------------------------------------------------------- /client/ui/components/edit/archive/editReferenceSingleField.vue: -------------------------------------------------------------------------------- 1 | 96 | 97 | 126 | 127 | 129 | -------------------------------------------------------------------------------- /client/ui/components/edit/editBooleanField.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 40 | 41 | 43 | 44 | -------------------------------------------------------------------------------- /client/ui/components/edit/editCodeField.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 47 | 48 | 54 | 55 | -------------------------------------------------------------------------------- /client/ui/components/edit/editColumnLayout.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 70 | 71 | 100 | -------------------------------------------------------------------------------- /client/ui/components/edit/editDateField.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 76 | 77 | 79 | 80 | -------------------------------------------------------------------------------- /client/ui/components/edit/editFileField.vue: -------------------------------------------------------------------------------- 1 | 83 | 84 | 104 | 105 | 107 | -------------------------------------------------------------------------------- /client/ui/components/edit/editLinkField.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 80 | 81 | 88 | -------------------------------------------------------------------------------- /client/ui/components/edit/editMultiLineTextbox.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 45 | 46 | 48 | 49 | -------------------------------------------------------------------------------- /client/ui/components/edit/editRichTextEditor.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 45 | 46 | 54 | -------------------------------------------------------------------------------- /client/ui/components/edit/editSelectField.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 54 | 55 | 57 | -------------------------------------------------------------------------------- /client/ui/components/edit/editSingleLineTextbox.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 45 | 46 | 48 | 49 | -------------------------------------------------------------------------------- /client/ui/components/view/viewSingleLineTextbox.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 45 | 46 | 48 | 49 | -------------------------------------------------------------------------------- /client/ui/fieldComponents/optionEditors/archive/contentTypesOption.vue: -------------------------------------------------------------------------------- 1 | 2 | 109 | 110 | 128 | 129 | 137 | 138 | -------------------------------------------------------------------------------- /client/ui/fieldComponents/optionEditors/archive/genericOptions.vue: -------------------------------------------------------------------------------- 1 | 2 | 60 | 61 | 68 | 69 | 77 | 78 | -------------------------------------------------------------------------------- /client/ui/fieldComponents/optionEditors/archive/groupOptions.vue: -------------------------------------------------------------------------------- 1 | 2 | 58 | 59 | 67 | 68 | 76 | 77 | -------------------------------------------------------------------------------- /client/ui/fieldComponents/optionEditors/archive/textOptions.vue: -------------------------------------------------------------------------------- 1 | 2 | 44 | 45 | 57 | 58 | 62 | 63 | -------------------------------------------------------------------------------- /client/ui/fieldComponents/optionEditors/contentTypesOption.vue: -------------------------------------------------------------------------------- 1 | 2 | 88 | 89 | 101 | 102 | 104 | -------------------------------------------------------------------------------- /client/ui/fieldComponents/optionEditors/groupModeOption.vue: -------------------------------------------------------------------------------- 1 | 2 | 53 | 54 | 60 | 61 | 69 | -------------------------------------------------------------------------------- /client/ui/fieldComponents/optionEditors/hintOption.vue: -------------------------------------------------------------------------------- 1 | 2 | 49 | 50 | 58 | 59 | 61 | -------------------------------------------------------------------------------- /client/ui/fieldComponents/optionEditors/requiredOption.vue: -------------------------------------------------------------------------------- 1 | 2 | 49 | 50 | 53 | 54 | 56 | -------------------------------------------------------------------------------- /client/ui/fieldComponents/optionEditors/selectModeOption.vue: -------------------------------------------------------------------------------- 1 | 2 | 53 | 54 | 60 | 61 | 69 | -------------------------------------------------------------------------------- /client/ui/filePreview.vue: -------------------------------------------------------------------------------- 1 | 2 | 88 | 89 | 118 | 119 | 132 | -------------------------------------------------------------------------------- /client/ui/forms/accessKeyForm.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 54 | 55 | 57 | 58 | -------------------------------------------------------------------------------- /client/ui/forms/blockForm.vue: -------------------------------------------------------------------------------- 1 | 2 | 66 | 67 | 77 | 78 | 89 | -------------------------------------------------------------------------------- /client/ui/forms/componentForm.vue: -------------------------------------------------------------------------------- 1 | 2 | 52 | 53 | 83 | 84 | 95 | -------------------------------------------------------------------------------- /client/ui/forms/contentTypeForm.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 77 | 78 | 80 | 81 | -------------------------------------------------------------------------------- /client/ui/forms/entryForm.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 86 | 87 | 89 | -------------------------------------------------------------------------------- /client/ui/forms/publicationForm.vue: -------------------------------------------------------------------------------- 1 | 2 | 39 | 40 | 47 | 48 | 50 | -------------------------------------------------------------------------------- /client/ui/imageColors.vue: -------------------------------------------------------------------------------- 1 | 2 | 60 | 61 | 66 | 67 | 79 | -------------------------------------------------------------------------------- /client/ui/lists/accessKeyList.vue: -------------------------------------------------------------------------------- 1 | 2 | 67 | 68 | 107 | 108 | 110 | -------------------------------------------------------------------------------- /client/ui/lists/entryList.vue: -------------------------------------------------------------------------------- 1 | 2 | 65 | 66 | 101 | 102 | 104 | -------------------------------------------------------------------------------- /client/ui/lists/publicationList.vue: -------------------------------------------------------------------------------- 1 | 2 | 75 | 76 | 121 | 122 | 124 | -------------------------------------------------------------------------------- /client/vuetify.options.js: -------------------------------------------------------------------------------- 1 | 2 | import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' 3 | 4 | const fasIcon = function (name) { 5 | return { 6 | component: FontAwesomeIcon, 7 | props: { 8 | icon: ['fas', name] 9 | } 10 | } 11 | } 12 | 13 | const iconsDefault = { 14 | save: fasIcon('check'), 15 | cancel: fasIcon('times'), 16 | close: fasIcon('times'), 17 | 18 | complete: fasIcon('check'), 19 | create: fasIcon('plus'), 20 | edit: fasIcon('pencil-alt'), 21 | delete: fasIcon('trash-alt'), 22 | clear: fasIcon('times-circle'), 23 | 24 | search: fasIcon('search'), 25 | help: fasIcon('question-circle'), 26 | email: fasIcon('envelope'), 27 | send: fasIcon('paper-plane'), 28 | print: fasIcon('print'), 29 | picture: fasIcon('camera'), 30 | exchange: fasIcon('exchange-alt'), 31 | 32 | file: fasIcon('file'), 33 | fileAlt: fasIcon('file-alt'), 34 | fileArchive: fasIcon('file-archive'), 35 | fileAudio: fasIcon('file-audio'), 36 | fileCode: fasIcon('file-code'), 37 | fileCsv: fasIcon('file-csv'), 38 | fileExcel: fasIcon('file-excel'), 39 | fileImage: fasIcon('file-image'), 40 | filePdf: fasIcon('file-pdf'), 41 | fileVideo: fasIcon('file-video'), 42 | fileWord: fasIcon('file-word'), 43 | 44 | media: fasIcon('photo-video'), 45 | image: fasIcon('image'), 46 | video: fasIcon('video'), 47 | key: fasIcon('key'), 48 | 49 | // clear: '...', 50 | // success: '...', 51 | // info: '...', 52 | // warning: '...', 53 | // error: '...', 54 | 55 | radioOff: fasIcon('circle'), 56 | radioOn: fasIcon('dot-circle'), 57 | 58 | up: fasIcon('chevron-up'), 59 | down: fasIcon('chevron-down'), 60 | 61 | prev: fasIcon('chevron-left'), 62 | next: fasIcon('chevron-right'), 63 | 64 | checkboxOn: fasIcon('check-square'), 65 | checkboxOff: fasIcon('square'), 66 | 67 | expand: fasIcon('chevron-down'), 68 | menu: fasIcon('bars'), 69 | dropdown: fasIcon('caret-down'), 70 | 71 | yes: fasIcon('check'), 72 | no: fasIcon('times'), 73 | 74 | DateField: fasIcon('calendar-alt'), 75 | 76 | upload: fasIcon('upload'), 77 | 78 | list: fasIcon('th-list'), 79 | grid: fasIcon('th-large'), 80 | 81 | gripVertical: fasIcon('grip-vertical'), 82 | 83 | paragraph: fasIcon('paragraph'), 84 | heading: fasIcon('heading'), 85 | quoteRight: fasIcon('quote-right'), 86 | listUl: fasIcon('list-ul'), 87 | listOl: fasIcon('list-ol'), 88 | link: fasIcon('link'), 89 | bold: fasIcon('bold'), 90 | italic: fasIcon('italic'), 91 | strikethrough: fasIcon('strikethrough'), 92 | underline: fasIcon('underline'), 93 | code: fasIcon('code'), 94 | keyboard: fasIcon('keyboard'), 95 | alignLeft: fasIcon('align-left'), 96 | alignCenter: fasIcon('align-center'), 97 | alignRight: fasIcon('align-right'), 98 | alignJustify: fasIcon('align-justify'), 99 | 100 | externalLinkAlt: fasIcon('external-link-alt') 101 | } 102 | 103 | export default function ({ app, env }) { 104 | const config = { 105 | icons: { 106 | iconfont: 'faSvg', 107 | values: { 108 | ...iconsDefault, 109 | generic: { 110 | component: FontAwesomeIcon 111 | } 112 | } 113 | }, 114 | theme: { 115 | themes: { 116 | light: { 117 | primary: '#3F51B5', 118 | secondary: '#464646', 119 | accent: '#66B82C', 120 | success: '#66B82C' 121 | } 122 | }, 123 | options: { 124 | customProperties: true 125 | } 126 | } 127 | } 128 | 129 | return config 130 | } 131 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | 2 | // General Configuration 3 | // 4 | // options in this file are overidden by keys in environment specific files. e.g. dev.js or prod.js 5 | 6 | module.exports = { 7 | appName: 'PageFlo', 8 | middleware: [ 9 | 'performance', 10 | 'body', 11 | 'httpError', 12 | 'session', 13 | 'nuxtRender', 14 | 'router' 15 | ], 16 | koa: { 17 | proxy: false 18 | }, 19 | koaBody: { 20 | multipart: false 21 | }, 22 | services: [ 23 | 'init', 24 | 'utils', 25 | 'nuxt', 26 | 'arango', 27 | 'arangofs', 28 | 'schema', 29 | 'sessionStorage', 30 | // 'settings', 31 | 'bcrypt', 32 | // 'mailer', 33 | 'images', 34 | 'contentResolver', 35 | 'contentMigration', 36 | 'contentTypeValidator' 37 | ], 38 | session: { 39 | sessionCookieName: 'auth.pageflo.local', 40 | sessionCookieMaxAge: 1000 * 60 * 60 * 24 * 365 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config/policies.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | '*': 'isLoggedIn', 4 | 5 | loginController: { 6 | '*': true 7 | }, 8 | 9 | api: { 10 | '*': 'isLoggedIn', 11 | 12 | fileController: { 13 | download: 'isLoggedInOrLocal' 14 | } 15 | }, 16 | 17 | client: { 18 | '*': true 19 | }, 20 | 21 | preview: { 22 | '*': 'isLoggedInOrLocal' 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /config/routes.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | // Admin API 4 | 'post /api/auth/getLink': 'loginController.getLink', 5 | 'post /api/auth/login': 'loginController.login', 6 | 'post /api/auth/logout': 'loginController.logout', 7 | 'get /api/auth/user': 'loginController.user', 8 | 9 | 'get /api/file/download/:filename*': 'api.fileController.download', 10 | 'post /api/file/upload': 'api.fileController.upload', 11 | 12 | // Client API 13 | 'get /api/client/file/download/:filename': 'client.fileController.download', 14 | 'get /api/client/entry/list': 'client.entryController.list', 15 | 'options /api/client/entry/list': 'client.entryController.list', 16 | 'get /api/client/entry/show': 'client.entryController.show', 17 | 'options /api/client/entry/show': 'client.entryController.show' 18 | } 19 | -------------------------------------------------------------------------------- /example-config.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path') 3 | 4 | module.exports = { 5 | baseURL: 'http://localhost:8000', 6 | port: 8000, 7 | arango: { 8 | url: 'http://localhost:8529', 9 | database: 'pageflo', 10 | username: null, // Needed if auth is configured 11 | password: null // Needed if auth is configured 12 | }, 13 | arangoFS: { 14 | path: path.join(__dirname, 'fileStore') 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /graphql/index.js: -------------------------------------------------------------------------------- 1 | 2 | const { gql } = require('apollo-server-koa') 3 | const requireAll = require('require-all') 4 | const path = require('path') 5 | const _ = require('lodash') 6 | 7 | const keyPrefix = '=-_-=' 8 | 9 | const collapse = function (obj, depth) { 10 | const output = {} 11 | depth = depth || [] 12 | for (let [key, val] of Object.entries(obj)) { 13 | let hasPrefix = false 14 | if (key.includes(keyPrefix)) { 15 | key = key.replace(keyPrefix, '') 16 | hasPrefix = true 17 | } 18 | 19 | if (hasPrefix) { 20 | if (_.isFunction(val) || _.isString(val) || _.isArray(val) || _.isBoolean(val)) { 21 | Object.assign(output, { [depth.concat([key]).join('.')]: val }) 22 | } else if (_.isObject(val)) { 23 | Object.assign(output, collapse(val, depth.concat([key]))) 24 | } 25 | } else { 26 | Object.assign(output, { [depth.concat([key]).join('.')]: val }) 27 | } 28 | } 29 | return output 30 | } 31 | 32 | const libs = collapse(requireAll({ 33 | dirname: path.join(__dirname, 'schema'), 34 | filter: /(.+)\.js$/, 35 | recursive: true, 36 | map: function (name, path) { 37 | if (path.includes('.js')) { 38 | return name 39 | } else { 40 | return keyPrefix + name 41 | } 42 | } 43 | })) 44 | 45 | const typeDef = gql` 46 | type Query { 47 | me: String 48 | } 49 | 50 | type Mutation { 51 | me: String 52 | } 53 | ` 54 | 55 | const resolver = { 56 | Query: { 57 | } 58 | } 59 | 60 | const typeDefsList = [ 61 | typeDef, 62 | ...Object.values(libs).map(lib => lib.typeDefs) 63 | ] 64 | const resolverList = [ 65 | resolver, 66 | ...Object.values(libs).map(lib => lib.resolvers) 67 | ] 68 | 69 | const schema = { 70 | typeDefs: typeDefsList, 71 | resolvers: _.merge(...resolverList) 72 | } 73 | 74 | module.exports = schema 75 | -------------------------------------------------------------------------------- /graphql/schema/_scalers/dateTime.js: -------------------------------------------------------------------------------- 1 | 2 | let { GraphQLScalarType } = require('graphql') 3 | let { gql } = require('apollo-server-koa') 4 | let moment = require('moment') 5 | 6 | let typeDefs = gql` 7 | scalar DateTime 8 | ` 9 | 10 | let resolvers = { 11 | DateTime: new GraphQLScalarType({ 12 | name: 'DateTime', 13 | description: 'Date/Time value', 14 | parseValue: function (value) { 15 | let result = null 16 | 17 | if (value != null) { 18 | result = moment(value).toDate() 19 | } 20 | 21 | return result 22 | }, 23 | serialize: function (value) { 24 | let result = null 25 | 26 | if (value != null) { 27 | result = moment(value).toISOString() 28 | } 29 | 30 | return result 31 | }, 32 | parseLiteral: function (ast) { 33 | console.log(ast) 34 | // if (ast.kind === Kind.INT) { 35 | // return parseInt(ast.value, 10) 36 | // } 37 | return null 38 | } 39 | }) 40 | } 41 | 42 | module.exports = { 43 | typeDefs, 44 | resolvers 45 | } 46 | -------------------------------------------------------------------------------- /graphql/schema/_scalers/json.js: -------------------------------------------------------------------------------- 1 | 2 | let { GraphQLScalarType } = require('graphql') 3 | let { gql } = require('apollo-server-koa') 4 | let _ = require('lodash') 5 | 6 | let typeDefs = gql` 7 | scalar JSON 8 | ` 9 | 10 | let getAstValue = function (field) { 11 | let value = null 12 | 13 | if (field.kind === 'StringValue') { 14 | value = field.value 15 | } else if (field.kind === 'IntValue') { 16 | value = parseInt(field.value, 10) 17 | } else if (field.kind === 'FloatValue') { 18 | value = parseFloat(field.value) 19 | } else if (field.kind === 'ObjectValue') { 20 | value = {} 21 | for (let subField of field.fields) { 22 | value[subField.name.value] = getAstValue(subField) 23 | } 24 | } else if (field.kind === 'ObjectField') { 25 | value = getAstValue(field.value) 26 | } else if (field.kind === 'ListValue') { 27 | value = [] 28 | for (let subField of field.values) { 29 | value.push(getAstValue(subField)) 30 | } 31 | } 32 | 33 | return value 34 | } 35 | 36 | let resolvers = { 37 | JSON: new GraphQLScalarType({ 38 | name: 'JSON', 39 | description: 'JSON Data', 40 | parseValue: function (value) { 41 | let result = null 42 | 43 | if (value != null) { 44 | if (_.isPlainObject(value) || _.isArray(value)) { 45 | result = value 46 | } else if (_.isString(value)) { 47 | result = JSON.parse(value) 48 | } 49 | } 50 | 51 | return result 52 | }, 53 | serialize: function (value) { 54 | let result = null 55 | 56 | if (value != null) { 57 | if (_.isPlainObject(value) || _.isArray(value)) { 58 | result = value 59 | } 60 | } 61 | 62 | return result 63 | }, 64 | parseLiteral: function (ast) { 65 | let value = null 66 | 67 | if (ast.kind === 'ObjectValue') { 68 | value = getAstValue(ast) 69 | } 70 | 71 | return value 72 | } 73 | }) 74 | } 75 | 76 | module.exports = { 77 | typeDefs, 78 | resolvers 79 | } 80 | -------------------------------------------------------------------------------- /graphql/schema/accessKey.js: -------------------------------------------------------------------------------- 1 | 2 | // let _ = require('lodash') 3 | const { gql } = require('apollo-server-koa') 4 | // let { listSubFields } = require('../../utils.js') 5 | const { uniqueId } = require('../../lib/utils.js') 6 | 7 | const typeDefs = gql` 8 | type AccessKeyConnection { 9 | count: Int 10 | pageCount: Int 11 | items: [AccessKey] 12 | } 13 | 14 | type AccessKey { 15 | _key: ID! 16 | publication_key: ID! 17 | title: String 18 | environment: String 19 | apikey: String 20 | createdAt: DateTime 21 | updatedAt: DateTime 22 | } 23 | 24 | extend type Query { 25 | allAccessKeys ( 26 | publication_key: ID = null, 27 | page: Int = 1, 28 | pageSize: Int = 10, 29 | search: String = "" 30 | ): AccessKeyConnection 31 | getAccessKey (_key: ID): AccessKey 32 | } 33 | 34 | input AccessKeyInput { 35 | _key: ID 36 | publication_key: ID! 37 | title: String 38 | environment: String! 39 | } 40 | 41 | extend type Mutation { 42 | upsertAccessKey (accessKey: AccessKeyInput!): AccessKey 43 | destroyAccessKey (_key: ID!): AccessKey 44 | } 45 | ` 46 | 47 | const resolvers = { 48 | Query: { 49 | allAccessKeys: async function (obj, args, ctx, info) { 50 | const offset = args.pageSize * (args.page - 1) 51 | const search = `%${args.search}%` 52 | 53 | const { items, count } = await ctx.arango.qNext(ctx.aql` 54 | let items = ( 55 | FOR item IN accessKeys 56 | FILTER item.publication_key == ${args.publication_key} 57 | FILTER LIKE(item.title, ${search}, true) 58 | SORT item.title DESC 59 | RETURN item 60 | ) 61 | 62 | RETURN { 63 | items: SLICE(items, ${offset}, ${args.pageSize}), 64 | count: COUNT(items) 65 | } 66 | `) 67 | 68 | return { 69 | count, 70 | pageCount: Math.ceil(count / args.pageSize), 71 | items 72 | } 73 | }, 74 | getAccessKey: async function (obj, args, ctx, info) { 75 | if (args._key == null) { return } 76 | return ctx.arango.qNext(ctx.aql` 77 | RETURN DOCUMENT('accessKeys', ${args._key}) 78 | `) 79 | } 80 | }, 81 | Mutation: { 82 | upsertAccessKey: async function (obj, args, ctx, info) { 83 | let record = args.accessKey 84 | 85 | record.updatedAt = new Date() 86 | 87 | if (record._key == null) { 88 | record.apikey = uniqueId(40) 89 | record.createdAt = new Date() 90 | record = await ctx.arango.qNext(ctx.aql` 91 | INSERT ${record} INTO accessKeys RETURN NEW 92 | `) 93 | } else { 94 | record = await ctx.arango.qNext(ctx.aql` 95 | UPDATE ${record._key} WITH ${record} IN accessKeys RETURN NEW 96 | `) 97 | } 98 | 99 | return record 100 | }, 101 | destroyAccessKey: async function (obj, args, ctx, info) { 102 | await ctx.arango.qNext(ctx.aql` 103 | REMOVE { _key: ${args._key} } IN accessKeys RETURN OLD 104 | `) 105 | } 106 | }, 107 | AccessKey: { 108 | } 109 | } 110 | 111 | module.exports = { 112 | typeDefs, 113 | resolvers 114 | } 115 | -------------------------------------------------------------------------------- /graphql/schema/publication.js: -------------------------------------------------------------------------------- 1 | 2 | // let _ = require('lodash') 3 | const { gql } = require('apollo-server-koa') 4 | // let { updateTags } = require('../utils.js') 5 | // let { listSubFields } = require('../../utils.js') 6 | 7 | const typeDefs = gql` 8 | type PublicationConnection { 9 | count: Int 10 | pageCount: Int 11 | items: [Publication] 12 | } 13 | 14 | type Publication { 15 | _key: ID! 16 | title: String 17 | createdAt: DateTime 18 | updatedAt: DateTime 19 | } 20 | 21 | extend type Query { 22 | allPublications ( 23 | page: Int = 1, 24 | pageSize: Int = 10, 25 | search: String = "" 26 | ): PublicationConnection 27 | getPublication (_key: ID): Publication 28 | } 29 | 30 | input PublicationInput { 31 | _key: ID 32 | _id: ID 33 | title: String 34 | } 35 | 36 | extend type Mutation { 37 | upsertPublication (publication: PublicationInput!): Publication 38 | destroyPublication (_key: ID!): Publication 39 | } 40 | ` 41 | 42 | const resolvers = { 43 | Query: { 44 | allPublications: async function (obj, args, ctx, info) { 45 | const offset = args.pageSize * (args.page - 1) 46 | const search = `%${args.search}%` 47 | 48 | const { items, count } = await ctx.arango.qNext(ctx.aql` 49 | let items = ( 50 | FOR org IN publications 51 | FILTER LIKE(org.title, ${search}, true) 52 | SORT org.title DESC 53 | RETURN org 54 | ) 55 | 56 | RETURN { 57 | items: SLICE(items, ${offset}, ${args.pageSize}), 58 | count: COUNT(items) 59 | } 60 | `) 61 | 62 | return { 63 | count, 64 | pageCount: Math.ceil(count / args.pageSize), 65 | items 66 | } 67 | }, 68 | getPublication: async function (obj, args, ctx, info) { 69 | if (args._key == null) { return } 70 | return ctx.arango.qNext(ctx.aql` 71 | RETURN DOCUMENT('publications', ${args._key}) 72 | `) 73 | } 74 | }, 75 | Mutation: { 76 | upsertPublication: async function (obj, args, ctx, info) { 77 | let record = args.publication 78 | 79 | record.updatedAt = new Date() 80 | 81 | if (record._key == null) { 82 | record.createdAt = new Date() 83 | record = await ctx.arango.qNext(ctx.aql` 84 | INSERT ${record} INTO publications RETURN NEW 85 | `) 86 | } else { 87 | record = await ctx.arango.qNext(ctx.aql` 88 | UPDATE ${record._key} WITH ${record} IN publications RETURN NEW 89 | `) 90 | } 91 | 92 | // await updateTags(publication, ctx) 93 | 94 | return record 95 | }, 96 | destroyPublication: async function (obj, args, ctx, info) { 97 | await ctx.arango.qNext(ctx.aql` 98 | REMOVE { _key: ${args._key} } IN publications RETURN OLD 99 | `) 100 | } 101 | }, 102 | Publication: { 103 | } 104 | } 105 | 106 | module.exports = { 107 | typeDefs, 108 | resolvers 109 | } 110 | -------------------------------------------------------------------------------- /graphql/schema/user.js: -------------------------------------------------------------------------------- 1 | 2 | // let _ = require('lodash') 3 | const { gql } = require('apollo-server-koa') 4 | // let { listSubFields } = require('../../utils.js') 5 | 6 | const typeDefs = gql` 7 | type User { 8 | _id: ID! 9 | _key: ID! 10 | email: String 11 | settings: JSON 12 | created_at: DateTime 13 | updated_at: DateTime 14 | } 15 | 16 | extend type Query { 17 | allUsers (page: Int = 1): [User] 18 | userById (_id: ID!): User 19 | } 20 | 21 | extend type Mutation { 22 | updateMySettings (_id: ID!, settings: JSON!): User 23 | } 24 | ` 25 | 26 | const resolvers = { 27 | Query: { 28 | allUsers: async function (obj, args, ctx, info) { 29 | const cursor = await ctx.arango.query(ctx.aql` 30 | for x in user 31 | return x 32 | `) 33 | return cursor.all() 34 | }, 35 | userById: async function (obj, args, ctx, info) { 36 | const cursor = await ctx.arango.query(ctx.aql` 37 | for x in user 38 | FILTER x._id == ${args._id} 39 | return x 40 | `) 41 | return cursor.next() 42 | } 43 | }, 44 | Mutation: { 45 | updateMySettings: async function (obj, args, ctx, info) { 46 | const update = { 47 | settings: args.settings, 48 | updatedAt: new Date() 49 | } 50 | const cursor = await ctx.arango.query(ctx.aql` 51 | UPDATE ${ctx.user._key} WITH ${update} IN users RETURN NEW 52 | `) 53 | return cursor.next() 54 | } 55 | }, 56 | User: { 57 | } 58 | } 59 | 60 | module.exports = { 61 | typeDefs, 62 | resolvers 63 | } 64 | -------------------------------------------------------------------------------- /graphql/utils.js: -------------------------------------------------------------------------------- 1 | 2 | // let _ = require('lodash') 3 | // let Promise = require('bluebird') 4 | 5 | const updateTags = async function (obj, ctx) { 6 | const tags = obj.tags || [] 7 | 8 | const tagList = await ctx.arango.qAll(ctx.aql` 9 | FOR tag IN tags 10 | RETURN tag 11 | `) 12 | 13 | const missingTags = tags.filter(function (tag) { 14 | return tagList.find(t => t.title === tag) == null 15 | }) 16 | 17 | // console.log({ tagList, tags, missingTags }) 18 | 19 | if (missingTags.length > 0) { 20 | await ctx.arango.q(ctx.aql` 21 | FOR title IN ${missingTags} 22 | INSERT { title } INTO tags 23 | `) 24 | } 25 | 26 | await ctx.arango.q(ctx.aql` 27 | let removeList = ( 28 | FOR tag IN tags 29 | let hasPublication = COUNT( 30 | FOR item IN publications 31 | FILTER tag.title IN item.tags 32 | RETURN true 33 | ) > 0 34 | 35 | let hasContentType = COUNT( 36 | FOR item IN contentTypes 37 | FILTER tag.title IN item.tags 38 | RETURN true 39 | ) > 0 40 | 41 | let isUsed = hasPublication OR hasContentType 42 | FILTER isUsed == false 43 | 44 | RETURN tag 45 | ) 46 | 47 | FOR item IN removeList 48 | REMOVE item._key IN tags 49 | `) 50 | } 51 | 52 | module.exports = Object.freeze({ 53 | updateTags 54 | }) 55 | 56 | // let getNumber = async function (type) { 57 | // try { 58 | // let key = numberMap[type] 59 | 60 | // if (key == null) { 61 | // throw new Error('invalid type for getNumber') 62 | // } 63 | 64 | // let collections = { 65 | // exclusive: ['sys_settings'] 66 | // } 67 | 68 | // let action = String(function (params) { 69 | // let db = require('@arangodb').db 70 | // let aql = require('@arangodb').aql 71 | 72 | // let setting = db._query(aql` 73 | // FOR setting IN sys_settings 74 | // FILTER setting._key == ${params.key} 75 | // RETURN setting 76 | // `).toArray() 77 | 78 | // if (setting.length > 0) { 79 | // setting = setting[0] 80 | // } else { 81 | // setting = db._query(aql` 82 | // INSERT { _key: ${params.key}, value: 0 } IN sys_settings RETURN NEW 83 | // `).toArray()[0] 84 | // } 85 | 86 | // let value = setting.value 87 | // value += 1 88 | // setting.value += value 89 | 90 | // db._query(aql` 91 | // UPDATE ${setting} WITH { value: ${value} } IN sys_settings 92 | // `) 93 | 94 | // return value 95 | // }) 96 | 97 | // let params = { 98 | // key 99 | // } 100 | 101 | // let number = await arango.transaction( 102 | // collections, 103 | // action, 104 | // params, 105 | // { 106 | // // lockTimeout: 0 107 | // } 108 | // ) 109 | 110 | // return number 111 | // } catch (err) { 112 | // console.log(err) 113 | // // console.log('GETNUMBER WE CAUGHT ONE!!!!! ==============================================') 114 | // } 115 | // } 116 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "./.nuxt", 4 | "./node_modules" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /lib/colorPalette.js: -------------------------------------------------------------------------------- 1 | let chroma = require('chroma-js') 2 | 3 | let palette = [] 4 | 5 | for (let h of [0, 58, 90, 125, 165, 200, 250, 299, 320]) { 6 | palette.push(chroma.hcl(h, 65, 35)) 7 | palette.push(chroma.hcl(h, 82, 65)) 8 | palette.push(chroma.hcl(h, 100, 90)) 9 | palette.push(chroma.hcl(h, 50, 90)) 10 | } 11 | 12 | for (let l of [7, 35, 64, 93]) { 13 | palette.push(chroma.hcl(0, 0, l)) 14 | } 15 | 16 | module.exports = palette 17 | -------------------------------------------------------------------------------- /lib/fieldTypes.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | const types = [ 4 | 'BooleanField', 5 | 'CodeField', 6 | 'DateField', 7 | 'FileField', 8 | 'LinkField', 9 | 'MultiLineTextbox', 10 | 'ReferenceField', 11 | 'RichTextEditor', 12 | 'SelectField', 13 | 'SingleLineTextbox', 14 | 15 | 'ColumnLayout', 16 | 'GroupLayout' 17 | 18 | ] 19 | 20 | const data = {} 21 | 22 | const icons = { 23 | SingleLineTextbox: ['fas', 'minus'], 24 | MultiLineTextbox: ['fas', 'bars'], 25 | RichTextEditor: ['fas', 'paragraph'], 26 | SelectField: ['fas', 'tasks'], 27 | BooleanField: ['fas', 'toggle-on'], 28 | DateField: ['fas', 'calendar-alt'], 29 | CodeField: ['fas', 'code'], 30 | FileField: ['fas', 'photo-video'], 31 | LinkField: ['fas', 'link'], 32 | ReferenceField: ['fas', 'project-diagram'], 33 | 34 | ColumnLayout: ['fas', 'columns'], 35 | GroupLayout: ['fas', 'object-group'] 36 | } 37 | 38 | const defaults = function (type) { 39 | const data = { 40 | id: null, 41 | type, 42 | label: `New ${_.startCase(type)}`, 43 | slug: _.camelCase(`New ${_.startCase(type)}`), 44 | options: {} 45 | } 46 | 47 | if (type === 'GroupLayout') { 48 | data.options.groupMode = 'single' 49 | data.slug = _.camelCase(data.label) 50 | } else if (type === 'SelectField') { 51 | data.options.selectMode = 'single' 52 | } else if (type === 'ReferenceField') { 53 | data.options.selectMode = 'single' 54 | } else if (type === 'ColumnLayout') { 55 | data.label = null 56 | } 57 | 58 | return data 59 | } 60 | 61 | const group = function (type) { 62 | const layouts = [ 63 | 'ColumnLayout', 64 | 'GroupLayout' 65 | ] 66 | 67 | return layouts.includes(type) ? 'layout' : 'field' 68 | } 69 | 70 | const optionList = function (type) { 71 | const list = [] 72 | 73 | if ([ 74 | 'DateField', 75 | 'InspectionField', 76 | 'MultiLineTextbox', 77 | 'NumberField', 78 | 'SelectField', 79 | 'SingleLineTextbox' 80 | ].includes(type)) { 81 | // list.push('required') 82 | } 83 | 84 | if ([ 85 | 'SingleLineTextbox', 86 | 'MultiLineTextbox' 87 | ].includes(type)) { 88 | list.push('hint') 89 | } 90 | 91 | if ([ 92 | 'ReferenceField', 93 | 'SelectField' 94 | ].includes(type)) { 95 | list.push('selectMode') 96 | } 97 | 98 | if ([ 99 | 'ReferenceField' 100 | ].includes(type)) { 101 | list.push('contentTypes') 102 | } 103 | 104 | if ([ 105 | 'SelectField' 106 | ].includes(type)) { 107 | list.push('choices') 108 | } 109 | 110 | if ([ 111 | 'GroupLayout' 112 | ].includes(type)) { 113 | list.push('groupMode') 114 | } 115 | 116 | if ([ 117 | 'RichTextEditor' 118 | ].includes(type)) { 119 | list.push('imageSize') 120 | } 121 | 122 | return list 123 | } 124 | 125 | for (const type of types) { 126 | data[type] = { 127 | title: _.startCase(type), 128 | slug: type, 129 | icon: icons[type], 130 | group: group(type), 131 | defaults: defaults(type), 132 | optionList: optionList(type) 133 | } 134 | } 135 | 136 | export default data 137 | -------------------------------------------------------------------------------- /lib/format.js: -------------------------------------------------------------------------------- 1 | 2 | import numeral from 'numeral' 3 | import moment from 'moment' 4 | import _ from 'lodash' 5 | 6 | const formatters = { 7 | bytes: function (value) { 8 | if (value == null) { 9 | return '' 10 | } 11 | return numeral(value).format('0.0 b') 12 | }, 13 | date: function (value) { 14 | if (value == null) { 15 | return '' 16 | } 17 | return moment(value).format('ll') 18 | }, 19 | dateTime: function (value) { 20 | if (value == null) { 21 | return 'Never' 22 | } 23 | return moment(value).format('ll LT') 24 | }, 25 | dateTimeSeconds: function (value) { 26 | if (value == null) { 27 | return 'Never' 28 | } 29 | return moment(value).format('ll LTS') 30 | }, 31 | money: function (value) { 32 | if (value == null) { 33 | return '' 34 | } 35 | return numeral(value).format('$0,000.00') 36 | }, 37 | percent: function (value) { 38 | if (value == null) { 39 | return '' 40 | } 41 | return numeral(value).format('0%') 42 | }, 43 | truncate: function (value, length = 50) { 44 | if (value == null) { 45 | return '' 46 | } 47 | return _.truncate(value, { length: length }) 48 | }, 49 | capitalize: function (value) { 50 | return _.capitalize(value) 51 | }, 52 | dataDisplay: function (value, length = 50) { 53 | return _.truncate(JSON.stringify(value), { length: length }) 54 | } 55 | } 56 | 57 | export default function (...formatterList) { 58 | const payload = {} 59 | 60 | formatterList.forEach(function (name) { 61 | if (formatters[name]) { 62 | payload[name] = formatters[name] 63 | } 64 | }) 65 | 66 | return payload 67 | } 68 | 69 | // export default Object.freeze({ 70 | // f: { 71 | // bytes, 72 | // date, 73 | // dateTime, 74 | // dateTimeSeconds, 75 | // truncate, 76 | // capitalize, 77 | // dataDisplay, 78 | // friendlyCronTime 79 | // } 80 | // }) 81 | -------------------------------------------------------------------------------- /lib/mobileDocAtoms.js: -------------------------------------------------------------------------------- 1 | 2 | // const urljoin = require('url-join') 3 | 4 | export default [ 5 | ] 6 | -------------------------------------------------------------------------------- /lib/mobileDocCards.js: -------------------------------------------------------------------------------- 1 | 2 | const urljoin = require('url-join') 3 | const _ = require('lodash') 4 | 5 | export default function (config) { 6 | const width = config.width 7 | const height = config.height 8 | const sizing = config.sizing 9 | 10 | return [ 11 | { 12 | name: 'image', 13 | type: 'dom', 14 | render: function ({ env, options, payload }) { 15 | const inBrowser = (typeof document !== 'undefined') 16 | const filename = payload.filename 17 | const altText = payload.altText || '' 18 | const className = payload.className || '' 19 | const path = `${config.baseURL}/api/file/download/` 20 | 21 | const params = [] 22 | let query = '' 23 | 24 | if (inBrowser) { 25 | params.push(`r=${Date.now()}`) 26 | } 27 | 28 | if (Number.isFinite(width)) { 29 | params.push(`width=${width}`) 30 | } 31 | if (Number.isFinite(height)) { 32 | params.push(`height=${height}`) 33 | } 34 | if (!_.isEmpty(sizing)) { 35 | params.push(`sizing=${sizing}`) 36 | } 37 | 38 | if (params.length > 0) { 39 | query = `?${params.join('&')}` 40 | } 41 | 42 | const doc = inBrowser ? document : env.dom 43 | const img = doc.createElement('img') 44 | img.setAttribute('class', className) 45 | img.setAttribute('alt', altText) 46 | img.src = urljoin(path, filename, query) 47 | 48 | return img 49 | } 50 | }, 51 | { 52 | name: 'video', 53 | type: 'dom', 54 | render: function ({ env, options, payload }, dom) { 55 | const inBrowser = (typeof document !== 'undefined') 56 | const filename = payload.filename 57 | const className = payload.className || '' 58 | const path = `${config.baseURL}/api/file/download/` 59 | 60 | const params = [] 61 | let query = '' 62 | params.push(`r=${Date.now()}`) 63 | 64 | if (params.length > 0) { 65 | query = `?${params.join('&')}` 66 | } 67 | 68 | const doc = inBrowser ? document : env.dom 69 | const video = doc.createElement('video') 70 | video.setAttribute('class', className) 71 | video.setAttribute('controls', null) 72 | video.src = urljoin(path, filename, query) 73 | return video 74 | } 75 | } 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 2 | import _ from 'lodash' 3 | import crypto from 'crypto' 4 | 5 | const encode = function (val) { 6 | return encodeURIComponent(val) 7 | .replace(/%40/gi, '@') 8 | .replace(/%3A/gi, ':') 9 | .replace(/%24/g, '$') 10 | .replace(/%2C/gi, ',') 11 | .replace(/%20/g, '+') 12 | .replace(/%5B/gi, '[') 13 | .replace(/%5D/gi, ']') 14 | } 15 | 16 | export const to = function (promise) { 17 | return promise.then(function (val) { 18 | return val || {} 19 | }).catch(function (err) { 20 | err.isError = true 21 | return err 22 | }) 23 | } 24 | 25 | export const uniqueId = function (length = 10) { 26 | const chars = '1234567890BCDFGHJKMNPQRSTVWXYZ' 27 | const bytes = Array.from(crypto.randomBytes(length)) 28 | 29 | const value = bytes.map(function (byte, idx) { 30 | return chars[byte % chars.length].toString() 31 | }) 32 | 33 | return value.join('') 34 | } 35 | 36 | export const createId = function () { 37 | return `${Date.now()}X${uniqueId(8).toLowerCase()}` 38 | } 39 | 40 | export const errMsg = function (err) { 41 | if (process.env.isDevelopment) { 42 | console.log(JSON.stringify(err)) 43 | } 44 | 45 | const messagePaths = [ 46 | 'graphQLErrors[0].message', 47 | 'networkError.result.errors[0].message' 48 | ] 49 | let message = null 50 | 51 | for (const path of messagePaths) { 52 | message = _.get(err, path) 53 | 54 | if (message != null) { 55 | break 56 | } 57 | } 58 | 59 | return message 60 | } 61 | 62 | export const buildQueryString = function (params) { 63 | const parts = [] 64 | 65 | for (let [key, val] of Object.entries(params)) { 66 | if (val === null || typeof val === 'undefined') { 67 | continue 68 | } 69 | 70 | if (_.isArray(val)) { 71 | key = key + '[]' 72 | } else { 73 | val = [val] 74 | } 75 | 76 | for (let value of val) { 77 | if (_.isDate(value)) { 78 | value = value.toISOString() 79 | } else if (_.isObject(value)) { 80 | value = JSON.stringify(value) 81 | } 82 | 83 | parts.push(encode(key) + '=' + encode(value)) 84 | } 85 | } 86 | 87 | const serializedParams = parts.join('&') 88 | 89 | return serializedParams 90 | } 91 | 92 | export const cleanMutation = function (value) { 93 | if (value === null || value === undefined) { 94 | return value 95 | } else if (Array.isArray(value)) { 96 | return value.map(v => cleanMutation(v)) 97 | } else if (typeof value === 'object') { 98 | const newObj = {} 99 | Object.entries(value).forEach(([key, v]) => { 100 | if (key !== '__typename') { 101 | newObj[key] = cleanMutation(v) 102 | } 103 | }) 104 | return newObj 105 | } 106 | 107 | return value 108 | } 109 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path') 3 | 4 | module.exports = { 5 | apollo: { 6 | clientConfigs: { 7 | default: { 8 | httpLinkOptions: { 9 | credentials: 'same-origin' 10 | } 11 | } 12 | } 13 | }, 14 | auth: { 15 | strategies: { 16 | local: { 17 | endpoints: { 18 | login: { url: '/api/auth/login', method: 'post', propertyName: 'token' }, 19 | logout: { url: '/api/auth/logout', method: 'post' }, 20 | user: { url: '/api/auth/user', method: 'get', propertyName: 'user' } 21 | } 22 | } 23 | }, 24 | token: { 25 | prefix: 'pageflo.' 26 | }, 27 | cookie: { 28 | options: { 29 | maxAge: 60 * 60 * 24 * 300 30 | } 31 | }, 32 | localStorage: {}, 33 | rewriteRedirects: true, 34 | fullPathRedirect: true 35 | }, 36 | axios: {}, 37 | build: { 38 | babel: { 39 | presets: function ({ isServer }) { 40 | return [ 41 | [ 42 | require.resolve('@nuxt/babel-preset-app'), 43 | // require.resolve('@nuxt/babel-preset-app-edge'), // For nuxt-edge users 44 | { 45 | buildTarget: isServer ? 'server' : 'client', 46 | corejs: { version: 3 } 47 | } 48 | ] 49 | ] 50 | } 51 | }, 52 | extend: function (config, { isDev, isClient }) { 53 | if (isDev && isClient) { 54 | config.devtool = 'inline-source-map' 55 | } 56 | }, 57 | parallel: true, 58 | splitChunks: { 59 | layouts: false, 60 | pages: false, 61 | commons: false 62 | } 63 | }, 64 | css: [ 65 | ], 66 | buildModules: [ 67 | '@nuxtjs/vuetify' 68 | ], 69 | env: {}, 70 | head: { 71 | title: 'PageFlo', 72 | meta: [ 73 | { charset: 'utf-8' }, 74 | { name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui' }, 75 | { hid: 'description', name: 'description', content: 'PageFlo' } 76 | ], 77 | link: [ 78 | { rel: 'icon', type: 'image/x-icon', href: '/icon.png' } 79 | ] 80 | }, 81 | mode: 'spa', 82 | loading: { color: '#888888' }, 83 | modules: [ 84 | '@nuxtjs/apollo', 85 | '@nuxtjs/auth', 86 | '@nuxtjs/axios' 87 | ], 88 | plugins: [ 89 | 'plugins/fontAwesomeSolid.js', 90 | 'plugins/graphClient.js', 91 | 'plugins/startup.js' 92 | ], 93 | rootDir: path.join(__dirname), 94 | router: { 95 | middleware: [ 96 | 'auth' 97 | ], 98 | extendRoutes: function (routes, resolve) { 99 | routes.push({ 100 | path: '/', 101 | redirect: '/publications' 102 | }) 103 | } 104 | }, 105 | srcDir: path.join(__dirname, 'client'), 106 | vuetify: { 107 | defaultAssets: false, 108 | optionsPath: path.join(__dirname, 'client', 'vuetify.options.js') 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pageflo", 3 | "version": "0.1.0", 4 | "main": "app.js", 5 | "private": true, 6 | "homepage": "https://github.com/internalfx/pageflo", 7 | "author": "InternalFX inc.", 8 | "license": "Apache-2.0", 9 | "dependencies": { 10 | "@babel/core": "^7.10.2", 11 | "@babel/plugin-transform-modules-commonjs": "^7.10.1", 12 | "@babel/preset-env": "^7.10.2", 13 | "@babel/register": "^7.10.1", 14 | "@fortawesome/fontawesome-svg-core": "^1.2.28", 15 | "@fortawesome/free-solid-svg-icons": "^5.13.0", 16 | "@fortawesome/vue-fontawesome": "^0.1.10", 17 | "@internalfx/arangofs": "^0.1.6", 18 | "@internalfx/substruct": "^5.0.4", 19 | "@nuxtjs/apollo": "^4.0.1-rc.1", 20 | "@nuxtjs/auth": "^4.9.1", 21 | "@nuxtjs/vuetify": "^1.11.2", 22 | "apollo-cache-inmemory": "^1.6.6", 23 | "apollo-client": "^2.6.10", 24 | "apollo-link": "^1.2.14", 25 | "apollo-link-http": "^1.5.17", 26 | "apollo-server-koa": "^2.14.5", 27 | "arangojs": "^6.14.1", 28 | "axios": "^0.19.2", 29 | "bcryptjs": "^2.4.3", 30 | "bluebird": "^3.7.2", 31 | "chroma-js": "^2.1.0", 32 | "core-js": "3.6.5", 33 | "css-loader": "3.6.0", 34 | "del": "^5.1.0", 35 | "distinct-colors": "^3.0.0", 36 | "ejs": "^3.1.3", 37 | "graphql": "^15.1.0", 38 | "graphql-tag": "^2.10.3", 39 | "ifx-search": "0.0.5", 40 | "inquirer": "^7.2.0", 41 | "ip": "^1.1.5", 42 | "ipaddr.js": "^1.9.0", 43 | "jimp": "^0.13.0", 44 | "jsdom": "^16.2.2", 45 | "lockfile": "^1.0.4", 46 | "lodash": "^4.17.15", 47 | "marked": "^1.1.0", 48 | "mime-types": "^2.1.27", 49 | "mkdirp": "^1.0.4", 50 | "mobiledoc-dom-renderer": "^0.7.0", 51 | "mobiledoc-kit": "^0.12.4", 52 | "moment": "^2.26.0", 53 | "multiparty": "^4.2.1", 54 | "node-sass": "^4.14.1", 55 | "node-vibrant": "^3.2.0-alpha", 56 | "numeral": "^2.0.6", 57 | "nuxt": "2.12.2", 58 | "object-hash": "^2.0.3", 59 | "path-to-regexp": "^6.1.0", 60 | "promisify-child-process": "^3.1.4", 61 | "request": "^2.88.2", 62 | "request-promise": "^4.2.5", 63 | "require-all": "^3.0.0", 64 | "sanitize-html": "^1.26.0", 65 | "sharp": "^0.25.4", 66 | "simple-dom": "^1.4.0", 67 | "sortablejs": "^1.10.2", 68 | "stream-to-promise": "^2.2.0", 69 | "url-join": "^4.0.0", 70 | "validate.js": "^0.13.1", 71 | "vue": "2.6.10", 72 | "vue-loader": "^15.9.2", 73 | "vue-server-renderer": "2.6.10", 74 | "vue-template-compiler": "2.6.10", 75 | "vue-the-mask": "^0.11.1", 76 | "vuedraggable": "^2.23.1", 77 | "vuetify": "2.3.1", 78 | "vuex": "^3.4.0", 79 | "vuex-map-fields": "^1.4.0", 80 | "webpack": "^4.43.0" 81 | }, 82 | "devDependencies": { 83 | "babel-eslint": "^10.1.0", 84 | "eslint": "^7.2.0", 85 | "eslint-config-standard": "^14.1.1", 86 | "eslint-plugin-html": "^6.0.2", 87 | "eslint-plugin-import": "^2.21.2", 88 | "eslint-plugin-node": "^11.1.0", 89 | "eslint-plugin-promise": "^4.2.1", 90 | "eslint-plugin-standard": "^4.0.1", 91 | "eslint-plugin-vue": "^6.2.2" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /scripts/imageData.js: -------------------------------------------------------------------------------- 1 | const Promise = require('bluebird') 2 | const fs = Promise.promisifyAll(require('fs')) 3 | const sharp = require('sharp') 4 | const spec = JSON.parse(process.argv[2]) 5 | const Vibrant = require('node-vibrant') 6 | 7 | const { input } = spec 8 | 9 | const main = async function () { 10 | const pipeline = sharp(input) 11 | const imageData = await pipeline.metadata() 12 | const sampleBuffer = await pipeline.resize(200, null, { withoutEnlargement: true }).toBuffer() 13 | let colors = await Vibrant.from(sampleBuffer).getPalette() 14 | colors = Object.values(colors).reduce(function (acc, color) { 15 | if (color != null) { 16 | acc.push(color.getHex()) 17 | } 18 | return acc 19 | }, []) 20 | await fs.unlinkAsync(input) 21 | 22 | delete imageData.icc 23 | 24 | process.stdout.write(JSON.stringify({ ...imageData, colors: colors })) 25 | } 26 | 27 | Promise.resolve().then(main).catch(function (err) { 28 | console.log(err) 29 | }).finally(function () { 30 | process.exit() 31 | }) 32 | -------------------------------------------------------------------------------- /scripts/imageResize.js: -------------------------------------------------------------------------------- 1 | const Promise = require('bluebird') 2 | const fs = Promise.promisifyAll(require('fs')) 3 | const sharp = require('sharp') 4 | const _ = require('lodash') 5 | const spec = JSON.parse(process.argv[2]) 6 | 7 | let { input, output, width, height, background, sizing, format, enlarge } = spec 8 | 9 | const main = async function () { 10 | const buffer = await fs.readFileAsync(input) 11 | let pipeline = sharp(buffer) 12 | .rotate() 13 | .png({ force: false }) 14 | .jpeg({ 15 | quality: 80, 16 | trellisQuantisation: true, 17 | overshootDeringing: true, 18 | optimizeScans: true, 19 | force: false 20 | }) 21 | 22 | const imageData = await pipeline.metadata() 23 | 24 | if (_.isString(background)) { 25 | background = `#${background}` 26 | } else if (!_.isObject(background)) { 27 | if (imageData.hasAlpha) { 28 | background = { r: 255, g: 255, b: 255, alpha: 0 } 29 | } else { 30 | background = { r: 255, g: 255, b: 255, alpha: 1 } 31 | } 32 | } 33 | 34 | pipeline = pipeline.resize({ width, height, fit: sizing, position: sharp.strategy.entropy, background, withoutEnlargement: !enlarge }) 35 | 36 | if (format != null) { 37 | if (format === 'jpg') { 38 | pipeline = pipeline.flatten({ background }) 39 | pipeline = pipeline.toFormat('jpeg') 40 | } else if (format === 'png') { 41 | pipeline = pipeline.toFormat('png') 42 | } 43 | } 44 | 45 | await pipeline.toFile(input) 46 | await fs.renameAsync(input, output) 47 | } 48 | 49 | Promise.resolve().then(main).catch(function (err) { 50 | console.log(err) 51 | }).finally(function () { 52 | process.exit() 53 | }) 54 | -------------------------------------------------------------------------------- /static/contentEditor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/internalfx/pageflo/3819c30c075523d8e0552a8a4c40ff22182c4293/static/contentEditor.png -------------------------------------------------------------------------------- /static/files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/internalfx/pageflo/3819c30c075523d8e0552a8a4c40ff22182c4293/static/files.png -------------------------------------------------------------------------------- /static/typeEditor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/internalfx/pageflo/3819c30c075523d8e0552a8a4c40ff22182c4293/static/typeEditor.png -------------------------------------------------------------------------------- /system/middleware/nuxtRender.js: -------------------------------------------------------------------------------- 1 | 2 | const substruct = require(`@internalfx/substruct`) 3 | const { pathToRegexp } = require(`path-to-regexp`) 4 | 5 | module.exports = function (config) { 6 | const nuxt = substruct.services.nuxt 7 | const routeReg = pathToRegexp(`/api/:path*`) 8 | 9 | return async function (ctx, next) { 10 | if (routeReg.test(ctx.path) === false) { 11 | ctx.status = 200 12 | 13 | await new Promise((resolve, reject) => { 14 | ctx.res.on(`close`, resolve) 15 | ctx.res.on(`finish`, resolve) 16 | ctx.res.on(`error`, reject) 17 | nuxt.render(ctx.req, ctx.res) 18 | }) 19 | } 20 | 21 | await next() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /system/middleware/session.js: -------------------------------------------------------------------------------- 1 | 2 | const substruct = require(`@internalfx/substruct`) 3 | const hash = require(`object-hash`) 4 | const crypto = require(`crypto`) 5 | const _ = require(`lodash`) 6 | 7 | const createToken = function (length) { 8 | const chars = `1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz` 9 | const charLength = chars.length 10 | const bytes = Array.from(crypto.randomBytes(length)) 11 | 12 | const value = bytes.map(function (byte) { 13 | return chars[byte % charLength] 14 | }) 15 | 16 | return value.join(``) 17 | } 18 | 19 | module.exports = function (config) { 20 | const { arango, aql } = substruct.services.arango 21 | const cookieName = config.session.sessionCookieName 22 | 23 | const loadSession = async function (token) { 24 | let data = {} 25 | 26 | if (token) { 27 | data = null 28 | 29 | if (data == null) { 30 | const storedSession = await arango.qNext(aql` 31 | FOR x IN sys_sessions 32 | FILTER x._key == ${token} 33 | return x 34 | `) 35 | 36 | if (storedSession != null) { 37 | data = storedSession.data 38 | } 39 | } 40 | 41 | if (data == null) { 42 | data = {} 43 | } 44 | } 45 | 46 | return data 47 | } 48 | 49 | const saveSession = async function (token, data) { 50 | const obj = { 51 | _key: token, 52 | data 53 | } 54 | 55 | await arango.q(aql` 56 | UPSERT { _key: ${token} } INSERT ${obj} REPLACE ${_.omit(obj, `_key`)} IN sys_sessions 57 | `) 58 | } 59 | 60 | return async function (ctx, next) { 61 | let token 62 | 63 | const cookieToken = decodeURI(ctx.cookies.get(cookieName)) 64 | const headerToken = ctx.headers.authorization 65 | 66 | if (_.isString(cookieToken) && !_.isEmpty(cookieToken)) { 67 | token = cookieToken.replace(`Bearer `, ``) 68 | } 69 | 70 | if (_.isString(headerToken) && !_.isEmpty(headerToken)) { 71 | token = headerToken.replace(`Bearer `, ``) 72 | } 73 | 74 | if (token == null || token.length < 40) { 75 | token = createToken(40) 76 | } 77 | 78 | ctx.state.session = await loadSession(token) 79 | ctx.state.token = token 80 | 81 | const prevSession = hash(ctx.state.session) 82 | 83 | await next() 84 | 85 | const nextSession = hash(ctx.state.session) 86 | 87 | if (nextSession !== prevSession) { 88 | await saveSession(token, ctx.state.session) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /system/services/arango.js: -------------------------------------------------------------------------------- 1 | 2 | const arangojs = require(`arangojs`) 3 | const _ = require(`lodash`) 4 | 5 | module.exports = async function (config) { 6 | const arango = new arangojs.Database({ 7 | url: config.arango.url 8 | }) 9 | 10 | if (config.arango.database) { 11 | arango.useDatabase(config.arango.database) 12 | } 13 | 14 | if (config.arango.username && config.arango.password) { 15 | arango.useBasicAuth(config.arango.username, config.arango.password) 16 | } 17 | 18 | const numberMap = { 19 | entry: `entrySequence` 20 | } 21 | 22 | const getNumber = async function (type) { 23 | try { 24 | const key = numberMap[type] 25 | 26 | if (key == null) { 27 | throw new Error(`invalid type for getNumber`) 28 | } 29 | 30 | const collections = { 31 | exclusive: [`sys_settings`] 32 | } 33 | 34 | const action = String(function (params) { 35 | const db = require(`@arangodb`).db 36 | const aql = require(`@arangodb`).aql 37 | 38 | let setting = db._query(aql` 39 | FOR setting IN sys_settings 40 | FILTER setting._key == ${params.key} 41 | RETURN setting 42 | `).toArray() 43 | 44 | if (setting.length > 0) { 45 | setting = setting[0] 46 | } else { 47 | setting = db._query(aql` 48 | INSERT { _key: ${params.key}, value: 0 } IN sys_settings RETURN NEW 49 | `).toArray()[0] 50 | } 51 | 52 | let value = setting.value 53 | value += 1 54 | 55 | db._query(aql` 56 | UPDATE ${setting} WITH { value: ${value} } IN sys_settings 57 | `) 58 | 59 | return value 60 | }) 61 | 62 | const params = { 63 | key 64 | } 65 | 66 | const number = await arango.transaction( 67 | collections, 68 | action, 69 | params, 70 | { waitForSync: true } 71 | ) 72 | 73 | return number 74 | } catch (err) { 75 | console.log(err) 76 | // console.log('GETNUMBER WE CAUGHT ONE!!!!! ==============================================') 77 | } 78 | } 79 | 80 | const q = async function (...args) { 81 | let cursor = null 82 | let attempts = 0 83 | 84 | while (cursor == null) { 85 | attempts += 1 86 | try { 87 | cursor = await arango.query(...args) 88 | } catch (err) { 89 | if (err.errorNum !== 1200 || attempts >= 50) { 90 | console.log(_.get(args, `[0].query`)) 91 | throw err 92 | } 93 | } 94 | } 95 | 96 | return cursor 97 | } 98 | 99 | const qNext = async function (...args) { 100 | const cursor = await q(...args) 101 | return cursor.next() 102 | } 103 | 104 | const qAll = async function (...args) { 105 | const cursor = await q(...args) 106 | return cursor.all() 107 | } 108 | 109 | arango.q = q 110 | arango.qNext = qNext 111 | arango.qAll = qAll 112 | 113 | return { 114 | arango, 115 | aql: arangojs.aql, 116 | getNumber 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /system/services/arangofs.js: -------------------------------------------------------------------------------- 1 | const ArangoFS = require(`@internalfx/arangofs`) 2 | const Promise = require(`bluebird`) 3 | const fs = Promise.promisifyAll(require(`fs`)) 4 | 5 | module.exports = async function (config) { 6 | const storagePath = config.arangoFS.path 7 | 8 | await fs.mkdirAsync(storagePath, { recursive: true }) 9 | 10 | const arangoFS = ArangoFS(config.arango, { path: storagePath }) 11 | 12 | await arangoFS.initBucket() 13 | 14 | return arangoFS 15 | } 16 | -------------------------------------------------------------------------------- /system/services/bcrypt.js: -------------------------------------------------------------------------------- 1 | 2 | // const _ = require('lodash') 3 | // const substruct = require('@internalfx/substruct') 4 | const bcrypt = require(`bcryptjs`) 5 | const Promise = require(`bluebird`) 6 | 7 | module.exports = async function (config) { 8 | const targetRounds = 10 9 | const hashPassword = async function (password) { 10 | console.time(`hashPassword`) 11 | const salt = await Promise.fromCallback(function (cb) { bcrypt.genSalt(targetRounds, cb) }) 12 | const hash = await Promise.fromCallback(function (cb) { bcrypt.hash(password, salt, cb) }) 13 | console.timeEnd(`hashPassword`) 14 | return hash 15 | } 16 | 17 | const checkPassword = async function (password, hash) { 18 | console.time(`checkPassword`) 19 | let newHash = null 20 | const result = await bcrypt.compare(password, hash) 21 | 22 | if (result === true && bcrypt.getRounds(hash) !== targetRounds) { 23 | newHash = await hashPassword(password) 24 | } 25 | 26 | console.timeEnd(`checkPassword`) 27 | return { 28 | result, 29 | newHash 30 | } 31 | } 32 | 33 | return { 34 | hashPassword, 35 | checkPassword 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /system/services/contentTypeValidator.js: -------------------------------------------------------------------------------- 1 | // const substruct = require('@internalfx/substruct') 2 | const _ = require(`lodash`) 3 | // const Promise = require('bluebird') 4 | 5 | module.exports = async function (config) { 6 | // const { arango, aql } = substruct.services.arango 7 | 8 | const getConflicts = function (template) { 9 | const output = { 10 | paths: {}, 11 | fieldIds: [] 12 | } 13 | 14 | template.fields.forEach(function (field) { 15 | getConflictsField(field, output, `fields`) 16 | }) 17 | 18 | output.fieldIds = _.uniq(output.fieldIds) 19 | 20 | return output 21 | } 22 | 23 | const addField = function (field, output, path) { 24 | if (output.paths[path] == null) { 25 | output.paths[path] = [field.id] 26 | } else { 27 | output.paths[path].push(field.id) 28 | output.fieldIds = [...output.fieldIds, ...output.paths[path]] 29 | } 30 | } 31 | 32 | const getConflictsField = function (field, output, path) { 33 | if (field.type === `ColumnLayout`) { 34 | const fields = _.isArray(_.get(field, `options.columns`)) ? _.get(field, `options.columns`).flat() : [] 35 | 36 | fields.forEach(function (subField) { 37 | getConflictsField(subField, output, path) 38 | }) 39 | } else if (field.type === `GroupLayout`) { 40 | addField(field, output, `${path}.${field.slug}`) 41 | const fields = _.get(field, `options.fields`) || [] 42 | 43 | fields.forEach(function (subField) { 44 | getConflictsField(subField, output, `${path}.${field.slug}`) 45 | }) 46 | } else { 47 | addField(field, output, `${path}.${field.slug}`) 48 | } 49 | } 50 | 51 | return { 52 | getConflicts 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /system/services/ejs.js: -------------------------------------------------------------------------------- 1 | 2 | const ejs = require(`ejs`) 3 | const path = require(`path`) 4 | const _ = require(`lodash`) 5 | 6 | module.exports = async function (config) { 7 | const render = async function (templatePath, context) { 8 | const defaultCtx = { 9 | production: config.env === `production`, 10 | development: config.env === `development` 11 | } 12 | 13 | const locals = { _, ...defaultCtx, ...context } 14 | return ejs.renderFile(path.join(config.apiDir, `views`, templatePath), locals, { 15 | async: true, 16 | cache: config.env === `production` 17 | }) 18 | } 19 | 20 | return { 21 | render 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /system/services/images.js: -------------------------------------------------------------------------------- 1 | 2 | const substruct = require(`@internalfx/substruct`) 3 | const Promise = require(`bluebird`) 4 | const crypto = require(`crypto`) 5 | const path = require(`path`) 6 | const { exec } = require(`promisify-child-process`) 7 | const fs = Promise.promisifyAll(require(`fs`)) 8 | const lockFile = Promise.promisifyAll(require(`lockfile`)) 9 | const streamPromise = require(`stream-to-promise`) 10 | 11 | module.exports = async function (config) { 12 | const arangofs = substruct.services.arangofs 13 | 14 | // let spec = { 15 | // file, 16 | // width, 17 | // height, 18 | // background, 19 | // sizing, 20 | // format 21 | // } 22 | 23 | await fs.mkdirAsync(path.join(config.appDir, `cache`, `temp`), { recursive: true }) 24 | 25 | const fileExists = async function (filePath) { 26 | let exists = true 27 | 28 | try { 29 | await fs.accessAsync(filePath, fs.constants.F_OK) 30 | } catch (err) { 31 | exists = false 32 | } 33 | 34 | return exists 35 | } 36 | 37 | const process = async function (spec) { 38 | const { file, width, height, sizing, background, format, enlarge } = spec 39 | const ext = format != null ? format : file.ext 40 | // let gridFile = await arangofs.getFile({ filename: file.filename }) 41 | 42 | const cacheKey = `${file.sha256}:${width}:${height}:${sizing}:${background}:${format}:${enlarge}.${ext}` 43 | const cacheHash = crypto.createHash(`sha256`).update(cacheKey).digest(`hex`) 44 | 45 | const tempPath = path.join(config.appDir, `cache`, `temp`, `${cacheHash}.${ext}`) 46 | const lockPath = path.join(config.appDir, `cache`, `temp`, `${cacheHash}.lock`) 47 | const cachePath = path.join(config.appDir, `cache`, `${cacheHash}.${ext}`) 48 | 49 | if (await fileExists(cachePath)) { 50 | return fs.createReadStream(cachePath) 51 | } 52 | 53 | await lockFile.lockAsync(lockPath, { wait: 30000, stale: 15000 }) 54 | 55 | if (await fileExists(cachePath)) { 56 | await lockFile.unlockAsync(lockPath) 57 | return fs.createReadStream(cachePath) 58 | } 59 | 60 | const gridFile = await arangofs.getFile({ filename: file.filename }) 61 | const rStream = await arangofs.createReadStream({ _id: gridFile._id }) 62 | const wStream = fs.createWriteStream(tempPath) 63 | rStream.pipe(wStream) 64 | 65 | await streamPromise(wStream) 66 | 67 | const args = { 68 | input: tempPath, 69 | output: cachePath, 70 | width, 71 | height, 72 | background, 73 | sizing, 74 | format, 75 | enlarge 76 | } 77 | 78 | const res = await exec(`${config.nodePath} scripts/imageResize.js '${JSON.stringify(args)}'`) 79 | // console.log(res) 80 | if (res.stdout) { 81 | console.log(res.stdout) 82 | } 83 | if (res.stderr) { 84 | console.log(res.stderr) 85 | } 86 | 87 | await lockFile.unlockAsync(lockPath) 88 | return fs.createReadStream(cachePath) 89 | } 90 | 91 | return Object.freeze({ 92 | process 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /system/services/init.js: -------------------------------------------------------------------------------- 1 | const mkdirp = require(`mkdirp`) 2 | const path = require(`path`) 3 | 4 | module.exports = async function (config) { 5 | await mkdirp(path.join(config.appDir, `cache`)) 6 | 7 | if (process.env.NODE_PATH) { 8 | config.nodePath = process.env.NODE_PATH 9 | } else { 10 | config.nodePath = `node` 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /system/services/mailer.js: -------------------------------------------------------------------------------- 1 | const Promise = require(`bluebird`) 2 | const rp = require(`request-promise`) 3 | 4 | module.exports = function (config) { 5 | let mailer 6 | let send 7 | 8 | if (config.mailer.type === `direct`) { 9 | const directTransport = require(`nodemailer-direct-transport`) 10 | mailer = Promise.promisifyAll(require(`nodemailer`).createTransport(directTransport())) 11 | send = function (mail) { 12 | mail.from = config.mailer.fromEmail || `noreply@example.com` 13 | return mailer.sendMailAsync(mail) 14 | } 15 | } else if (config.mailer.type === `mailgun`) { 16 | send = function (mail) { 17 | return rp({ 18 | uri: `https://api:${config.mailer.api_key}@api.mailgun.net/v3/${config.mailer.domain}/messages`, 19 | method: `POST`, 20 | formData: { 21 | from: config.mailer.fromEmail, 22 | to: mail.to, 23 | subject: mail.subject, 24 | text: mail.text || ``, 25 | html: mail.html || ``, 26 | attachment: mail.attachment || [] 27 | } 28 | }) 29 | } 30 | } 31 | 32 | return Object.freeze({ 33 | send 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /system/services/nuxt.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = async function (config) { 3 | const { Nuxt, Builder } = require(`nuxt`) 4 | const nuxtConfig = require(`../../nuxt.config.js`) 5 | 6 | // Config Overrides 7 | nuxtConfig.dev = (config.env !== `production`) 8 | nuxtConfig.axios.baseURL = config.baseURL 9 | nuxtConfig.apollo.clientConfigs.default.httpEndpoint = `${config.baseURL}/api/graphql` 10 | nuxtConfig.env.baseURL = config.baseURL 11 | nuxtConfig.env.isDevelopment = (config.env === `development`) 12 | 13 | const nuxt = new Nuxt(nuxtConfig) 14 | 15 | await nuxt.ready() 16 | 17 | if (config.argv.build === true) { 18 | new Builder(nuxt).build() 19 | } 20 | 21 | return nuxt 22 | } 23 | -------------------------------------------------------------------------------- /system/services/schema.js: -------------------------------------------------------------------------------- 1 | const substruct = require(`@internalfx/substruct`) 2 | const Promise = require(`bluebird`) 3 | 4 | module.exports = async function (config) { 5 | const { arango, aql } = substruct.services.arango 6 | 7 | const collectionList = [ 8 | `accessKeys`, 9 | `contentTypes`, 10 | `entries`, 11 | `files`, 12 | `publications`, 13 | `users`, 14 | `sys_sessions`, 15 | `sys_settings` 16 | ] 17 | 18 | Promise.map(collectionList, async function (collectionName) { 19 | const collection = arango.collection(collectionName) 20 | const exists = await collection.exists() 21 | if (exists === false) { 22 | await collection.create() 23 | } 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /system/services/sessionStorage.js: -------------------------------------------------------------------------------- 1 | const substruct = require(`@internalfx/substruct`) 2 | const _ = require(`lodash`) 3 | // const lruCache = require('lru-cache') 4 | 5 | module.exports = async function (config) { 6 | const { arango, aql } = substruct.services.arango 7 | // let cache = lruCache({ 8 | // max: 10000 9 | // }) 10 | 11 | config.session.load = async function (token) { 12 | let data = {} 13 | 14 | if (token) { 15 | // data = cache.get(token) 16 | data = null 17 | 18 | if (data == null) { 19 | const storedSession = await arango.qNext(aql` 20 | FOR x IN sys_sessions 21 | FILTER x._key == ${token} 22 | return x 23 | `) 24 | 25 | if (storedSession != null) { 26 | data = storedSession.data 27 | // cache.set(token, data) 28 | } 29 | } 30 | 31 | if (data == null) { 32 | data = {} 33 | // cache.set(token, data) 34 | } 35 | } 36 | 37 | return data 38 | } 39 | 40 | config.session.save = async function (token, data) { 41 | // cache.set(token, data) 42 | 43 | const obj = { 44 | _key: token, 45 | data 46 | } 47 | 48 | await arango.query(aql` 49 | UPSERT ${_.pick(obj, `_key`)} INSERT ${obj} REPLACE ${_.omit(obj, `_key`)} IN sys_sessions 50 | `) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /system/services/settings.js: -------------------------------------------------------------------------------- 1 | 2 | const substruct = require(`@internalfx/substruct`) 3 | 4 | module.exports = async function (config) { 5 | const { arango, aql } = substruct.services.arango 6 | 7 | const get = async function (key) { 8 | const record = await arango.qNext(aql` 9 | RETURN DOCUMENT('sys_settings', ${key}) 10 | `) 11 | 12 | let val = null 13 | 14 | if (record) { 15 | if (record.type === `DateTime`) { 16 | val = new Date(record.value) 17 | } else { 18 | val = record.value 19 | } 20 | } 21 | 22 | return val 23 | } 24 | 25 | const set = async function (key, value) { 26 | const obj = { 27 | value, 28 | type: null 29 | } 30 | 31 | if (value instanceof Date) { 32 | obj.type = `DateTime` 33 | } 34 | 35 | await arango.q(aql` 36 | UPSERT { _key: ${key} } 37 | INSERT MERGE({ _key: ${key} }, ${obj}) 38 | UPDATE ${obj} 39 | IN sys_settings 40 | `) 41 | 42 | return true 43 | } 44 | 45 | const unset = async function (key) { 46 | await arango.q(aql` 47 | FOR x IN sys_settings 48 | FILTER x._key == ${key} 49 | REMOVE x IN sys_settings 50 | `) 51 | 52 | return true 53 | } 54 | 55 | return Object.freeze({ 56 | set, 57 | get, 58 | unset 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /system/services/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const crypto = require(`crypto`) 4 | const path = require(`path`) 5 | 6 | module.exports = function (config) { 7 | config.scriptsDir = path.join(config.appDir, `assets`, `scripts`) 8 | 9 | const to = function (promise) { 10 | return promise.then(function (val) { 11 | return val 12 | }).catch(function (err) { 13 | err.isError = true 14 | return err 15 | }) 16 | } 17 | 18 | const uniqueId = function (length) { 19 | const letters = `ABCDEFGHIJKLMNOPQRSTUVWXYZ` 20 | const numbers = `1234567890` 21 | const bytes = Array.from(crypto.randomBytes(length)) 22 | 23 | const value = bytes.map(function (byte, idx) { 24 | if (idx % 2) { 25 | return letters[byte % letters.length].toString() 26 | } else { 27 | return numbers[byte % numbers.length].toString() 28 | } 29 | }) 30 | 31 | return value.join(``) 32 | } 33 | 34 | return Object.freeze({ 35 | to, 36 | uniqueId 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /users.js: -------------------------------------------------------------------------------- 1 | require('@babel/register')({ 2 | cwd: __dirname, 3 | plugins: ['@babel/plugin-transform-modules-commonjs'], 4 | only: [ 5 | './lib/*' 6 | ] 7 | }) 8 | 9 | const path = require('path') 10 | const appDir = path.join(__dirname) 11 | 12 | const substruct = require('@internalfx/substruct') 13 | const inquirer = require('inquirer') 14 | const _ = require('lodash') 15 | 16 | const configPath = path.join(process.cwd(), 'config.js') 17 | const userConfig = require(configPath) 18 | 19 | substruct.configure({ 20 | build: false, 21 | runCron: false, 22 | runDir: process.cwd(), 23 | appDir, 24 | ...userConfig 25 | }) 26 | 27 | substruct.load().then(async function ({ koa, config }) { 28 | const { arango, aql } = substruct.services.arango 29 | const bcrypt = substruct.services.bcrypt 30 | 31 | const mainMenu = async function () { 32 | const answers = await inquirer.prompt([ 33 | { 34 | type: 'list', 35 | name: 'action', 36 | message: 'What would you like to do?', 37 | choices: [ 38 | { name: 'Create User', value: 'create' }, 39 | { name: 'Reset User Password', value: 'reset' }, 40 | { name: 'Exit', value: 'exit' } 41 | ] 42 | } 43 | ]) 44 | 45 | if (answers.action === 'create') { 46 | await createUser() 47 | await mainMenu() 48 | } else if (answers.action === 'reset') { 49 | await resetPassword() 50 | await mainMenu() 51 | } else if (answers.action === 'exit') { 52 | await substruct.stop() 53 | } 54 | } 55 | 56 | const createUser = async function () { 57 | const answers = await inquirer.prompt([ 58 | { 59 | type: 'email', 60 | name: 'email', 61 | message: 'Email?' 62 | }, 63 | { 64 | type: 'list', 65 | name: 'role', 66 | message: 'Role?', 67 | choices: [ 68 | { name: 'User', value: 'USR' }, 69 | { name: 'Administrator', value: 'ADM' } 70 | ] 71 | }, 72 | { 73 | type: 'password', 74 | name: 'password', 75 | message: 'Password?' 76 | } 77 | ]) 78 | 79 | let record = _.omit(answers, 'password') 80 | 81 | record.createdAt = new Date() 82 | record.updatedAt = new Date() 83 | 84 | record.passwordHash = await bcrypt.hashPassword(answers.password) 85 | 86 | record = await arango.qNext(aql` 87 | INSERT ${record} INTO users RETURN NEW 88 | `) 89 | 90 | console.log('User Created!') 91 | } 92 | 93 | const resetPassword = async function () { 94 | let users = await arango.qAll(aql` 95 | FOR user IN users 96 | RETURN user 97 | `) 98 | 99 | users = users.map(function (user) { 100 | return { 101 | name: `${user.email}`, 102 | value: user._key 103 | } 104 | }) 105 | 106 | const answers = await inquirer.prompt([ 107 | { 108 | type: 'list', 109 | name: 'user', 110 | message: 'User?', 111 | choices: users 112 | }, 113 | { 114 | type: 'password', 115 | name: 'password', 116 | message: 'Password?' 117 | } 118 | ]) 119 | 120 | const passwordHash = await bcrypt.hashPassword(answers.password) 121 | 122 | await arango.q(aql` 123 | UPDATE ${answers.user} WITH { passwordHash: ${passwordHash} } IN users 124 | `) 125 | 126 | console.log('Password Updated!') 127 | } 128 | 129 | return mainMenu() 130 | }).catch(function (err) { 131 | console.log(err) 132 | }) 133 | --------------------------------------------------------------------------------