├── .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 | 
63 | 
64 | 
--------------------------------------------------------------------------------
/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 |
11 |
12 |
13 | PageFlo
14 |
15 |
16 |
17 |
18 |
19 |
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 |
41 |
42 |
43 |
44 | Login
45 |
46 |
47 |
48 |
49 |
50 | {{error}}
51 |
52 |
53 |
54 |
55 |
56 |
57 | Login
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
67 |
--------------------------------------------------------------------------------
/client/pages/publications/_publication_key/accessKeys/_accessKey_key/edit.vue:
--------------------------------------------------------------------------------
1 |
2 |
50 |
51 |
52 |
53 | Edit Content Type
54 |
55 |
56 |
57 |
58 | $save Save
59 |
60 |
61 |
62 |
63 |
65 |
66 |
--------------------------------------------------------------------------------
/client/pages/publications/_publication_key/accessKeys/create.vue:
--------------------------------------------------------------------------------
1 |
2 |
53 |
54 |
55 |
56 | Create Access Key
57 |
58 |
59 |
60 |
61 | $save Save
62 |
63 |
64 |
65 |
66 |
68 |
69 |
--------------------------------------------------------------------------------
/client/pages/publications/_publication_key/accessKeys/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
81 |
82 |
83 |
84 |
85 | Access Keys
86 |
87 | $plus Add New
88 |
89 |
90 |
91 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
113 |
114 |
--------------------------------------------------------------------------------
/client/pages/publications/_publication_key/contentTypes/_contentType_key/edit.vue:
--------------------------------------------------------------------------------
1 |
2 |
54 |
55 |
56 |
57 | Edit Content Type
58 |
59 |
60 |
61 |
62 | $save Save
63 |
64 |
65 |
66 |
67 |
69 |
--------------------------------------------------------------------------------
/client/pages/publications/_publication_key/contentTypes/_contentType_key/entries/_entry_key/edit.vue:
--------------------------------------------------------------------------------
1 |
2 |
70 |
71 |
72 |
73 | Edit {{contentType.title}}
74 | Edit {{contentType.title}} Entry
75 |
76 |
77 |
78 |
79 | $save Save
80 |
81 |
82 | $send Publish
83 |
84 |
85 |
86 |
87 |
89 |
--------------------------------------------------------------------------------
/client/pages/publications/_publication_key/contentTypes/_contentType_key/entries/create.vue:
--------------------------------------------------------------------------------
1 |
2 |
70 |
71 |
72 |
73 | Edit {{contentType.title}}
74 | Create {{contentType.title}} Entry
75 |
76 |
77 |
78 |
79 | $save Save
80 |
81 |
82 | $send Publish
83 |
84 |
85 |
86 |
87 |
89 |
--------------------------------------------------------------------------------
/client/pages/publications/_publication_key/contentTypes/create.vue:
--------------------------------------------------------------------------------
1 |
2 |
54 |
55 |
56 |
57 | Create Content Type
58 |
59 |
60 |
61 |
62 | $save Save
63 |
64 |
65 |
66 |
67 |
69 |
70 |
--------------------------------------------------------------------------------
/client/pages/publications/_publication_key/contentTypes/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
90 |
91 |
92 |
93 |
94 | Content Types
95 |
96 | $plus Add New
97 |
98 |
99 |
100 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
122 |
123 |
--------------------------------------------------------------------------------
/client/pages/publications/_publication_key/edit.vue:
--------------------------------------------------------------------------------
1 |
2 |
51 |
52 |
53 |
54 | Edit Publication
55 |
56 |
57 |
58 |
59 | $save Save
60 |
61 |
62 |
63 |
64 |
66 |
67 |
--------------------------------------------------------------------------------
/client/pages/publications/_publication_key/files/_file_key/edit.vue:
--------------------------------------------------------------------------------
1 |
2 |
25 |
26 |
27 |
28 | {{$route.params._file_key}}
29 | Edit File
30 |
31 |
32 |
33 |
34 |
35 |
36 |
38 |
39 |
--------------------------------------------------------------------------------
/client/pages/publications/create.vue:
--------------------------------------------------------------------------------
1 |
2 |
50 |
51 |
52 |
53 | Create Publication
54 |
55 |
56 |
57 |
58 | $save Save
59 |
60 |
61 |
62 |
63 |
65 |
66 |
--------------------------------------------------------------------------------
/client/pages/publications/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
76 |
77 |
78 |
79 |
80 | Publications
81 |
82 | $plus Add New
83 |
84 |
85 |
86 |
94 |
95 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
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 |
47 |
61 |
62 |
63 |
79 |
--------------------------------------------------------------------------------
/client/ui/components/design/designCodeField.vue:
--------------------------------------------------------------------------------
1 |
2 |
45 |
46 |
47 |
62 |
63 |
64 |
85 |
--------------------------------------------------------------------------------
/client/ui/components/design/designColumnLayout.vue:
--------------------------------------------------------------------------------
1 |
2 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
{{field.label}}
58 |
59 |
60 |
61 |
62 |
63 |
64 | Drag Fields Here
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | Drag Fields Here
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
118 |
--------------------------------------------------------------------------------
/client/ui/components/design/designDateField.vue:
--------------------------------------------------------------------------------
1 |
2 |
45 |
46 |
47 |
60 |
61 |
62 |
78 |
--------------------------------------------------------------------------------
/client/ui/components/design/designFileField.vue:
--------------------------------------------------------------------------------
1 |
2 |
48 |
49 |
50 |
51 |
52 | {{field.label}}
53 |
54 |
55 |
56 |
57 |
No File Selected
58 |
59 |
60 | $search Choose File
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
85 |
--------------------------------------------------------------------------------
/client/ui/components/design/designLinkField.vue:
--------------------------------------------------------------------------------
1 |
2 |
45 |
46 |
47 |
48 |
49 |
$generic
50 |
51 | {{field.label}}
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
90 |
--------------------------------------------------------------------------------
/client/ui/components/design/designMultiLineTextbox.vue:
--------------------------------------------------------------------------------
1 |
2 |
45 |
46 |
47 |
60 |
61 |
62 |
78 |
--------------------------------------------------------------------------------
/client/ui/components/design/designReferenceField.vue:
--------------------------------------------------------------------------------
1 |
2 |
45 |
46 |
47 |
48 |
49 |
$generic
50 |
51 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
80 |
--------------------------------------------------------------------------------
/client/ui/components/design/designRichTextEditor.vue:
--------------------------------------------------------------------------------
1 |
44 |
45 |
46 |
53 |
54 |
55 |
71 |
--------------------------------------------------------------------------------
/client/ui/components/design/designSelectField.vue:
--------------------------------------------------------------------------------
1 |
2 |
45 |
46 |
47 |
60 |
61 |
62 |
78 |
--------------------------------------------------------------------------------
/client/ui/components/design/designSingleLineTextbox.vue:
--------------------------------------------------------------------------------
1 |
2 |
45 |
46 |
47 |
60 |
61 |
62 |
78 |
--------------------------------------------------------------------------------
/client/ui/components/edit/archive/editReferenceMultiField.vue:
--------------------------------------------------------------------------------
1 |
96 |
97 |
98 |
99 |
110 |
114 |
115 |
118 | {{item.contentType.title}}:
119 |
120 | {{item.title}}
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
130 |
--------------------------------------------------------------------------------
/client/ui/components/edit/archive/editReferenceSingleField.vue:
--------------------------------------------------------------------------------
1 |
96 |
97 |
98 |
99 |
109 |
110 | {{item.contentType.title}}:
111 | {{item.title}}
112 |
113 |
114 |
117 | {{item.contentType.title}}:
118 |
119 | {{item.title}}
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
129 |
--------------------------------------------------------------------------------
/client/ui/components/edit/editBooleanField.vue:
--------------------------------------------------------------------------------
1 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
43 |
44 |
--------------------------------------------------------------------------------
/client/ui/components/edit/editCodeField.vue:
--------------------------------------------------------------------------------
1 |
34 |
35 |
36 |
37 |
45 |
46 |
47 |
48 |
54 |
55 |
--------------------------------------------------------------------------------
/client/ui/components/edit/editColumnLayout.vue:
--------------------------------------------------------------------------------
1 |
57 |
58 |
59 |
60 |
{{field.label}}
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
100 |
--------------------------------------------------------------------------------
/client/ui/components/edit/editDateField.vue:
--------------------------------------------------------------------------------
1 |
48 |
49 |
50 |
51 |
59 |
60 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
79 |
80 |
--------------------------------------------------------------------------------
/client/ui/components/edit/editFileField.vue:
--------------------------------------------------------------------------------
1 |
83 |
84 |
85 |
86 | {{field.label}}
87 |
88 |
89 |
90 |
91 |
No File Selected
92 |
93 |
94 | $search Choose File
95 |
96 |
97 | $cancel Remove File
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
107 |
--------------------------------------------------------------------------------
/client/ui/components/edit/editLinkField.vue:
--------------------------------------------------------------------------------
1 |
58 |
59 |
60 |
61 | {{field.label}}
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
88 |
--------------------------------------------------------------------------------
/client/ui/components/edit/editMultiLineTextbox.vue:
--------------------------------------------------------------------------------
1 |
34 |
35 |
36 |
37 |
43 |
44 |
45 |
46 |
48 |
49 |
--------------------------------------------------------------------------------
/client/ui/components/edit/editRichTextEditor.vue:
--------------------------------------------------------------------------------
1 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
54 |
--------------------------------------------------------------------------------
/client/ui/components/edit/editSelectField.vue:
--------------------------------------------------------------------------------
1 |
42 |
43 |
44 |
45 |
52 |
53 |
54 |
55 |
57 |
--------------------------------------------------------------------------------
/client/ui/components/edit/editSingleLineTextbox.vue:
--------------------------------------------------------------------------------
1 |
34 |
35 |
36 |
37 |
43 |
44 |
45 |
46 |
48 |
49 |
--------------------------------------------------------------------------------
/client/ui/components/view/viewSingleLineTextbox.vue:
--------------------------------------------------------------------------------
1 |
33 |
34 |
35 |
36 |
43 |
44 |
45 |
46 |
48 |
49 |
--------------------------------------------------------------------------------
/client/ui/fieldComponents/optionEditors/archive/contentTypesOption.vue:
--------------------------------------------------------------------------------
1 |
2 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
126 |
127 |
128 |
129 |
137 |
138 |
--------------------------------------------------------------------------------
/client/ui/fieldComponents/optionEditors/archive/genericOptions.vue:
--------------------------------------------------------------------------------
1 |
2 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
77 |
78 |
--------------------------------------------------------------------------------
/client/ui/fieldComponents/optionEditors/archive/groupOptions.vue:
--------------------------------------------------------------------------------
1 |
2 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
76 |
77 |
--------------------------------------------------------------------------------
/client/ui/fieldComponents/optionEditors/archive/textOptions.vue:
--------------------------------------------------------------------------------
1 |
2 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
62 |
63 |
--------------------------------------------------------------------------------
/client/ui/fieldComponents/optionEditors/contentTypesOption.vue:
--------------------------------------------------------------------------------
1 |
2 |
88 |
89 |
90 |
100 |
101 |
102 |
104 |
--------------------------------------------------------------------------------
/client/ui/fieldComponents/optionEditors/groupModeOption.vue:
--------------------------------------------------------------------------------
1 |
2 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
69 |
--------------------------------------------------------------------------------
/client/ui/fieldComponents/optionEditors/hintOption.vue:
--------------------------------------------------------------------------------
1 |
2 |
49 |
50 |
51 |
57 |
58 |
59 |
61 |
--------------------------------------------------------------------------------
/client/ui/fieldComponents/optionEditors/requiredOption.vue:
--------------------------------------------------------------------------------
1 |
2 |
49 |
50 |
51 |
52 |
53 |
54 |
56 |
--------------------------------------------------------------------------------
/client/ui/fieldComponents/optionEditors/selectModeOption.vue:
--------------------------------------------------------------------------------
1 |
2 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
69 |
--------------------------------------------------------------------------------
/client/ui/filePreview.vue:
--------------------------------------------------------------------------------
1 |
2 |
88 |
89 |
90 |
91 |
92 |
93 |
![]()
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | {{mimeIcon}}
102 |
103 |
104 |
{{file.uploadedFilename}}
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | $cancel
113 |
114 |
115 |
116 |
117 |
118 |
119 |
132 |
--------------------------------------------------------------------------------
/client/ui/forms/accessKeyForm.vue:
--------------------------------------------------------------------------------
1 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
57 |
58 |
--------------------------------------------------------------------------------
/client/ui/forms/blockForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
89 |
--------------------------------------------------------------------------------
/client/ui/forms/componentForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | Choose Component Type
64 |
65 |
66 |
67 | Rich Text
68 | A user friendly editor for text content.
69 |
70 |
71 | HTML
72 | Create raw html for maximum flexibility.
73 |
74 |
75 | Media
76 | Add images or video.
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
95 |
--------------------------------------------------------------------------------
/client/ui/forms/contentTypeForm.vue:
--------------------------------------------------------------------------------
1 |
52 |
53 |
54 |
55 |
56 | Settings
57 |
58 |
59 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
80 |
81 |
--------------------------------------------------------------------------------
/client/ui/forms/entryForm.vue:
--------------------------------------------------------------------------------
1 |
77 |
78 |
79 |
85 |
86 |
87 |
89 |
--------------------------------------------------------------------------------
/client/ui/forms/publicationForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
50 |
--------------------------------------------------------------------------------
/client/ui/imageColors.vue:
--------------------------------------------------------------------------------
1 |
2 |
60 |
61 |
62 |
65 |
66 |
67 |
79 |
--------------------------------------------------------------------------------
/client/ui/lists/accessKeyList.vue:
--------------------------------------------------------------------------------
1 |
2 |
67 |
68 |
69 |
76 |
77 | {{item.title}}
78 |
79 |
80 | {{item.environment}}
81 | {{item.environment}}
82 |
83 |
84 |
85 |
86 |
87 |
88 | $edit
89 |
90 |
91 |
92 | Edit
93 |
94 |
95 |
96 |
97 |
98 | $delete
99 |
100 |
101 |
102 | Delete
103 |
104 |
105 |
106 |
107 |
108 |
110 |
--------------------------------------------------------------------------------
/client/ui/lists/entryList.vue:
--------------------------------------------------------------------------------
1 |
2 |
65 |
66 |
67 |
74 |
75 | {{item.title}}
76 |
77 |
78 |
79 |
80 |
81 |
82 | $edit
83 |
84 |
85 |
86 | Edit
87 |
88 |
89 |
90 |
91 |
92 | $delete
93 |
94 |
95 |
96 | Delete
97 |
98 |
99 |
100 |
101 |
102 |
104 |
--------------------------------------------------------------------------------
/client/ui/lists/publicationList.vue:
--------------------------------------------------------------------------------
1 |
2 |
75 |
76 |
77 |
84 |
85 | {{item.title}}
86 |
87 |
88 |
89 |
90 |
91 |
92 | $externalLinkAlt
93 |
94 |
95 |
96 | Open
97 |
98 |
99 |
100 |
101 |
102 | $edit
103 |
104 |
105 |
106 | Edit
107 |
108 |
109 |
110 |
111 |
112 | $delete
113 |
114 |
115 |
116 | Delete
117 |
118 |
119 |
120 |
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 |
--------------------------------------------------------------------------------