├── .env.dev
├── .eslintignore
├── .eslintrc
├── .github
├── CODE_OF_CONDUCT.md
└── CONTRIBUTING.md
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── app
├── actions
│ ├── assetActions.js
│ ├── entryActions.js
│ ├── fetchData.js
│ ├── fieldActions.js
│ ├── pageActions.js
│ ├── pluginActions.js
│ ├── sectionActions.js
│ ├── siteActions.js
│ ├── uiActions.js
│ ├── userActions.js
│ └── usergroupActions.js
├── assets
│ ├── android-chrome-192x192.png
│ ├── android-chrome-256x256.png
│ ├── apple-touch-icon.png
│ ├── browserconfig.xml
│ ├── default_user.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.png
│ ├── mstile-150x150.png
│ └── safari-pinned-tab.svg
├── components
│ ├── Avatar
│ │ └── index.js
│ ├── Breadcrumbs
│ │ ├── Breadcrumbs.scss
│ │ └── index.js
│ ├── Button
│ │ ├── Button.scss
│ │ └── index.js
│ ├── Checkbox
│ │ ├── Checkbox.scss
│ │ ├── Checkboxes.js
│ │ └── index.js
│ ├── DatePicker
│ │ ├── DatePicker.scss
│ │ ├── DayTile.js
│ │ ├── Days.js
│ │ └── index.js
│ ├── DeleteIcon
│ │ └── index.js
│ ├── DropdownButton
│ │ ├── DropdownButtons.scss
│ │ └── index.js
│ ├── FieldOptions
│ │ └── index.js
│ ├── Fields
│ │ ├── Asset
│ │ │ ├── Asset.scss
│ │ │ ├── AssetModal.js
│ │ │ └── index.js
│ │ ├── Color
│ │ │ ├── Color.scss
│ │ │ └── index.js
│ │ ├── Dropdown
│ │ │ ├── Dropdown.scss
│ │ │ └── index.js
│ │ ├── Group
│ │ │ ├── FieldColumn.js
│ │ │ ├── Group.scss
│ │ │ ├── GroupTile.js
│ │ │ ├── NewBlockModal.js
│ │ │ ├── Panel.js
│ │ │ ├── Panel.scss
│ │ │ └── index.js
│ │ ├── Numeric
│ │ │ └── index.js
│ │ ├── RichText
│ │ │ ├── RichText.scss
│ │ │ └── index.js
│ │ ├── Toggle
│ │ │ ├── Toggle.scss
│ │ │ └── index.js
│ │ └── index.js
│ ├── FileInput
│ │ ├── FileInput.scss
│ │ └── index.js
│ ├── FlintLogo
│ │ ├── FlintLogo.scss
│ │ └── index.js
│ ├── Input
│ │ ├── Input.scss
│ │ └── index.js
│ ├── List
│ │ ├── List.scss
│ │ └── index.js
│ ├── Loading
│ │ ├── Loading.scss
│ │ └── index.js
│ ├── Modals
│ │ └── ConfirmModal.js
│ ├── Notification
│ │ ├── Notification.scss
│ │ └── index.js
│ ├── SecondaryNav
│ │ ├── SecondaryNav.scss
│ │ └── index.js
│ ├── StatusDot
│ │ ├── StatusDot.scss
│ │ └── index.js
│ ├── Table
│ │ ├── Cell.js
│ │ ├── THead.js
│ │ ├── Table.scss
│ │ └── index.js
│ ├── TitleBar
│ │ ├── TitleBar.scss
│ │ └── index.js
│ └── Toast
│ │ ├── Toast.scss
│ │ └── index.js
├── containers
│ ├── Aside
│ │ ├── Aside.scss
│ │ └── index.js
│ ├── Empty
│ │ ├── Empty.scss
│ │ └── index.js
│ ├── ErrorContainer
│ │ ├── ErrorContainer.scss
│ │ └── index.js
│ ├── FieldLayout
│ │ ├── FieldLayout.scss
│ │ ├── FieldSource.js
│ │ ├── FieldTarget.js
│ │ ├── FieldTargetCard.js
│ │ ├── index.js
│ │ └── utils
│ │ │ ├── collect.js
│ │ │ ├── constants.js
│ │ │ ├── source.js
│ │ │ └── target.js
│ ├── Footer
│ │ ├── Footer.scss
│ │ └── index.js
│ ├── LoginContainer
│ │ ├── LoginContainer.scss
│ │ └── index.js
│ ├── Main
│ │ ├── Main.scss
│ │ └── index.js
│ ├── MainNav
│ │ ├── MainNav.scss
│ │ └── index.js
│ ├── Modals
│ │ ├── Modals.scss
│ │ └── index.js
│ └── Page
│ │ ├── Page.scss
│ │ └── index.js
├── index.tpl.html
├── main.js
├── main.scss
├── manifest.json
├── reducers
│ ├── assets.js
│ ├── entries.js
│ ├── fields.js
│ ├── pages.js
│ ├── plugins.js
│ ├── sections.js
│ ├── site.js
│ ├── ui.js
│ ├── user.js
│ ├── usergroups.js
│ └── users.js
├── scss
│ ├── normalize.scss
│ └── tools
│ │ ├── mixins.scss
│ │ ├── tools.scss
│ │ └── variables.scss
├── utils
│ ├── camelcase.js
│ ├── formatFields.js
│ ├── getUserPermissions.js
│ ├── graphFetcher.js
│ ├── helpers.js
│ ├── icons.js
│ ├── permissionsQuery.js
│ ├── prettyNames.js
│ ├── renderOption.js
│ ├── rootReducer.js
│ ├── socketEvents.js
│ ├── store.js
│ ├── types.js
│ └── validateFields.js
└── views
│ ├── 404
│ └── index.js
│ ├── Assets
│ ├── Asset
│ │ └── index.js
│ ├── Assets
│ │ └── index.js
│ └── NewAsset
│ │ └── index.js
│ ├── Auth
│ ├── ForgotPassword
│ │ └── index.js
│ ├── Install
│ │ ├── Install.scss
│ │ └── index.js
│ ├── Login
│ │ └── index.js
│ └── SetPassword
│ │ └── index.js
│ ├── Entries
│ ├── Entries
│ │ └── index.js
│ ├── Entry
│ │ └── index.js
│ └── NewEntry
│ │ └── index.js
│ ├── Fields
│ ├── Field
│ │ └── index.js
│ ├── Fields
│ │ └── index.js
│ └── NewField
│ │ └── index.js
│ ├── Home
│ ├── Home.scss
│ └── index.js
│ ├── Pages
│ ├── NewPage
│ │ └── index.js
│ ├── Page
│ │ └── index.js
│ ├── Pages
│ │ └── index.js
│ └── SettingsPage
│ │ └── index.js
│ ├── Sections
│ ├── NewSection
│ │ └── index.js
│ ├── Section
│ │ └── index.js
│ └── Sections
│ │ └── index.js
│ ├── Settings
│ ├── Logs
│ │ ├── Logs.scss
│ │ └── index.js
│ ├── Plugins
│ │ └── index.js
│ ├── Settings.scss
│ ├── Site
│ │ └── index.js
│ ├── Styles
│ │ ├── CodeMirror.scss
│ │ ├── Styles.scss
│ │ └── index.js
│ └── index.js
│ ├── UserGroups
│ ├── NewUserGroup
│ │ └── index.js
│ ├── UserGroup
│ │ └── index.js
│ └── UserGroups
│ │ └── index.js
│ └── Users
│ ├── NewUser
│ └── index.js
│ ├── User
│ └── index.js
│ └── Users
│ └── index.js
├── codecov.yml
├── config
├── constants.js
├── webpack.config.js
└── webpack.production.config.js
├── dev.js
├── index.js
├── package-lock.json
├── package.json
├── server
├── apps
│ ├── admin.js
│ ├── api.js
│ ├── graphql.js
│ └── routes
│ │ ├── assets.js
│ │ ├── auth.js
│ │ ├── logs.js
│ │ └── site.js
├── graphql
│ ├── get-projection.js
│ ├── index.js
│ ├── mutations
│ │ ├── assets
│ │ │ ├── addAsset.js
│ │ │ ├── index.js
│ │ │ ├── indexAssets.js
│ │ │ ├── removeAsset.js
│ │ │ └── updateAsset.js
│ │ ├── entries
│ │ │ ├── addEntry.js
│ │ │ ├── index.js
│ │ │ ├── removeEntry.js
│ │ │ └── updateEntry.js
│ │ ├── fields
│ │ │ ├── addField.js
│ │ │ ├── index.js
│ │ │ ├── removeField.js
│ │ │ └── updateField.js
│ │ ├── index.js
│ │ ├── pages
│ │ │ ├── addPage.js
│ │ │ ├── index.js
│ │ │ ├── removePage.js
│ │ │ └── updatePage.js
│ │ ├── sections
│ │ │ ├── addSection.js
│ │ │ ├── index.js
│ │ │ ├── removeSection.js
│ │ │ └── updateSection.js
│ │ ├── site
│ │ │ ├── index.js
│ │ │ └── updateSite.js
│ │ ├── usergroups
│ │ │ ├── addUserGroup.js
│ │ │ ├── index.js
│ │ │ ├── removeUserGroup.js
│ │ │ └── updateUserGroup.js
│ │ └── users
│ │ │ ├── addUser.js
│ │ │ ├── deleteUser.js
│ │ │ ├── index.js
│ │ │ ├── resetPassword.js
│ │ │ └── updateUser.js
│ ├── queries
│ │ ├── assets
│ │ │ ├── index.js
│ │ │ ├── multiple.js
│ │ │ └── single.js
│ │ ├── entries
│ │ │ ├── index.js
│ │ │ ├── multiple.js
│ │ │ └── single.js
│ │ ├── fields
│ │ │ ├── index.js
│ │ │ ├── multiple.js
│ │ │ └── single.js
│ │ ├── index.js
│ │ ├── pages
│ │ │ ├── index.js
│ │ │ ├── multiple.js
│ │ │ └── single.js
│ │ ├── plugins
│ │ │ ├── index.js
│ │ │ └── multiple.js
│ │ ├── sections
│ │ │ ├── index.js
│ │ │ ├── multiple.js
│ │ │ └── single.js
│ │ ├── site
│ │ │ ├── index.js
│ │ │ └── site.js
│ │ ├── usergroups
│ │ │ ├── index.js
│ │ │ ├── multiple.js
│ │ │ └── single.js
│ │ └── users
│ │ │ ├── index.js
│ │ │ ├── multiple.js
│ │ │ └── single.js
│ └── types
│ │ ├── Assets.js
│ │ ├── CustomTypes
│ │ ├── DateTime.js
│ │ ├── FieldType.js
│ │ ├── ObjectType.js
│ │ └── index.js
│ │ ├── Entries.js
│ │ ├── Fields.js
│ │ ├── Pages.js
│ │ ├── Plugins.js
│ │ ├── Sections.js
│ │ ├── Site.js
│ │ ├── UserGroups.js
│ │ └── Users.js
├── index.js
├── models
│ ├── AssetSchema.js
│ ├── EntrySchema.js
│ ├── FieldSchema.js
│ ├── PageSchema.js
│ ├── PluginSchema.js
│ ├── SectionSchema.js
│ ├── SiteSchema.js
│ ├── UserGroupSchema.js
│ └── UserSchema.js
└── utils
│ ├── FlintPlugin.js
│ ├── collect-data.js
│ ├── compile-sass.js
│ ├── compile.js
│ ├── create-admin-usergroup.js
│ ├── database.js
│ ├── emails
│ ├── compile.js
│ ├── flintlogo.png
│ ├── index.js
│ ├── sendEmail.js
│ └── templates
│ │ ├── forgot-password.html
│ │ ├── layouts
│ │ └── base.html
│ │ ├── new-account.html
│ │ ├── reset-password.html
│ │ └── styles
│ │ ├── _settings.scss
│ │ ├── emails.scss
│ │ └── foundation
│ │ ├── _foundation.scss
│ │ ├── _global.scss
│ │ ├── components
│ │ ├── _alignment.scss
│ │ ├── _button.scss
│ │ ├── _callout.scss
│ │ ├── _code.scss
│ │ ├── _media-query.scss
│ │ ├── _menu.scss
│ │ ├── _normalize.scss
│ │ ├── _outlook-first.scss
│ │ ├── _thumbnail.scss
│ │ ├── _typography.scss
│ │ └── _visibility.scss
│ │ ├── grid
│ │ ├── _block-grid.scss
│ │ └── _grid.scss
│ │ ├── settings
│ │ └── _settings.scss
│ │ └── util
│ │ └── _util.scss
│ ├── emit-socket-event.js
│ ├── events.js
│ ├── four-oh-four-handler.js
│ ├── generate-env-file.js
│ ├── get-asset-details.js
│ ├── get-entry-data.js
│ ├── get-user-permissions.js
│ ├── handle-compile-error-routes.js
│ ├── helpers.js
│ ├── log.js
│ ├── logger.js
│ ├── nunjucks.js
│ ├── passport.js
│ ├── permissions.json
│ ├── public-registration.js
│ ├── reduce-perms-to-object.js
│ ├── register-plugins.js
│ ├── scaffold.js
│ ├── template-routes.js
│ ├── update-site-config.js
│ └── validate-env-variables.js
└── test
├── fixtures
├── images
│ ├── image.png
│ └── image2.png
├── logs
│ ├── flint.log
│ └── http-requests.log
├── plugins
│ ├── ConsolePlugin.js
│ └── icon.png
├── scss
│ ├── main.css
│ └── main.scss
└── templates
│ ├── 404.njk
│ ├── 404.txt
│ ├── empty
│ └── .gitkeep
│ ├── entry.njk
│ ├── entry.txt
│ ├── fieldFilter.njk
│ ├── fieldFilter.txt
│ ├── index.njk
│ ├── index.txt
│ ├── page-with-vars.njk
│ └── page-with-vars.txt
├── index.test.js
├── mocks
├── assets.js
├── entries.js
├── fields.js
├── index.js
├── pages.js
├── plugins.js
├── sections.js
├── site.js
├── user.js
├── usergroups.js
└── users.js
├── populatedb.js
├── server
├── apps
│ ├── admin.test.js
│ ├── common.js
│ ├── first-run.test.js
│ ├── plugins.test.js
│ ├── routes
│ │ ├── assets.test.js
│ │ ├── auth.test.js
│ │ ├── logs.test.js
│ │ └── site.test.js
│ └── types
│ │ ├── assets.test.js
│ │ ├── entries.test.js
│ │ ├── fields.test.js
│ │ ├── pages.test.js
│ │ ├── sections.test.js
│ │ ├── site.test.js
│ │ ├── usergroups.test.js
│ │ └── users.test.js
└── utils
│ ├── FlintPlugin.test.js
│ ├── __snapshots__
│ └── compile.test.js.snap
│ ├── compile-sass.test.js
│ ├── compile.test.js
│ ├── create-admin-usergroup.test.js
│ ├── generate-env-file.test.js
│ ├── get-entry-data.test.js
│ ├── helpers.test.js
│ ├── public-registration.test.js
│ └── update-site-config.test.js
└── setup.js
/.env.dev:
--------------------------------------------------------------------------------
1 | # Secret settings
2 | SESSION_SECRET=Fy#xXd)L6UOjrJiOFCHpf3qqesa!h#+z
3 |
4 | # Mongo Credentials
5 | DB_HOST=127.0.0.1/test
6 | DB_USER=admin
7 | DB_PASS=admin
8 |
9 | DEBUG=flint*
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | admin
3 | public
4 | coverage
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": ["standard", "standard-jsx"],
4 | "rules": {
5 | "jsx-quotes": ["error", "prefer-double"],
6 | "react/jsx-no-bind": 0
7 | },
8 | "parserOptions": {
9 | "ecmaFeatures": {
10 | "jsx": true,
11 | "experimentalObjectRestSpread": true
12 | }
13 | },
14 | "settings": {
15 | "import/resolver": {
16 | "webpack": {
17 | "config": "config/webpack.config.js"
18 | }
19 | }
20 | },
21 | "env": {
22 | "jest": true,
23 | "browser": true,
24 | "node": true
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /admin
3 | /templates
4 | /public
5 | /scss
6 | .env
7 | admin
8 | flint
9 | coverage
10 | public
11 | .nyc_output
12 | .DS_Store
13 | *.log
14 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | templates
2 | scss
3 | logs
4 | public
5 | dev.js
6 | test
7 | admin/report.html
8 | app
9 | .*
10 | coverage
11 | config
12 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 |
3 | language: node_js
4 |
5 | cache:
6 | directories:
7 | - node_modules
8 |
9 | services:
10 | - mongodb
11 |
12 | node_js:
13 | - "8"
14 |
15 | before_script:
16 | - npm run build
17 |
18 | notifications:
19 | disabled: true
20 |
21 | install:
22 | - npm install -g codecov
23 | - npm install
24 |
25 | branches:
26 | except:
27 | - /^v\d+\.\d+\.\d+$/
28 |
29 | script:
30 | - npm test
31 | - codecov
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Jason Etcovitch
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/app/actions/pluginActions.js:
--------------------------------------------------------------------------------
1 | export const REQUEST_PLUGINS = 'REQUEST_PLUGINS'
2 | export const RECEIVE_PLUGINS = 'RECEIVE_PLUGINS'
3 |
--------------------------------------------------------------------------------
/app/actions/siteActions.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import graphFetcher from '../utils/graphFetcher'
3 | import { newToast, errorToasts } from './uiActions'
4 |
5 | export const REQUEST_SITE = 'REQUEST_SITE'
6 | export const RECEIVE_SITE = 'RECEIVE_SITE'
7 | export const UPDATE_SITE = 'UPDATE_SITE'
8 |
9 | /**
10 | * Saves updates of the general site config
11 | * @param {Object} data
12 | */
13 | export function updateSite (data) {
14 | return async (dispatch) => {
15 | const query = `mutation ($data: SiteInput!) {
16 | updateSite(data: $data) {
17 | siteUrl
18 | siteName
19 | defaultUserGroup
20 | allowPublicRegistration
21 | }
22 | }`
23 |
24 | return graphFetcher(query, { data })
25 | .then((json) => {
26 | const { updateSite: updatedSite } = json.data.data
27 | dispatch({ type: UPDATE_SITE, updateSite: updatedSite })
28 | dispatch(newToast({
29 | message: The site configuration has been has been updated! ,
30 | style: 'success'
31 | }))
32 | })
33 | .catch(errorToasts)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/assets/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/app/assets/android-chrome-192x192.png
--------------------------------------------------------------------------------
/app/assets/android-chrome-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/app/assets/android-chrome-256x256.png
--------------------------------------------------------------------------------
/app/assets/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/app/assets/apple-touch-icon.png
--------------------------------------------------------------------------------
/app/assets/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #fe6300
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/assets/default_user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/app/assets/default_user.png
--------------------------------------------------------------------------------
/app/assets/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/app/assets/favicon-16x16.png
--------------------------------------------------------------------------------
/app/assets/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/app/assets/favicon-32x32.png
--------------------------------------------------------------------------------
/app/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/app/assets/favicon.png
--------------------------------------------------------------------------------
/app/assets/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/app/assets/mstile-150x150.png
--------------------------------------------------------------------------------
/app/assets/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 | Created by potrace 1.11, written by Peter Selinger 2001-2013
9 |
10 |
12 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/components/Avatar/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { shape, string } from 'prop-types'
3 | import defaultImage from 'assets/default_user.png'
4 |
5 | export default function Avatar ({ user }) {
6 | if (user.image) return
7 | return
8 | }
9 |
10 | Avatar.propTypes = {
11 | user: shape({
12 | username: string.isRequired,
13 | image: string
14 | }).isRequired
15 | }
16 |
--------------------------------------------------------------------------------
/app/components/Breadcrumbs/Breadcrumbs.scss:
--------------------------------------------------------------------------------
1 | .breadcrumbs {
2 | box-sizing: border-box;
3 | width: 100%;
4 | padding: 0.75rem 2rem;
5 | border-bottom: 1px solid $gray-300;
6 | flex-shrink: 0;
7 | background-color: $gray-000;
8 | font-size: 0.7rem;
9 |
10 | @include media($on-mobile) {
11 | padding: 0.7rem 1rem;
12 | }
13 |
14 | &__list {
15 | display: flex;
16 | list-style-type: none;
17 | padding: 0;
18 | margin: 0;
19 |
20 | &-item {
21 | &__separator {
22 | margin: 0 0.4em;
23 | color: $gray-500;
24 | }
25 |
26 | a {
27 | color: $gray-500;
28 | text-decoration: none;
29 |
30 | &:hover { color: $gray-700; }
31 | }
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/components/Breadcrumbs/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Link } from 'react-router-dom'
4 | import './Breadcrumbs.scss'
5 |
6 | export default class Breadcrumbs extends Component {
7 | static propTypes = {
8 | links: PropTypes.arrayOf(PropTypes.shape({
9 | label: PropTypes.string.isRequired,
10 | path: PropTypes.string.isRequired
11 | })).isRequired
12 | };
13 |
14 | render () {
15 | const { links } = this.props
16 | return (
17 |
18 |
19 | {links.map((l, i, arr) => {
20 | const isLast = i === arr.length - 1
21 | return (
22 |
23 | {l.label}
24 | {!isLast && › }
25 |
26 | )
27 | })}
28 |
29 |
30 | )
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/components/Button/Button.scss:
--------------------------------------------------------------------------------
1 | .btn {
2 | display: inline-block;
3 | padding: 0.6em;
4 | border: 1px solid $orange-dark;
5 | border-radius: 3px;
6 | background-color: $orange;
7 | background-image: linear-gradient(to top, rgba(white, 0), rgba(white, 0.1));
8 | color: white;
9 | font-family: inherit;
10 | text-decoration: none;
11 | cursor: pointer;
12 |
13 | &:hover { background-color: $orange-light; }
14 |
15 | &:active { background-color: $orange-dark; }
16 |
17 | &:disabled { opacity: 0.5; pointer-events: none; }
18 |
19 | &--yes { background-color: $orange; }
20 |
21 | &--subtle {
22 | border-color: transparent;
23 | background-color: transparent;
24 | color: $gray-500;
25 |
26 | &:hover {
27 | border-color: $gray-300;
28 | background-color: $gray-300;
29 | }
30 | }
31 |
32 | &--small { font-size: 0.8rem; }
33 | }
34 |
--------------------------------------------------------------------------------
/app/components/Checkbox/Checkbox.scss:
--------------------------------------------------------------------------------
1 | .checkbox {
2 | &-wrapper {
3 | display: flex;
4 | align-items: center;
5 | }
6 |
7 | position: relative;
8 | width: 1em;
9 | height: 1em;
10 | border: 1px solid $gray-300;
11 | border-radius: 3px;
12 | background: none;
13 | transition: background-color 150ms $standard;
14 |
15 | &-group {
16 | .checkbox-wrapper + .checkbox-wrapper { margin-top: 0.4em; }
17 |
18 | &__label {
19 | display: block;
20 | margin-bottom: 0.4em;
21 | font-weight: 700;
22 | font-size: $size0;
23 | }
24 | }
25 |
26 | svg {
27 | position: absolute;
28 | top: 50%;
29 | left: 50%;
30 | fill: white;
31 | transform: translate(-50%, -50%) scale(0);
32 | transition: transform 101ms $exit;
33 | }
34 |
35 | &.is-checked {
36 | border-color: $gray-500;
37 | background-color: $orange;
38 |
39 | svg {
40 | transform: translate(-50%, -50%) scale(1);
41 | transition-duration: 200ms;
42 | transition-timing-function: $bounce;
43 | }
44 | }
45 |
46 | + .input__label {
47 | margin-left: 1em;
48 | margin-bottom: 0;
49 | font-weight: 500;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/components/Checkbox/Checkboxes.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import Checkbox from './index'
4 |
5 | export default class Checkboxes extends Component {
6 | static propTypes = {
7 | name: PropTypes.string.isRequired,
8 | label: PropTypes.string.isRequired,
9 | instructions: PropTypes.string,
10 | checkboxes: PropTypes.arrayOf(PropTypes.object).isRequired
11 | }
12 |
13 | static defaultProps = {
14 | instructions: null
15 | }
16 |
17 | render () {
18 | const { checkboxes, name, label, instructions } = this.props
19 |
20 | return (
21 |
22 |
{label}
23 | {instructions &&
{instructions}
}
24 | {checkboxes.map(check =>
)}
25 |
26 | )
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/components/DatePicker/DayTile.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import classnames from 'classnames'
4 |
5 | const DayTile = ({ day, isActive, disabled, onClick }) => {
6 | const classes = classnames(
7 | 'datepicker__date',
8 | { 'is-active': isActive },
9 | { 'is-disabled': disabled }
10 | )
11 |
12 | return (
13 |
19 | {day}
20 |
21 | )
22 | }
23 |
24 | DayTile.propTypes = {
25 | day: PropTypes.number.isRequired,
26 | isActive: PropTypes.bool.isRequired,
27 | disabled: PropTypes.bool,
28 | onClick: PropTypes.func
29 | }
30 |
31 | DayTile.defaultProps = {
32 | disabled: false,
33 | onClick: null
34 | }
35 |
36 | export default DayTile
37 |
--------------------------------------------------------------------------------
/app/components/DatePicker/Days.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
4 |
5 | const Days = () => (
6 |
7 | {days.map(day => {day} )}
8 |
)
9 |
10 | export default Days
11 |
--------------------------------------------------------------------------------
/app/components/DeleteIcon/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { openModal } from 'actions/uiActions'
4 | import Icon from 'utils/icons'
5 | import store from 'utils/store'
6 | import ConfirmModal from '../Modals/ConfirmModal'
7 |
8 | export default class DeleteIcon extends Component {
9 | static propTypes = {
10 | onClick: PropTypes.func.isRequired,
11 | message: PropTypes.string,
12 | small: PropTypes.bool
13 | }
14 |
15 | static defaultProps = {
16 | message: undefined,
17 | small: false
18 | }
19 |
20 | constructor (props) {
21 | super(props)
22 | this.onClick = this.onClick.bind(this)
23 | }
24 |
25 | onClick () {
26 | const { onClick, message, small } = this.props
27 | store.dispatch(openModal(
28 | )
33 | )
34 | }
35 |
36 | render () {
37 | return (
38 |
39 | )
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/components/DropdownButton/DropdownButtons.scss:
--------------------------------------------------------------------------------
1 | .dropdown-btn {
2 | &__btn {
3 | position: relative;
4 | border-color: $orange-dark;
5 | background-color: $orange;
6 |
7 | &::before {
8 | content: '';
9 | position: absolute;
10 | top: 0;
11 | right: 2em;
12 | width: 1px;
13 | height: 100%;
14 | background-color: $orange-light;
15 | }
16 |
17 | &::after {
18 | border-top-color: white;
19 | }
20 | }
21 |
22 | &__options {
23 | border-color: $orange-dark;
24 | background-color: $orange;
25 | }
26 |
27 | &__opt {
28 | display: block;
29 | box-sizing: border-box;
30 | padding: 0.6em;
31 | border: none;
32 | border-bottom: 1px solid $orange-dark;
33 | background: none;
34 | outline: none;
35 | background-color: $orange;
36 | color: white;
37 | text-decoration: none;
38 |
39 | &:last-of-type { border-bottom: none; }
40 |
41 | &:hover, &:focus {
42 | background-color: $orange-light;
43 | }
44 | }
45 |
46 | &.is-open {
47 | .dropdown-btn__btn {
48 | background-color: $orange-light;
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/components/FieldOptions/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/no-array-index-key */
2 |
3 | import React, { Component } from 'react'
4 | import PropTypes from 'prop-types'
5 | import Fields from 'components/Fields'
6 |
7 | export default class FieldOptions extends Component {
8 | static propTypes = {
9 | fields: PropTypes.object,
10 | field: PropTypes.object,
11 | type: PropTypes.string.isRequired,
12 | onChange: PropTypes.func
13 | }
14 |
15 | static defaultProps = {
16 | fields: Fields,
17 | field: null,
18 | onChange: null
19 | }
20 |
21 | render () {
22 | const { fields, type, field, onChange } = this.props
23 | const optionFields = fields[type].fields
24 |
25 | if (!optionFields) return null
26 |
27 | return (
28 |
29 |
{type} Options
30 | {(optionFields).map((F) => {
31 | if (!field) return
32 | return
33 | })}
34 |
35 | )
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/components/Fields/Asset/Asset.scss:
--------------------------------------------------------------------------------
1 | .asset {
2 | &__img-wrapper {
3 | display: flex;
4 | align-items: center;
5 | justify-content: center;
6 | width: 8rem;
7 | height: 8rem;
8 | padding: 1em;
9 | border-radius: 3px;
10 | border: 1px solid $gray-300;
11 |
12 | img { max-width: 100%; }
13 | }
14 |
15 | &__btn {
16 | padding: 0;
17 | border: none;
18 | margin: 0;
19 | background: white;
20 | cursor: pointer;
21 |
22 | @extend %focus;
23 |
24 | svg {
25 | fill: $gray-300;
26 | }
27 |
28 | &:hover {
29 | .asset__img-wrapper {
30 | border-color: $gray-500;
31 | }
32 | }
33 |
34 | &__title {
35 | box-sizing: border-box;
36 | width: 100%;
37 | border-radius: 3px;
38 | margin-top: 1em;
39 | margin-bottom: 0;
40 | font-size: $size0;
41 | font-weight: 500;
42 | }
43 | }
44 |
45 | &-modal {
46 | &__assets {
47 | display: flex;
48 | }
49 |
50 | .table-wrapper {
51 | width: 100%;
52 |
53 | .table__row:hover {
54 | background-color: $gray-100;
55 | cursor: pointer;
56 | }
57 | }
58 | }
59 |
60 | &__thumbnail {
61 | width: 4rem;
62 | height: 4rem;
63 | padding: 0;
64 | border: none;
65 | margin: 0;
66 | background: none;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/app/components/Fields/Color/Color.scss:
--------------------------------------------------------------------------------
1 | .color {
2 | &__btn {
3 | display: block;
4 | width: 60px;
5 | height: 2em;
6 | border-radius: 3px;
7 | border: 1px solid $gray-300;
8 | }
9 |
10 | &__overlay {
11 | position: fixed;
12 | top: 0;
13 | right: 0;
14 | bottom: 0;
15 | left: 0;
16 | }
17 |
18 | &__picker {
19 | position: absolute;
20 | z-index: 4;
21 | opacity: 0;
22 | visibility: hidden;
23 | transition:
24 | opacity 150ms $standard,
25 | visibility 150ms $standard;
26 |
27 | &.is-open {
28 | opacity: 1;
29 | visibility: visible;
30 | }
31 |
32 | .chrome-picker { font-family: inherit !important; } // stylelint-disable-line
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/components/Fields/Group/Group.scss:
--------------------------------------------------------------------------------
1 | .group {
2 | &__buttons {
3 | display: flex;
4 | font-size: $size0;
5 |
6 | &__btn {
7 | padding: 0.6em;
8 | border: 1px solid $gray-300;
9 | border-right-width: 0;
10 | background: white;
11 | font-family: inherit;
12 | cursor: pointer;
13 |
14 | &:hover {
15 | background-color: $gray-300;
16 | }
17 |
18 | &:first-of-type {
19 | border-top-left-radius: 3px;
20 | border-bottom-left-radius: 3px;
21 |
22 | &::before {
23 | content: '+ ';
24 | }
25 | }
26 |
27 | &:last-of-type {
28 | border-right-width: 1px;
29 | border-top-right-radius: 3px;
30 | border-bottom-right-radius: 3px;
31 | }
32 | }
33 | }
34 |
35 | &__block {
36 | position: relative;
37 | padding: 1em;
38 | border: 1px solid $gray-300;
39 | border-radius: 3px;
40 | background-color: $gray-100;
41 |
42 | &__btns {
43 | position: absolute;
44 | z-index: 2;
45 | top: 1em;
46 | right: 1em;
47 | display: flex;
48 | align-items: center;
49 | }
50 | }
51 |
52 | &__drag {
53 | display: flex;
54 | padding: 0;
55 | border: none;
56 | margin-right: 0.3em;
57 | background: none;
58 | cursor: pointer;
59 |
60 | svg { fill: $gray-400; }
61 |
62 | &:hover svg { fill: $gray-500; }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/components/Fields/Group/GroupTile.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/no-static-element-interactions */
2 |
3 | import React from 'react'
4 | import PropTypes from 'prop-types'
5 | import DeleteIcon from 'components/DeleteIcon'
6 | import store from 'utils/store'
7 |
8 | export default function GroupTile ({ onClick, label, handle, isActive, onDelete }) {
9 | const { dispatch } = store
10 | return (
11 |
15 | {label}
16 | {handle && {handle} }
17 | {onDelete && onDelete(label)} dispatch={dispatch} />}
18 |
19 | )
20 | }
21 |
22 | GroupTile.propTypes = {
23 | onClick: PropTypes.func.isRequired,
24 | label: PropTypes.string.isRequired,
25 | handle: PropTypes.string,
26 | isActive: PropTypes.bool.isRequired,
27 | onDelete: PropTypes.func
28 | }
29 |
30 | GroupTile.defaultProps = {
31 | onDelete: null,
32 | handle: null
33 | }
34 |
--------------------------------------------------------------------------------
/app/components/Fields/RichText/RichText.scss:
--------------------------------------------------------------------------------
1 | .rich-text {
2 | &__tools {
3 | position: relative;
4 | display: flex;
5 | align-items: center;
6 | padding: 1em;
7 | border-top-left-radius: 3px;
8 | border-top-right-radius: 3px;
9 | border: 1px solid $gray-300;
10 | background-color: white;
11 | }
12 |
13 | &-editor {
14 | overflow-y: auto;
15 | max-height: 500px;
16 | font-family: $font;
17 | background-color: white;
18 | line-height: 1.5;
19 |
20 | @include scrollbar($gray-300, white, 8px);
21 | }
22 |
23 | &-toolbar {
24 | font-family: $font;
25 |
26 | button {
27 | border-color: $gray-300;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/components/Fields/Toggle/Toggle.scss:
--------------------------------------------------------------------------------
1 | .toggle {
2 | position: relative;
3 | width: 2em;
4 | height: 1em;
5 | padding: 0;
6 | border: 1px solid $gray-300;
7 | border-radius: calc(0.5em + 2px);
8 | margin: 0;
9 | background-color: white;
10 | outline: none;
11 |
12 | @extend %focus;
13 |
14 | &__marker {
15 | position: absolute;
16 | top: -0.2em;
17 | left: -0.1em;
18 | width: 1.2em;
19 | height: 1.2em;
20 | border-radius: 50%;
21 | border: 1px solid $gray-300;
22 | background-color: white;
23 | box-shadow: 0 2px 4px rgba(black, 0.1);
24 | transition: transform 150ms $bounce;
25 | }
26 |
27 | &-wrapper {
28 | &.is-active {
29 | .toggle {
30 | background-color: $orange;
31 |
32 | &__marker {
33 | transform: translateX(calc(100% - 0.2em));
34 | }
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/components/FileInput/FileInput.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/app/components/FileInput/FileInput.scss
--------------------------------------------------------------------------------
/app/components/FlintLogo/FlintLogo.scss:
--------------------------------------------------------------------------------
1 | .flint-logo {
2 | &-wrapper {
3 | color: $gray-500;
4 | }
5 |
6 | &__poweredBy {
7 | margin: 0;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/app/components/Loading/Loading.scss:
--------------------------------------------------------------------------------
1 | .loading {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | width: 100%;
6 | height: 100%;
7 | background-color: $blurple;
8 | color: white;
9 | font-size: $size5;
10 | letter-spacing: -1px;
11 | }
12 |
--------------------------------------------------------------------------------
/app/components/Loading/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import './Loading.scss'
3 |
4 | export default class Loading extends Component {
5 | render () {
6 | return (
7 | Loading...
8 | )
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/app/components/Modals/ConfirmModal.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import Button from '../Button'
4 |
5 | export default class ConfirmModal extends Component {
6 | static propTypes = {
7 | confirm: PropTypes.func.isRequired,
8 | close: PropTypes.func,
9 | message: PropTypes.string,
10 | small: PropTypes.bool
11 | }
12 |
13 | static defaultProps = {
14 | close: null,
15 | message: 'Are you sure you want to do this?',
16 | small: false
17 | }
18 |
19 | constructor (props) {
20 | super(props)
21 | this.confirm = this.confirm.bind(this)
22 | this.handleKeyPress = this.handleKeyPress.bind(this)
23 | }
24 |
25 | componentDidMount () { window.addEventListener('keyup', this.handleKeyPress) }
26 | componentWillUnmount () { window.removeEventListener('keyup', this.handleKeyPress) }
27 |
28 | handleKeyPress (e) {
29 | if (e.which === 13) this.confirm()
30 | }
31 |
32 | confirm () {
33 | this.props.confirm()
34 | this.props.close()
35 | }
36 |
37 | render () {
38 | const { close, message, small } = this.props
39 |
40 | return (
41 |
42 | {message}
43 |
44 | Confirm
45 | Cancel
46 |
47 |
48 | )
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/components/Notification/Notification.scss:
--------------------------------------------------------------------------------
1 | .notification {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | padding: 1em;
6 | border-radius: 3px;
7 | font-size: $size0;
8 |
9 | svg { margin-right: 1em; flex-shrink: 0; }
10 |
11 | &--warning { background-color: $yellow; }
12 |
13 | &--error {
14 | background-color: $red;
15 | color: white;
16 |
17 | svg { fill: white; }
18 | }
19 |
20 | &--success { background-color: $green; }
21 | }
22 |
--------------------------------------------------------------------------------
/app/components/Notification/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import classnames from 'classnames'
4 | import Icon from 'utils/icons'
5 | import './Notification.scss'
6 |
7 | export default function Notification ({ type, children, formElement }) {
8 | const classes = classnames(
9 | 'notification',
10 | `notification--${type}`,
11 | { 'form-element': formElement }
12 | )
13 |
14 | const icons = {
15 | warning: 'breakLink',
16 | error: 'cross',
17 | success: 'checkmark'
18 | }
19 |
20 | return (
21 |
22 |
23 | {children}
24 |
25 | )
26 | }
27 |
28 | Notification.propTypes = {
29 | type: PropTypes.oneOf(['warning', 'error', 'success']),
30 | children: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
31 | formElement: PropTypes.bool
32 | }
33 |
34 | Notification.defaultProps = {
35 | type: 'warning',
36 | formElement: true
37 | }
38 |
--------------------------------------------------------------------------------
/app/components/SecondaryNav/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { NavLink } from 'react-router-dom'
4 | import './SecondaryNav.scss'
5 |
6 | const NavItem = props => {props.children}
7 | const NavButton = props => {props.children}
8 |
9 | NavItem.propTypes = {
10 | to: PropTypes.string.isRequired,
11 | children: PropTypes.string.isRequired
12 | }
13 | NavButton.propTypes = {
14 | onClick: PropTypes.func.isRequired,
15 | children: PropTypes.string.isRequired
16 | }
17 |
18 | export default class SecondaryNav extends Component {
19 | static propTypes = {
20 | links: PropTypes.arrayOf(PropTypes.shape({
21 | label: PropTypes.string.isRequired,
22 | path: PropTypes.string,
23 | onClick: PropTypes.func
24 | })).isRequired,
25 | children: PropTypes.element
26 | }
27 |
28 | static defaultProps = {
29 | children: null
30 | }
31 |
32 | render () {
33 | const { links, children } = this.props
34 |
35 | return (
36 |
37 |
38 | {children}
39 | {links.map((link) => {
40 | if (link.path) return {link.label}
41 | return {link.label}
42 | })}
43 |
44 |
45 | )
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/components/StatusDot/StatusDot.scss:
--------------------------------------------------------------------------------
1 | .status {
2 | width: 0.8rem;
3 | height: 0.8rem;
4 | border-radius: 50%;
5 | border-width: 1px;
6 | border-style: solid;
7 |
8 | &--live {
9 | border-color: darken($green, 10%);
10 | background-color: $green;
11 | }
12 |
13 | &--draft {
14 | border-color: $orange-dark;
15 | background-color: $orange;
16 | }
17 |
18 | &--disabled {
19 | border-color: $gray-600;
20 | background-color: $gray-500;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/components/StatusDot/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { capitalize } from 'utils/helpers'
4 | import './StatusDot.scss'
5 |
6 | const StatusDot = ({ status }) =>
7 |
8 | StatusDot.propTypes = { status: PropTypes.oneOf(['live', 'draft', 'disabled']) }
9 | StatusDot.defaultProps = { status: 'disabled' }
10 |
11 | export default StatusDot
12 |
--------------------------------------------------------------------------------
/app/components/Table/Cell.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { truncate } from 'utils/helpers'
4 |
5 | const Cell = ({ column, children }) => (
6 |
7 | {typeof children === 'string' ? truncate(children, 20) : children.component}
8 |
9 | )
10 |
11 | Cell.propTypes = {
12 | children: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
13 | column: PropTypes.string.isRequired
14 | }
15 | Cell.defaultProps = { children: '-' }
16 |
17 | export default Cell
18 |
--------------------------------------------------------------------------------
/app/components/Table/THead.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import classnames from 'classnames'
4 | import p from 'utils/prettyNames'
5 | import { truncate } from 'utils/helpers'
6 |
7 | const THead = ({ sortBy, column, direction, has, onClick, shouldTruncate }) => {
8 | const btnClass = classnames(
9 | 'table__header__btn',
10 | { 'is-active': sortBy === column },
11 | { desc: sortBy === column && direction === 'DESC' },
12 | { asc: sortBy === column && direction === 'ASC' }
13 | )
14 |
15 | const label = p[column] || column
16 |
17 | if (has) return
18 | return (
19 |
20 | {shouldTruncate ? truncate(label) : label}
24 |
25 | )
26 | }
27 |
28 | THead.propTypes = {
29 | sortBy: PropTypes.string.isRequired,
30 | column: PropTypes.string.isRequired,
31 | direction: PropTypes.oneOf(['ASC', 'DESC']).isRequired,
32 | has: PropTypes.bool.isRequired,
33 | onClick: PropTypes.func.isRequired,
34 | shouldTruncate: PropTypes.bool.isRequired
35 | }
36 |
37 | export default THead
38 |
--------------------------------------------------------------------------------
/app/components/TitleBar/TitleBar.scss:
--------------------------------------------------------------------------------
1 | .title-bar {
2 | display: flex;
3 | align-items: center;
4 | justify-content: space-between;
5 | padding: 1em 2em;
6 | border-bottom: 1px solid $gray-300;
7 | flex-shrink: 0;
8 | background-color: $gray-000;
9 |
10 | @include media($on-mobile) {
11 | padding: 0.5em 1em;
12 | }
13 |
14 | &__title {
15 | margin: 1.18px 0;
16 | color: $orange;
17 | font-size: 1.6rem;
18 | font-weight: 500;
19 |
20 | @include media($on-mobile) {
21 | margin: 5px 0;
22 | font-size: 1.2rem;
23 | }
24 | }
25 |
26 | &__children {
27 | display: flex;
28 |
29 | .btn + .btn { margin-left: 0.5em; }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/components/TitleBar/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { truncate, setTitle } from 'utils/helpers'
4 | import './TitleBar.scss'
5 |
6 | export default class TitleBar extends Component {
7 | static propTypes = {
8 | title: PropTypes.string.isRequired,
9 | setTitle: PropTypes.bool,
10 | children: PropTypes.any
11 | }
12 |
13 | static defaultProps = {
14 | children: null,
15 | setTitle: true
16 | }
17 |
18 | componentDidMount () {
19 | if (this.props.setTitle) setTitle(this.props.title)
20 | }
21 | componentWillUnmount () {
22 | if (this.props.setTitle) setTitle()
23 | }
24 |
25 | render () {
26 | const { title, children } = this.props
27 | return (
28 |
29 | {truncate(title, 40)}
30 |
31 | {children}
32 |
33 |
34 | )
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/components/Toast/Toast.scss:
--------------------------------------------------------------------------------
1 | .toasts {
2 | position: absolute;
3 | right: 0;
4 | bottom: 1em;
5 | overflow: hidden;
6 | padding-right: 0.8em;
7 | z-index: 9999;
8 |
9 | &__toast {
10 | display: flex;
11 | align-items: center;
12 | height: 3.4em;
13 | padding: 0 1em;
14 | border-radius: 3px;
15 | border: 1px solid $gray-900;
16 | background-color: $gray-700;
17 | color: white;
18 | font-size: $size0;
19 | box-shadow: $shadow1;
20 | transform: translateX(0);
21 | will-change: height;
22 | transition:
23 | height 200ms 300ms,
24 | transform 300ms,
25 | opacity 300ms;
26 | transition-timing-function: $enter;
27 |
28 | &--entering { transform: translateX(calc(100% + 0.6em)); }
29 |
30 | & + & {
31 | margin-top: 0.4em;
32 | }
33 |
34 | &--success {
35 | border-color: darken($green, 10%);
36 | background-color: $green;
37 | }
38 |
39 | &--error {
40 | border-color: darken($red, 10%);
41 | background-color: $red;
42 | }
43 |
44 | &--leaving {
45 | height: 0;
46 | opacity: 0;
47 | transform: translateX(calc(100% + 0.6em));
48 | transition-timing-function: $exit;
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/containers/Aside/Aside.scss:
--------------------------------------------------------------------------------
1 | .aside {
2 | align-self: flex-start;
3 | position: relative;
4 | box-sizing: border-box;
5 | width: 200px;
6 | padding: 2em;
7 | border-radius: 3px;
8 | border: 1px solid $gray-300;
9 | margin: 2em 2em 0 1em;
10 | background-color: $gray-000;
11 |
12 | @include media($on-mobile) {
13 | width: calc(100% - 2em);
14 | padding: 1em;
15 | margin: 1em;
16 | order: -1;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/containers/Empty/Empty.scss:
--------------------------------------------------------------------------------
1 | .empty {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | width: 100%;
6 | height: 100%;
7 |
8 | &__inner {
9 | display: flex;
10 | flex-direction: column;
11 | align-items: center;
12 | justify-content: center;
13 | line-height: 1.4;
14 |
15 | svg {
16 | margin-bottom: 1em;
17 | fill: $gray-500;
18 | }
19 |
20 | a { color: $orange; }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/containers/Empty/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import Icon from 'utils/icons'
4 | import './Empty.scss'
5 |
6 | export default class Empty extends Component {
7 | static propTypes = { children: PropTypes.any.isRequired }
8 |
9 | render () {
10 | const { children } = this.props
11 |
12 | return (
13 |
14 |
15 |
16 | {children}
17 |
18 |
19 | )
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/containers/ErrorContainer/ErrorContainer.scss:
--------------------------------------------------------------------------------
1 | .error-page {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | width: 100%;
6 | height: 100%;
7 |
8 | &__inner {
9 | display: flex;
10 | align-items: center;
11 | padding: 2em;
12 | max-width: 300px;
13 | border-radius: 5px;
14 | border: 1px solid $gray-500;
15 | flex-direction: column;
16 | background-color: $gray-100;
17 | font-size: $size0;
18 |
19 | a {
20 | color: $orange;
21 | text-decoration: none;
22 |
23 | &:hover {
24 | text-decoration: underline;
25 | }
26 | }
27 |
28 | svg { fill: $gray-500; margin-bottom: 1em; }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/containers/ErrorContainer/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { Link } from 'react-router-dom'
3 | import { getUrlParameter } from 'utils/helpers'
4 | import Icon from 'utils/icons'
5 | import './ErrorContainer.scss'
6 |
7 | export default class ErrorContainer extends Component {
8 | componentDidMount () { document.body.classList.add('body--grey') }
9 | componentWillUnmount () { document.body.classList.remove('body--grey') }
10 |
11 | render () {
12 | const reason = getUrlParameter('r')
13 | const page = getUrlParameter('p')
14 | const template = getUrlParameter('t')
15 |
16 | let str
17 |
18 | /* eslint-disable max-len */
19 | switch (reason) {
20 | case 'no-template':
21 | str = The requested template {template}.njk was not found when the {page} page was requested.
22 | break
23 | case 'no-html':
24 | str = There was an error when compiling the template for the {page} page was requested.
25 | break
26 | case 'no-homepage':
27 | str = Your website does not have a homepage yet! You can create one by signing in to the admin dashboard.
28 | break
29 | default:
30 | str = Unknown error!
31 | }
32 | /* eslint-enable max-len */
33 |
34 | return (
35 |
36 |
37 |
38 | {str}
39 |
40 |
41 | )
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/containers/FieldLayout/FieldSource.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { DragSource } from 'react-dnd'
4 | import classnames from 'classnames'
5 | import source from './utils/source'
6 | import { collectSource } from './utils/collect'
7 | import c from './utils/constants'
8 |
9 | class FieldSource extends Component {
10 | static propTypes = {
11 | connectDragSource: PropTypes.func.isRequired,
12 | isDragging: PropTypes.bool.isRequired,
13 | field: PropTypes.object.isRequired,
14 | disabled: PropTypes.bool
15 | }
16 |
17 | static defaultProps = {
18 | disabled: false
19 | }
20 |
21 | render () {
22 | const { isDragging, connectDragSource, field, disabled } = this.props
23 | const { title } = field
24 | const classes = classnames(
25 | 'field-layout__fields__field',
26 | { 'is-disabled': disabled },
27 | { 'is-dragging': isDragging }
28 | )
29 |
30 | const comp = (
31 |
32 | {title}
33 |
34 | )
35 |
36 | if (disabled) return comp
37 |
38 | return connectDragSource(comp)
39 | }
40 | }
41 |
42 | export default DragSource(c.FIELD, source, collectSource)(FieldSource)
43 |
--------------------------------------------------------------------------------
/app/containers/FieldLayout/FieldTarget.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { DropTarget } from 'react-dnd'
4 | import classnames from 'classnames'
5 | import target from './utils/target'
6 | import { collectTarget } from './utils/collect'
7 | import c from './utils/constants'
8 | import FieldTargetCard from './FieldTargetCard'
9 |
10 | class FieldTarget extends Component {
11 | static propTypes = {
12 | isOver: PropTypes.bool.isRequired,
13 | connectDropTarget: PropTypes.func.isRequired,
14 | removeField: PropTypes.func.isRequired,
15 | sortField: PropTypes.func.isRequired,
16 | canDrop: PropTypes.bool.isRequired,
17 | fields: PropTypes.array.isRequired
18 | }
19 |
20 | render () {
21 | const {
22 | isOver,
23 | canDrop,
24 | connectDropTarget,
25 | fields,
26 | removeField,
27 | sortField
28 | } = this.props
29 |
30 | const classes = classnames(
31 | 'field-layout__target',
32 | { 'can-drop': isOver && canDrop }
33 | )
34 |
35 | return connectDropTarget(
36 |
37 | {fields.map((field, i) => (
38 |
45 | ))}
46 |
47 | )
48 | }
49 | }
50 |
51 | export default DropTarget(c.FIELD, target, collectTarget)(FieldTarget)
52 |
--------------------------------------------------------------------------------
/app/containers/FieldLayout/utils/collect.js:
--------------------------------------------------------------------------------
1 | export function collectTarget (connect, monitor) {
2 | return {
3 | connectDropTarget: connect.dropTarget(),
4 | isOver: monitor.isOver(),
5 | isOverCurrent: monitor.isOver({ shallow: true }),
6 | canDrop: monitor.canDrop()
7 | }
8 | }
9 |
10 | export function collectSource (connect, monitor) {
11 | return {
12 | connectDragSource: connect.dragSource(),
13 | isDragging: monitor.isDragging()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/containers/FieldLayout/utils/constants.js:
--------------------------------------------------------------------------------
1 | const ItemTypes = {
2 | FIELD: 'field'
3 | }
4 |
5 | export default ItemTypes
6 |
--------------------------------------------------------------------------------
/app/containers/FieldLayout/utils/source.js:
--------------------------------------------------------------------------------
1 | const fieldSource = {
2 | beginDrag (props) {
3 | return {
4 | index: props.index,
5 | field: props.field,
6 | isNew: props.isNew
7 | }
8 | }
9 | }
10 |
11 | export default fieldSource
12 |
--------------------------------------------------------------------------------
/app/containers/FieldLayout/utils/target.js:
--------------------------------------------------------------------------------
1 | const target = {
2 | canDrop () {
3 | return true
4 | },
5 |
6 | hover (props, monitor, component) {
7 | const item = monitor.getItem()
8 | if (!component.targ || item.isNew) return
9 |
10 | const dragIndex = item.index
11 | const hoverIndex = props.index
12 |
13 | if (dragIndex === hoverIndex) return
14 |
15 | const hoverBoundingRect = component.targ.getBoundingClientRect()
16 | const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2
17 | const clientOffset = monitor.getClientOffset()
18 | const hoverClientY = clientOffset.y - hoverBoundingRect.top
19 |
20 | // Dragging downwards
21 | if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return
22 |
23 | // Dragging upwards
24 | if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) return
25 |
26 | component.props.sortField(dragIndex, hoverIndex)
27 |
28 | item.index = hoverIndex // eslint-disable-line no-param-reassign
29 | },
30 |
31 | drop (props, monitor, component) {
32 | const item = monitor.getItem()
33 | if (item.isNew && component.props.addField) {
34 | component.props.addField(item.field, monitor.getItem().index)
35 | }
36 | }
37 | }
38 |
39 | export default target
40 |
--------------------------------------------------------------------------------
/app/containers/Footer/Footer.scss:
--------------------------------------------------------------------------------
1 | .footer {
2 | padding: 2em;
3 | border-top: 1px solid $gray-300;
4 | text-align: center;
5 |
6 | @include media($on-mobile) {
7 | padding: 1em;
8 | }
9 |
10 | &__made-with {
11 | color: $blurple;
12 | opacity: 0.5;
13 | filter: grayscale(100);
14 | font-size: 0.8rem;
15 | transition: filter 150ms $standard, opacity 150ms $standard;
16 |
17 | &:hover {
18 | opacity: 1;
19 | filter: none;
20 | }
21 |
22 | a { color: inherit; }
23 | }
24 |
25 | &__emoji {
26 | width: 1.6em;
27 | padding: 0;
28 | border: none;
29 | margin: 0;
30 | background: none;
31 | cursor: pointer;
32 | transition: transform 150ms $standard;
33 | outline: none;
34 |
35 | &:hover { transform: scale(1.2); }
36 | }
37 |
38 | .flint-logo {
39 | width: 80px;
40 | height: 40px;
41 | margin-top: 1em;
42 | fill: $gray-400;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/containers/Footer/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { shuffle } from 'utils/helpers'
3 | import FlintLogo from 'components/FlintLogo'
4 | import { version } from '../../../package.json'
5 | import './Footer.scss'
6 |
7 | const baseEmojis = ['🍍', '🕑', '🎏', '🔥', '🦄', '🍑', '🔑', '🙌', '❤️']
8 |
9 | export default class Footer extends Component {
10 | state = { emojis: shuffle(baseEmojis), i: 0 }
11 |
12 | randomizeEmoji () {
13 | const { emojis, i } = this.state
14 | this.setState({ i: i < emojis.length - 1 ? i + 1 : 0 })
15 | }
16 |
17 | render () {
18 | const { emojis, i } = this.state
19 | const btn = this.randomizeEmoji()}>{emojis[i]}
20 | return (
21 |
27 | )
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/containers/LoginContainer/LoginContainer.scss:
--------------------------------------------------------------------------------
1 | .login {
2 | display: flex;
3 | overflow-y: auto;
4 | width: 100%;
5 | height: 100%;
6 | flex-direction: column;
7 | align-items: center;
8 | justify-content: center;
9 |
10 | .flint-logo-wrapper {
11 | margin-bottom: 2em;
12 |
13 | .flint-logo { fill: $gray-500; }
14 | }
15 |
16 | &__inner {
17 | @extend %card;
18 | width: 420px;
19 | max-width: 90%;
20 | }
21 |
22 | &__title {
23 | display: block;
24 | width: 100%;
25 | margin-bottom: 1em;
26 | font-size: $size2;
27 | text-align: center;
28 | }
29 |
30 | &__img {
31 | display: block;
32 | max-width: 420px;
33 | max-height: 200px;
34 | margin: 1em auto;
35 | }
36 |
37 | .input, .btn {
38 | display: block;
39 | width: 100%;
40 | }
41 |
42 | .btn { padding: 1em; }
43 |
44 | .input { margin-bottom: 1em; }
45 |
46 | &__forgot {
47 | margin-top: 1em;
48 | color: $gray-500;
49 | font-size: $size0;
50 |
51 | &:hover {
52 | color: $gray-700;
53 | }
54 |
55 | + .flint-logo-wrapper {
56 | margin-top: 2em;
57 | margin-bottom: 0;
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/containers/LoginContainer/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Link } from 'react-router-dom'
4 | import { get } from 'axios'
5 | import FlintLogo from 'components/FlintLogo'
6 | import './LoginContainer.scss'
7 |
8 | export default class LoginContainer extends Component {
9 | static propTypes = {
10 | children: PropTypes.object.isRequired,
11 | forgot: PropTypes.bool
12 | }
13 |
14 | static defaultProps = {
15 | forgot: true
16 | }
17 |
18 | state = { siteLogo: null, isFetching: true }
19 |
20 | componentWillMount () { document.body.classList.add('body--grey') }
21 |
22 | componentDidMount () {
23 | get('/admin/api/site').then(({ data }) => {
24 | this.setState({ siteLogo: data.siteLogo, isFetching: false })
25 | })
26 | }
27 |
28 | componentWillUnmount () { document.body.classList.remove('body--grey') }
29 |
30 | render () {
31 | const { siteLogo, isFetching } = this.state
32 | const { children, forgot } = this.props
33 |
34 | if (isFetching) return null
35 |
36 | return (
37 |
38 | {siteLogo ?
:
}
39 | {children}
40 | {forgot &&
Forgot your password?}
41 | {siteLogo &&
}
42 |
43 | )
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/containers/Main/Main.scss:
--------------------------------------------------------------------------------
1 | .main {
2 | display: flex;
3 | overflow-y: auto;
4 | width: 100%;
5 | height: 100%;
6 | flex-wrap: wrap;
7 | }
8 |
--------------------------------------------------------------------------------
/app/containers/Page/Page.scss:
--------------------------------------------------------------------------------
1 | .page {
2 | display: flex;
3 | overflow-y: auto;
4 | width: calc(100% - 220px);
5 | min-height: 100%;
6 | flex-direction: column;
7 | flex-grow: 2;
8 |
9 | @include media($on-mobile) {
10 | width: 100%;
11 | flex: none;
12 | }
13 |
14 | .page__inner {
15 | box-sizing: border-box;
16 | padding: 2em;
17 | flex-grow: 2;
18 |
19 | @include media($on-mobile) {
20 | padding: 1em;
21 | }
22 | }
23 | }
24 |
25 | .content {
26 | display: flex;
27 | flex: 1 0 auto;
28 |
29 | @include media($on-mobile) {
30 | flex-direction: column;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/containers/Page/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import classnames from 'classnames'
4 | import Breadcrumbs from 'components/Breadcrumbs'
5 | import Footer from 'containers/Footer'
6 | import './Page.scss'
7 |
8 | export default class Page extends Component {
9 | static propTypes = {
10 | children: PropTypes.oneOfType([
11 | PropTypes.arrayOf(PropTypes.node),
12 | PropTypes.node
13 | ]).isRequired,
14 | links: PropTypes.arrayOf(PropTypes.shape({
15 | label: PropTypes.string.isRequired,
16 | path: PropTypes.string.isRequired
17 | })),
18 | name: PropTypes.string.isRequired,
19 | onSubmit: PropTypes.func
20 | };
21 |
22 | static defaultProps = {
23 | links: null,
24 | onSubmit: null
25 | };
26 |
27 | render () {
28 | const { name, children, links, onSubmit } = this.props
29 | const classes = classnames(
30 | 'page',
31 | `page--${name}`,
32 | { 'page--form': onSubmit },
33 | { 'has-breadcrumbs': links && links.length > 0 }
34 | )
35 |
36 | if (onSubmit) {
37 | return (
38 |
44 | )
45 | }
46 |
47 | return (
48 |
49 | {links && }
50 | {children}
51 |
52 |
53 |
54 | )
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/index.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | FlintCMS Dashboard
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
You must enable JavaScript to use the Flint admin dashboard.
22 |
Need help?
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/main.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/first */
2 |
3 | import './main.scss'
4 | import React from 'react'
5 | import { render } from 'react-dom'
6 | import { Provider, connect } from 'react-redux'
7 | import { withRouter } from 'react-router'
8 | import { Switch, Route } from 'react-router-dom'
9 | import { ConnectedRouter } from 'react-router-redux'
10 | import Main from 'containers/Main'
11 | import Login from 'views/Auth/Login'
12 | import SetPassword from 'views/Auth/SetPassword'
13 | import ForgotPassword from 'views/Auth/ForgotPassword'
14 | import ErrorContainer from 'containers/ErrorContainer'
15 | import Install from 'views/Auth/Install'
16 | import store, { history } from 'utils/store'
17 |
18 | export default function mapStateToProps (state) {
19 | return { ...state }
20 | }
21 |
22 | const App = withRouter(connect(mapStateToProps)(Main))
23 |
24 | const appWrapper = (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | } />
34 |
35 |
36 |
37 | )
38 |
39 | render(appWrapper, document.querySelector('.mount'))
40 |
--------------------------------------------------------------------------------
/app/main.scss:
--------------------------------------------------------------------------------
1 | @import './scss/normalize';
2 |
3 | html, body, .mount {
4 | height: 100%;
5 | }
6 |
7 | body {
8 | overflow-y: hidden;
9 | background-color: $gray-000;
10 | font-family: $font;
11 |
12 | &.body--grey { background-color: $gray-300; }
13 | }
14 |
15 | .form-element {
16 | & + & {
17 | margin-top: 1.4em;
18 | }
19 |
20 | &--required {
21 | .input__label::after {
22 | content: '*';
23 | margin-left: 0.4em;
24 | color: $red;
25 | }
26 | }
27 | }
28 |
29 | .content { background-color: white; }
30 |
31 | .no-script {
32 | display: flex;
33 | align-items: center;
34 | justify-content: center;
35 | width: 100%;
36 | height: 100%;
37 | flex-direction: column;
38 | background-color: $gray-300;
39 |
40 | h1 {
41 | max-width: 300px;
42 | padding: 2em;
43 | border-radius: 3px;
44 | border: 1px solid $gray-500;
45 | background-color: white;
46 | font-weight: 500;
47 | font-size: $size2;
48 | }
49 |
50 | a {
51 | color: $orange;
52 | font-size: $size0;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/app/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "FlintCMS",
3 | "short_name": "FlintCMS",
4 | "start_url": "http://localhost:4000",
5 | "icons": [
6 | {
7 | "src": "assets/android-chrome-192x192.png",
8 | "sizes": "192x192",
9 | "type": "image/png"
10 | },
11 | {
12 | "src": "assets/android-chrome-256x256.png",
13 | "sizes": "256x256",
14 | "type": "image/png"
15 | }
16 | ],
17 | "theme_color": "#fe6300",
18 | "background_color": "#353739",
19 | "display": "standalone"
20 | }
--------------------------------------------------------------------------------
/app/reducers/plugins.js:
--------------------------------------------------------------------------------
1 | import { REQUEST_PLUGINS, RECEIVE_PLUGINS } from 'actions/pluginActions'
2 |
3 | export default function plugins (state = {}, action) {
4 | switch (action.type) {
5 | case REQUEST_PLUGINS: {
6 | return {
7 | ...state,
8 | isFetching: true,
9 | didInvalidate: false
10 | }
11 | }
12 |
13 | case RECEIVE_PLUGINS: {
14 | return {
15 | ...state,
16 | plugins: action.plugins,
17 | isFetching: false,
18 | didInvalidate: false,
19 | lastUpdated: action.receivedAt
20 | }
21 | }
22 |
23 | default:
24 | return state
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/reducers/site.js:
--------------------------------------------------------------------------------
1 | import {
2 | REQUEST_SITE,
3 | RECEIVE_SITE,
4 | UPDATE_SITE
5 | } from 'actions/siteActions'
6 |
7 | export default function site (state = {}, action) {
8 | switch (action.type) {
9 | case REQUEST_SITE: {
10 | return {
11 | ...state,
12 | isFetching: true,
13 | didInvalidate: false
14 | }
15 | }
16 |
17 | case RECEIVE_SITE: {
18 | return {
19 | ...state,
20 | ...action.site,
21 | isFetching: false,
22 | didInvalidate: false,
23 | lastUpdated: action.receivedAt
24 | }
25 | }
26 |
27 | case UPDATE_SITE: {
28 | return {
29 | ...state,
30 | ...action.updateSite
31 | }
32 | }
33 |
34 | default:
35 | return state
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/reducers/ui.js:
--------------------------------------------------------------------------------
1 | import {
2 | NEW_TOAST,
3 | DELETE_TOAST,
4 | OPEN_MODAL,
5 | CLOSE_MODALS
6 | } from 'actions/uiActions'
7 |
8 | export default function ui (state = {}, action) {
9 | switch (action.type) {
10 | case NEW_TOAST: {
11 | const { message, style, dateCreated } = action
12 | return {
13 | ...state,
14 | toasts: [
15 | {
16 | message,
17 | style,
18 | dateCreated
19 | },
20 | ...state.toasts
21 | ]
22 | }
23 | }
24 |
25 | case DELETE_TOAST: {
26 | const toastIndex = state.toasts.findIndex(toast => toast.dateCreated === action.dateCreated)
27 | if (toastIndex === -1) return state
28 |
29 | return {
30 | ...state,
31 | toasts: [
32 | ...state.toasts.slice(0, toastIndex),
33 | ...state.toasts.slice(toastIndex + 1)
34 | ]
35 | }
36 | }
37 |
38 | case OPEN_MODAL: {
39 | return {
40 | ...state,
41 | modalIsOpen: true,
42 | currentModal: action.currentModal
43 | }
44 | }
45 |
46 | // Close any active modal
47 | case CLOSE_MODALS: {
48 | return {
49 | ...state,
50 | modalIsOpen: false,
51 | currentModal: null
52 | }
53 | }
54 |
55 | default:
56 | return state
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/app/reducers/user.js:
--------------------------------------------------------------------------------
1 | import {
2 | REQUEST_USER,
3 | RECEIVE_USER
4 | } from 'actions/userActions'
5 |
6 | export default function user (state = {}, action) {
7 | switch (action.type) {
8 | case REQUEST_USER: {
9 | return {
10 | ...state,
11 | isFetching: true,
12 | didInvalidate: false
13 | }
14 | }
15 |
16 | case RECEIVE_USER: {
17 | return {
18 | ...state,
19 | ...action.user,
20 | isFetching: false,
21 | didInvalidate: false,
22 | lastUpdated: action.receivedAt
23 | }
24 | }
25 |
26 | default:
27 | return state
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/scss/tools/mixins.scss:
--------------------------------------------------------------------------------
1 | // stylelint-disable no-browser-hacks, at-rule-empty-line-before
2 |
3 | @mixin box-shadow($depth) {
4 | box-shadow: 0 #{2 * $depth}px #{4 * $depth}px rgba(black, 0.1 * $depth);
5 | }
6 |
7 | // Media-Queries
8 | $on-mobile: 768px;
9 | $on-tablet: 1024px;
10 | $on-laptop: 1500px;
11 |
12 | @mixin media($max-width) {
13 | @media screen and (max-width: $max-width) {
14 | @content;
15 | }
16 | }
17 |
18 | @mixin scrollbar($thumbColor, $trackColor, $trackWidth) {
19 | &::-webkit-scrollbar { width: $trackWidth; }
20 |
21 | &::-webkit-scrollbar-track { background-color: $trackColor; }
22 |
23 | &::-webkit-scrollbar-thumb {
24 | border-radius: $trackWidth / 2;
25 | background-color: $thumbColor;
26 | }
27 | }
28 |
29 | @mixin triangle($direction, $size, $color) {
30 | width: 0;
31 | height: 0;
32 | border-width: $size;
33 | border-style: solid;
34 | border-color: transparent;
35 |
36 | @if $direction == 'top' {
37 | border-bottom-color: $color;
38 | }
39 | @else if $direction == 'bottom' {
40 | border-top-color: $color;
41 | }
42 | @else if $direction == 'right' {
43 | border-left-color: $color;
44 | }
45 | @else if $direction == 'left' {
46 | border-right-color: $color;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/scss/tools/tools.scss:
--------------------------------------------------------------------------------
1 | @import './mixins';
2 | @import './variables';
3 |
--------------------------------------------------------------------------------
/app/utils/formatFields.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Formats fields by key/value pairs into a larger, more descriptive object
3 | * @param {Object} fields
4 | * @param {Object} stateFields
5 | *
6 | * @typedef {Object} FieldObject
7 | * @property {String} fieldId - Mongo ID of the Field
8 | * @property {String} handle - Slug of the Field's title
9 | * @property {Any} value - the value for this field in the Page
10 | *
11 | * @returns {FieldObject}
12 | */
13 | async function formatFields (fields, stateFields) {
14 | if (fields.length <= 0) return fields
15 |
16 | const options = await Object.keys(fields).map((key) => {
17 | const fieldId = stateFields.find(field => key === field.handle)._id
18 | return {
19 | fieldId,
20 | handle: key,
21 | value: fields[key]
22 | }
23 | })
24 | return options
25 | }
26 |
27 | export default formatFields
28 |
--------------------------------------------------------------------------------
/app/utils/getUserPermissions.js:
--------------------------------------------------------------------------------
1 | import store from 'utils/store'
2 |
3 | export default function getUserPermissions () {
4 | return store.getState().user.usergroup.permissions
5 | }
6 |
--------------------------------------------------------------------------------
/app/utils/graphFetcher.js:
--------------------------------------------------------------------------------
1 | import { post } from 'axios'
2 | import store from './store'
3 |
4 | /**
5 | * Uses Axios to post to the GraphQL endpoint
6 | * @param {String} query
7 | * @param {Object} variables
8 | */
9 | export default function graphFetcher (query, variables = {}) {
10 | const { id } = store.getState().socket
11 | return post('/graphql', {
12 | query,
13 | variables,
14 | socket: id,
15 | withCredentials: true
16 | })
17 | }
18 |
--------------------------------------------------------------------------------
/app/utils/permissionsQuery.js:
--------------------------------------------------------------------------------
1 | import permissions from '../../server/utils/permissions.json'
2 |
3 | export default `
4 | permissions {
5 | ${Object.keys(permissions).map(key => `${key} {\n${permissions[key].map(({ name }) => `\t${name}`).join('\n')}\n}`).join('\n')}
6 | }
7 | `
8 |
--------------------------------------------------------------------------------
/app/utils/prettyNames.js:
--------------------------------------------------------------------------------
1 | const names = {
2 | title: 'Title',
3 | slug: 'Slug',
4 | filename: 'File Name',
5 | dateCreated: 'Date Created',
6 | author: 'Author',
7 | delete: 'Delete',
8 | size: 'File Size',
9 | username: 'Username',
10 | name: 'Name',
11 | type: 'Type',
12 | handle: 'Handle'
13 | }
14 |
15 | export default names
16 |
--------------------------------------------------------------------------------
/app/utils/renderOption.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Fields from 'components/Fields'
3 |
4 | /**
5 | * Returns a React component of the relevant Field
6 | * @param {Object} field - The field object
7 | * @param {Any} [value]
8 | * @param {Object} betterProps - Props to overwrite with
9 | */
10 | export default function renderOption (field, value, betterProps) {
11 | const fieldType = Fields[field.type]
12 |
13 | const props = {
14 | key: field._id,
15 | name: field.handle,
16 | label: field.title,
17 | instructions: field.instructions,
18 | defaultValue: value || (field.options ? field.options.defaultValue : ''),
19 | ...field.options,
20 | ...fieldType.props,
21 | ...betterProps
22 | }
23 |
24 | const Component = fieldType.component
25 |
26 | return
27 | }
28 |
--------------------------------------------------------------------------------
/app/utils/rootReducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import { routerReducer } from 'react-router-redux'
3 | import io from 'socket.io-client'
4 |
5 | import user from '../reducers/user'
6 | import users from '../reducers/users'
7 | import usergroups from '../reducers/usergroups'
8 | import entries from '../reducers/entries'
9 | import sections from '../reducers/sections'
10 | import fields from '../reducers/fields'
11 | import assets from '../reducers/assets'
12 | import site from '../reducers/site'
13 | import ui from '../reducers/ui'
14 | import plugins from '../reducers/plugins'
15 | import pages from '../reducers/pages'
16 |
17 | const socket = io()
18 |
19 | // Combine reducers into one, easily ingestible file
20 | // which is then imported into the Redux store
21 | // ----
22 | // Create an empty object to avoid extra reducers
23 | // recipes: (state = {}) => state,
24 | const rootReducer = combineReducers({
25 | user,
26 | users,
27 | usergroups,
28 | entries,
29 | sections,
30 | fields,
31 | assets,
32 | ui,
33 | site,
34 | pages,
35 | plugins,
36 | socket: (state = socket) => state,
37 | routing: routerReducer
38 | })
39 |
40 | export default rootReducer
41 |
--------------------------------------------------------------------------------
/app/utils/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux'
2 | import thunk from 'redux-thunk'
3 | import { routerMiddleware } from 'react-router-redux'
4 | import createHistory from 'history/createBrowserHistory'
5 | import rootReducer from './rootReducer'
6 |
7 | const defaultState = {
8 | user: { isFetching: true },
9 | users: { isFetching: true },
10 | entries: { isFetching: true },
11 | sections: { isFetching: true },
12 | assets: { isFetching: true },
13 | site: { isFetching: true },
14 | plugins: { isFetching: true },
15 | pages: { isFetching: true },
16 | ui: {
17 | toasts: [],
18 | modalIsOpen: false
19 | }
20 | }
21 | export const history = createHistory({ basename: '/admin' })
22 | const routerMiddle = routerMiddleware(history)
23 |
24 | const enhancers = compose(
25 | applyMiddleware(thunk, routerMiddle),
26 | window.devToolsExtension ? window.devToolsExtension() : f => f
27 | )
28 |
29 | const store = createStore(
30 | rootReducer,
31 | defaultState,
32 | enhancers
33 | )
34 |
35 | export default store
36 |
--------------------------------------------------------------------------------
/app/utils/validateFields.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Fields from 'components/Fields'
3 | import { newToast } from 'actions/uiActions'
4 | import store from './store'
5 |
6 | /**
7 | * Validates an object of field/value pairs using the
8 | * component class' own validate function.
9 | * @param {Object} fields - Object of fields
10 | * @returns {String[]} - An array of field handles
11 | */
12 | function validateFields (fields) {
13 | const { dispatch, getState } = store
14 | const { fields: f } = getState()
15 | // Validates fields using the appropriate validate method
16 | const v = Object.keys(fields).filter((fieldHandle) => {
17 | const { type } = f.fields.find(fld => fld.handle === fieldHandle)
18 | if (Fields[type].component.validate) {
19 | return !Fields[type].component.validate(fields[fieldHandle])
20 | }
21 | return false
22 | })
23 |
24 | if (v.length !== 0) {
25 | v.forEach((invalidField) => {
26 | const fieldTitle = f.fields.find(fld => fld.handle === invalidField).title
27 | dispatch(newToast({
28 | message: {fieldTitle} received an invalid value. ,
29 | style: 'error'
30 | }))
31 | })
32 | return v
33 | }
34 |
35 | return []
36 | }
37 |
38 | export default validateFields
39 |
--------------------------------------------------------------------------------
/app/views/404/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Page from 'containers/Page'
3 |
4 | export default class FourOhFour extends Component {
5 | render () {
6 | return (
7 |
8 |
9 |
10 | Page not found! Sorry!
11 |
12 |
13 |
14 | )
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/views/Auth/Install/Install.scss:
--------------------------------------------------------------------------------
1 | .install {
2 | display: flex;
3 | overflow-y: auto;
4 | width: 100%;
5 | height: 100%;
6 | flex-direction: column;
7 | align-items: center;
8 | justify-content: center;
9 |
10 | .flint-logo-wrapper {
11 | .flint-logo { fill: $gray-500; }
12 | }
13 |
14 | &__inner {
15 | display: flex;
16 | align-items: center;
17 | width: 700px;
18 | max-width: 90%;
19 |
20 | @include media($on-mobile) {
21 | flex-direction: column;
22 | }
23 | }
24 |
25 | &__col {
26 | display: flex;
27 | box-sizing: border-box;
28 | align-items: center;
29 | width: 50%;
30 | padding-right: 1em;
31 | flex-direction: column;
32 | color: $gray-700;
33 | text-align: center;
34 |
35 | @include media($on-mobile) {
36 | width: 100%;
37 | }
38 | }
39 |
40 | &__form {
41 | @extend %card;
42 | flex: 2;
43 |
44 | @include media($on-mobile) {
45 | width: 100%;
46 | }
47 | }
48 |
49 | &__title {
50 | display: block;
51 | width: 100%;
52 | margin-bottom: 1em;
53 | font-size: $size3;
54 | text-align: center;
55 | }
56 |
57 | .btn { width: 100%; }
58 |
59 | // .form-element + .form-element { margin-top: 0.5em; }
60 | }
61 |
--------------------------------------------------------------------------------
/app/views/Home/Home.scss:
--------------------------------------------------------------------------------
1 | .page--home {
2 | .content { background-color: transparent; }
3 |
4 | .title-bar { border-bottom: none; }
5 |
6 | .page__inner {
7 | display: flex;
8 | }
9 |
10 | }
11 |
12 | .home {
13 | &--empty {
14 | display: flex;
15 | align-items: center;
16 | justify-content: center;
17 | width: 100%;
18 | height: 100%;
19 | flex-direction: column;
20 | text-align: center;
21 |
22 | a { color: $orange; }
23 | }
24 |
25 | &__column {
26 | @extend %card;
27 | width: 32%;
28 |
29 | @include media($on-mobile) { width: 100%; }
30 |
31 | .subtitle {
32 | margin-top: 0;
33 | }
34 | }
35 |
36 | &__list {
37 | list-style-type: none;
38 | padding: 0;
39 | margin: 0;
40 |
41 | &-item {
42 | @extend %card;
43 | padding: 0;
44 |
45 | &:hover {
46 | box-shadow: $shadow2;
47 | }
48 |
49 | a {
50 | display: block;
51 | padding: 1.2em;
52 | color: black;
53 | font-size: $size0;
54 | text-decoration: none;
55 | }
56 |
57 | h4 { margin: 0 0 0.5em; font-size: 1.1em; }
58 |
59 | &__meta {
60 | display: flex;
61 | justify-content: space-between;
62 | }
63 |
64 | &__date {
65 | margin-left: auto;
66 | color: $gray-500;
67 | }
68 |
69 | & + & {
70 | margin-top: 1em;
71 | }
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/app/views/Settings/Logs/Logs.scss:
--------------------------------------------------------------------------------
1 | .logs {
2 | &__section {
3 | width: 100%;
4 |
5 | + & { margin-top: 1em; }
6 | }
7 |
8 | &__pre {
9 | overflow-x: auto;
10 | padding: 0.6em;
11 | max-height: 400px;
12 | border: 1px solid $gray-300;
13 | border-radius: 3px;
14 | font-size: 0.7rem;
15 | line-height: 1.8;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/views/Settings/Logs/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import Page from 'containers/Page'
3 | import TitleBar from 'components/TitleBar'
4 | import { get } from 'axios'
5 | import './Logs.scss'
6 |
7 | export default class Logs extends Component {
8 | static propTypes = {}
9 |
10 | state = { isFetching: true }
11 |
12 | componentDidMount () {
13 | get('/admin/api/logs')
14 | .then(({ data }) => {
15 | this.setState({ isFetching: false, data })
16 | })
17 | }
18 |
19 | render () {
20 | const { isFetching, data } = this.state
21 | if (isFetching) return null
22 |
23 | return (
24 |
25 |
26 |
27 |
28 |
29 |
30 | Flint Logs
31 |
32 |
33 | {data.flint.map(s => `${s}\n`)}
34 |
35 |
36 |
37 |
38 |
39 | HTTP Logs
40 |
41 |
42 | {data.http.map(s => `${s}\n`)}
43 |
44 |
45 |
46 |
47 |
48 |
49 | )
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/views/Settings/Plugins/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import Page from 'containers/Page'
4 | import TitleBar from 'components/TitleBar'
5 | import Table from 'components/Table'
6 |
7 | export default class Plugins extends Component {
8 | static propTypes = {
9 | plugins: PropTypes.shape({
10 | isFetching: PropTypes.bool.isRequired,
11 | plugins: PropTypes.arrayOf(PropTypes.shape({
12 | _id: PropTypes.string.isRequired,
13 | name: PropTypes.string.isRequired,
14 | icon: PropTypes.shape({
15 | path: PropTypes.string.isRequired,
16 | buffer: PropTypes.string.isRequired
17 | })
18 | }))
19 | }).isRequired
20 | }
21 |
22 | render () {
23 | const { plugins } = this.props
24 | const data = plugins.plugins.map(plugin => ({
25 | key: plugin._id,
26 | image: {
27 | sortBy: false,
28 | component:
29 | },
30 | title: plugin.title
31 | }))
32 |
33 | return (
34 |
35 |
36 |
41 |
42 | )
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/views/Settings/Styles/Styles.scss:
--------------------------------------------------------------------------------
1 | .page--styles .page__inner {
2 | display: flex;
3 | flex-direction: column;
4 | }
5 |
6 | .style-editor {
7 | &__editor {
8 | height: 100%;
9 | border: 1px solid $gray-300;
10 | border-radius: 3px;
11 |
12 | .ReactCodeMirror, .CodeMirror {
13 | height: 100%;
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | status:
3 | project:
4 | default:
5 | # Fail the status if coverage drops by >= 3%
6 | threshold: 3
7 | patch:
8 | default:
9 | threshold: 3
10 |
11 | comment:
12 | layout: diff
13 | require_changes: yes
14 |
--------------------------------------------------------------------------------
/config/constants.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | exports.browsers = [
4 | 'last 2 versions',
5 | 'ios_saf >= 8',
6 | 'ie >= 10',
7 | 'chrome >= 49',
8 | 'firefox >= 49',
9 | '> 1%'
10 | ]
11 |
12 | exports.resolve = {
13 | alias: {
14 | components: path.join(__dirname, '..', 'app', 'components'),
15 | containers: path.join(__dirname, '..', 'app', 'containers'),
16 | views: path.join(__dirname, '..', 'app', 'views'),
17 | actions: path.join(__dirname, '..', 'app', 'actions'),
18 | utils: path.join(__dirname, '..', 'app', 'utils'),
19 | assets: path.join(__dirname, '..', 'app', 'assets')
20 | }
21 | }
22 |
23 | exports.vendor = [
24 | 'react',
25 | 'react-dom',
26 | 'react-router',
27 | 'codemirror',
28 | 'react-codemirror',
29 | 'socket.io-client',
30 | 'socket.io-parser',
31 | 'engine.io-client',
32 | 'engine.io-parser',
33 | 'lodash',
34 | 'moment',
35 | 'draft-js',
36 | 'draft-js-export-html',
37 | 'draft-js-utils',
38 | 'react-color',
39 | 'react-redux',
40 | 'redux',
41 | 'immutability-helper',
42 | 'immutable',
43 | 'history',
44 | 'axios',
45 | 'react-dnd',
46 | 'react-dnd-html5-backend',
47 | 'core-js',
48 | 'react-rte'
49 | ]
50 |
--------------------------------------------------------------------------------
/dev.js:
--------------------------------------------------------------------------------
1 | const Flint = require('.')
2 |
3 | const flintServer = new Flint({
4 | enableCacheBusting: true
5 | }, true)
6 |
7 | flintServer.startServer()
8 |
--------------------------------------------------------------------------------
/server/apps/api.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 |
3 | const express = require('express')
4 |
5 | module.exports = (app, log) => {
6 | const api = express()
7 |
8 | api.use(require('./routes/assets')(app, log))
9 | api.use(require('./routes/site')(log))
10 | api.use(require('./routes/logs')(log))
11 |
12 | log.info('[App: API] initialized.')
13 | return api
14 | }
15 |
--------------------------------------------------------------------------------
/server/apps/graphql.js:
--------------------------------------------------------------------------------
1 | const graphqlHTTP = require('express-graphql')
2 | const h = require('../utils/helpers')
3 | const express = require('express')
4 | const schema = require('../graphql')
5 | const getUserPermissions = require('../utils/get-user-permissions')
6 | const emitSocketEvent = require('../utils/emit-socket-event')
7 | const events = require('../utils/events')
8 | const log = require('../utils/log')
9 |
10 | module.exports = (app, logger) => {
11 | const graphql = express()
12 | const io = app.get('io')
13 |
14 | graphql.use(h.loggedIn)
15 | graphql.use('/', graphqlHTTP(async req => ({
16 | schema,
17 | pretty: true,
18 | graphiql: global.FLINT.debugMode,
19 | rootValue: {
20 | io,
21 | req,
22 | user: req.user,
23 | perms: await getUserPermissions(req.user._id),
24 | events,
25 | socketEvent: (event, payload) => emitSocketEvent({ io, req }, event, payload),
26 | log
27 | }
28 | })))
29 |
30 | logger.info('[App: GraphQL] initialized.')
31 |
32 | return graphql
33 | }
34 |
--------------------------------------------------------------------------------
/server/apps/routes/logs.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const path = require('path')
3 | const { promisify } = require('util')
4 | const fs = require('fs')
5 |
6 | const readFile = promisify(fs.readFile)
7 |
8 | const router = express.Router()
9 |
10 | module.exports = () => {
11 | router.get('/logs', async (req, res) => {
12 | const { logsPath } = global.FLINT
13 | const flintLog = await readFile(path.join(logsPath, 'flint.log'), { encoding: 'utf-8' })
14 | const httpLog = await readFile(path.join(logsPath, 'http-requests.log'), { encoding: 'utf-8' })
15 |
16 | const flint = flintLog.split('\n')
17 | const http = httpLog.split('\n')
18 |
19 | res.json({ flint, http })
20 | })
21 |
22 | return router
23 | }
24 |
--------------------------------------------------------------------------------
/server/apps/routes/site.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const express = require('express')
3 | const getLatestVersion = require('latest-version')
4 | const semverDiff = require('semver-diff')
5 | const pkg = require('../../../package.json')
6 |
7 | const router = express.Router()
8 | const Site = mongoose.model('Site')
9 |
10 | module.exports = () => {
11 | router.get('/site', (req, res) => {
12 | Site.findOne().select('siteLogo')
13 | .then(site => res.status(200).json(site))
14 | })
15 |
16 | router.get('/hasUpdate', async (req, res) => {
17 | const currentVersion = pkg.version
18 | const latestVersion = await getLatestVersion(pkg.name)
19 |
20 | res.json({ hasUpdate: semverDiff(currentVersion, latestVersion) !== null })
21 | })
22 |
23 | return router
24 | }
25 |
--------------------------------------------------------------------------------
/server/graphql/get-projection.js:
--------------------------------------------------------------------------------
1 | module.exports = function getProjection (fieldASTs) {
2 | return fieldASTs.fieldNodes[0].selectionSet.selections.reduce((projections, selection) =>
3 | Object.assign({}, projections, { [selection.name.value]: 1 }), {})
4 | }
5 |
--------------------------------------------------------------------------------
/server/graphql/index.js:
--------------------------------------------------------------------------------
1 | const { GraphQLObjectType, GraphQLSchema } = require('graphql')
2 |
3 | const mutations = require('./mutations')
4 | const queries = require('./queries')
5 |
6 | module.exports = new GraphQLSchema({
7 | query: new GraphQLObjectType({
8 | name: 'Query',
9 | fields: queries
10 | }),
11 | mutation: new GraphQLObjectType({
12 | name: 'Mutation',
13 | fields: mutations
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/server/graphql/mutations/assets/addAsset.js:
--------------------------------------------------------------------------------
1 | const { GraphQLNonNull } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { inputType, outputType } = require('../../types/Assets')
4 |
5 | const Asset = mongoose.model('Asset')
6 |
7 | module.exports = {
8 | type: new GraphQLNonNull(outputType),
9 | args: {
10 | data: {
11 | name: 'data',
12 | type: new GraphQLNonNull(inputType)
13 | }
14 | },
15 | async resolve ({ io, perms, events, socketEvent }, args) {
16 | if (perms && !perms.assets.canAddAssets) throw new Error('You do not have permission to add new assets.')
17 |
18 | const newAsset = new Asset(args.data)
19 | if (events) events.emit('pre-new-asset', newAsset)
20 |
21 | const savedAsset = await newAsset.save()
22 |
23 | /* istanbul ignore if */
24 | if (!savedAsset) throw new Error('There was a problem saving the asset.')
25 |
26 | if (events) events.emit('post-new-asset', savedAsset)
27 | if (socketEvent) socketEvent('new-asset', savedAsset)
28 | return savedAsset
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/server/graphql/mutations/assets/index.js:
--------------------------------------------------------------------------------
1 | const addAsset = require('./addAsset')
2 | const indexAssets = require('./indexAssets')
3 | const removeAsset = require('./removeAsset')
4 | const updateAsset = require('./updateAsset')
5 |
6 | module.exports = {
7 | addAsset,
8 | indexAssets,
9 | removeAsset,
10 | updateAsset
11 | }
12 |
--------------------------------------------------------------------------------
/server/graphql/mutations/assets/removeAsset.js:
--------------------------------------------------------------------------------
1 | const { GraphQLNonNull, GraphQLID } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const fs = require('fs')
4 | const path = require('path')
5 | const { outputType } = require('../../types/Assets')
6 |
7 | const Asset = mongoose.model('Asset')
8 |
9 | module.exports = {
10 | type: outputType,
11 | args: {
12 | _id: {
13 | name: '_id',
14 | type: new GraphQLNonNull(GraphQLID)
15 | }
16 | },
17 | async resolve ({ events, perms, socketEvent }, { _id }) {
18 | if (!perms.assets.canDeleteAssets) throw new Error('You do not have permission to delete assets.')
19 |
20 | const foundAsset = await Asset.findById(_id).exec()
21 | if (!foundAsset) throw new Error('This asset doesn\'t exist.')
22 | events.emit('pre-delete-asset', foundAsset)
23 |
24 | const removedAsset = await Asset.findByIdAndRemove(_id).exec()
25 |
26 | /* istanbul ignore if */
27 | if (!removedAsset) throw new Error('Error removing asset')
28 |
29 | const pathToFile = path.join(global.FLINT.publicPath, 'assets', foundAsset.filename)
30 |
31 | try {
32 | fs.unlinkSync(pathToFile)
33 | } catch (e) {} // eslint-disable-line
34 |
35 | socketEvent('delete-asset', removedAsset)
36 | events.emit('post-delete-asset', removedAsset)
37 | return removedAsset
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/server/graphql/mutations/assets/updateAsset.js:
--------------------------------------------------------------------------------
1 | const { GraphQLNonNull, GraphQLID } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { inputType, outputType } = require('../../types/Assets')
4 |
5 | const Asset = mongoose.model('Asset')
6 |
7 | module.exports = {
8 | type: new GraphQLNonNull(outputType),
9 | args: {
10 | data: {
11 | name: 'data',
12 | type: new GraphQLNonNull(inputType)
13 | },
14 | _id: {
15 | name: '_id',
16 | type: new GraphQLNonNull(GraphQLID)
17 | }
18 | },
19 | async resolve ({ events, perms, socketEvent }, { data, _id }) {
20 | if (perms && !perms.assets.canEditAssets) throw new Error('You do not have permission to edit assets.')
21 |
22 | const foundAsset = await Asset.findById(_id).lean().exec()
23 | if (!foundAsset) throw new Error('There is no Asset with that id')
24 | if (events) events.emit('pre-update-asset', { _id, data })
25 |
26 | const updatedAsset = await Asset.findByIdAndUpdate(_id, data, { new: true })
27 |
28 | /* istanbul ignore if */
29 | if (!updatedAsset) throw new Error('Error updating Asset')
30 |
31 | if (socketEvent) socketEvent('update-asset', updatedAsset)
32 | if (events) events.emit('post-update-asset', updatedAsset)
33 | return updatedAsset
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/server/graphql/mutations/entries/index.js:
--------------------------------------------------------------------------------
1 | const addEntry = require('./addEntry')
2 | const removeEntry = require('./removeEntry')
3 | const updateEntry = require('./updateEntry')
4 |
5 | module.exports = {
6 | addEntry,
7 | removeEntry,
8 | updateEntry
9 | }
10 |
--------------------------------------------------------------------------------
/server/graphql/mutations/entries/removeEntry.js:
--------------------------------------------------------------------------------
1 | const { GraphQLNonNull, GraphQLID } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { outputType } = require('../../types/Entries')
4 | const getProjection = require('../../get-projection')
5 |
6 | const Entry = mongoose.model('Entry')
7 |
8 | module.exports = {
9 | type: outputType,
10 | args: {
11 | _id: {
12 | name: '_id',
13 | type: new GraphQLNonNull(GraphQLID)
14 | }
15 | },
16 | async resolve ({ events, perms, socketEvent }, { _id }, ctx, ast) {
17 | if (!perms.entries.canDeleteEntries) {
18 | throw new Error('You do not have permission to delete Entries')
19 | }
20 |
21 | const foundEntry = await Entry.findById(_id)
22 | if (!foundEntry) throw new Error('There is no entry with that id')
23 |
24 | if (!perms.entries.canEditOthersEntries && foundEntry.author !== ctx.user._id) {
25 | throw new Error('You may only delete Entries that you created.')
26 | }
27 |
28 | const projection = getProjection(ast)
29 | events.emit('pre-delete-entry', foundEntry)
30 |
31 | const removedEntry = await Entry
32 | .findByIdAndRemove(_id, { select: projection })
33 | .exec()
34 |
35 | /* istanbul ignore if */
36 | if (!removedEntry) throw new Error('Error removing entry')
37 |
38 | events.emit('post-delete-entry', removedEntry)
39 | socketEvent('delete-entry', removedEntry)
40 | return removedEntry
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/server/graphql/mutations/fields/addField.js:
--------------------------------------------------------------------------------
1 | const { GraphQLNonNull } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { inputType, outputType } = require('../../types/Fields')
4 | const h = require('../../../utils/helpers')
5 |
6 | const Field = mongoose.model('Field')
7 |
8 | module.exports = {
9 | type: new GraphQLNonNull(outputType),
10 | args: {
11 | data: {
12 | name: 'data',
13 | type: new GraphQLNonNull(inputType)
14 | }
15 | },
16 | async resolve ({ events, perms, socketEvent }, args) {
17 | if (!perms.fields.canAddFields) throw new Error('You do not have permission to create a new Field.')
18 |
19 | const { title } = args.data
20 | if (!title) throw new Error('You must include a title.')
21 |
22 | const slug = h.slugify(title)
23 | if (await Field.findOne({ slug })) throw new Error('There is already a field with that slug.')
24 |
25 | const newField = new Field(args.data)
26 | events.emit('pre-new-field', newField)
27 |
28 | // Emit new-field event, wait for plugins to affect the new field
29 | const savedField = await newField.save()
30 |
31 | /* istanbul ignore if */
32 | if (!savedField) throw new Error('Error adding new field')
33 |
34 | events.emit('post-new-field', savedField)
35 | socketEvent('new-field', savedField)
36 | return savedField
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/server/graphql/mutations/fields/index.js:
--------------------------------------------------------------------------------
1 | const addField = require('./addField')
2 | const removeField = require('./removeField')
3 | const updateField = require('./updateField')
4 |
5 | module.exports = {
6 | addField,
7 | removeField,
8 | updateField
9 | }
10 |
--------------------------------------------------------------------------------
/server/graphql/mutations/fields/removeField.js:
--------------------------------------------------------------------------------
1 | const { GraphQLNonNull, GraphQLID } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { outputType } = require('../../types/Fields')
4 | const getProjection = require('../../get-projection')
5 |
6 | const Field = mongoose.model('Field')
7 | const Section = mongoose.model('Section')
8 |
9 | module.exports = {
10 | type: outputType,
11 | args: {
12 | _id: {
13 | name: '_id',
14 | type: new GraphQLNonNull(GraphQLID)
15 | }
16 | },
17 | async resolve ({ io, events, perms, socketEvent }, { _id }, ctx, ast) {
18 | if (!perms.fields.canAddFields) throw new Error('You do not have permission to delete Fields.')
19 |
20 | const foundField = await Field.findById(_id).exec()
21 | if (!foundField) throw new Error('The field could not be found.')
22 |
23 | events.emit('pre-delete-field', foundField)
24 |
25 | const projection = getProjection(ast)
26 | const removedField = await Field
27 | .findByIdAndRemove(_id, { select: projection })
28 | .exec()
29 |
30 | Section.find({ fields: _id })
31 | .then(sections => sections
32 | .forEach(sec => Section
33 | .findByIdAndUpdate(sec._id, { $pull: { fields: _id } }, { new: true })
34 | .then(updateSection => io.emit('update-section', updateSection))))
35 |
36 | /* istanbul ignore if */
37 | if (!removedField) throw new Error('Error removing field')
38 |
39 | events.emit('post-delete-field', removedField)
40 | socketEvent('delete-field', removedField)
41 | return removedField
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/server/graphql/mutations/fields/updateField.js:
--------------------------------------------------------------------------------
1 | const { GraphQLNonNull, GraphQLID } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { inputType, outputType } = require('../../types/Fields')
4 |
5 | const Field = mongoose.model('Field')
6 |
7 | module.exports = {
8 | type: new GraphQLNonNull(outputType),
9 | args: {
10 | _id: {
11 | name: '_id',
12 | type: new GraphQLNonNull(GraphQLID)
13 | },
14 | data: {
15 | name: 'data',
16 | type: new GraphQLNonNull(inputType)
17 | }
18 | },
19 | async resolve ({ events, perms, socketEvent }, { _id, data }) {
20 | const foundField = await Field.findById(_id).lean().exec()
21 | if (!foundField) throw new Error('There is no Field with this ID')
22 |
23 | if (!perms.fields.canEditFields) throw new Error('You do not have permission to edit fields.')
24 |
25 | events.emit('pre-update-field', { _id, data })
26 |
27 | const updatedField = await Field.findByIdAndUpdate(_id, data, { new: true })
28 |
29 | /* istanbul ignore if */
30 | if (!updatedField) throw new Error('Error updating Field')
31 |
32 | socketEvent('update-field', updatedField)
33 |
34 | events.emit('post-update-field', updatedField)
35 | return updatedField
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/server/graphql/mutations/index.js:
--------------------------------------------------------------------------------
1 | const entries = require('./entries')
2 | const fields = require('./fields')
3 | const sections = require('./sections')
4 | const assets = require('./assets')
5 | const users = require('./users')
6 | const usergroups = require('./usergroups')
7 | const site = require('./site')
8 | const pages = require('./pages')
9 |
10 | module.exports = Object.assign({},
11 | entries,
12 | fields,
13 | sections,
14 | assets,
15 | users,
16 | usergroups,
17 | site,
18 | pages)
19 |
--------------------------------------------------------------------------------
/server/graphql/mutations/pages/index.js:
--------------------------------------------------------------------------------
1 | const addPage = require('./addPage')
2 | const removePage = require('./removePage')
3 | const updatePage = require('./updatePage')
4 |
5 | module.exports = {
6 | addPage,
7 | removePage,
8 | updatePage
9 | }
10 |
--------------------------------------------------------------------------------
/server/graphql/mutations/pages/removePage.js:
--------------------------------------------------------------------------------
1 | const { GraphQLNonNull, GraphQLID } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { outputType } = require('../../types/Pages')
4 | const getProjection = require('../../get-projection')
5 |
6 | const Page = mongoose.model('Page')
7 |
8 | module.exports = {
9 | type: outputType,
10 | args: {
11 | _id: {
12 | name: '_id',
13 | type: new GraphQLNonNull(GraphQLID)
14 | }
15 | },
16 | async resolve ({ events, perms, socketEvent }, args, ctx, ast) {
17 | if (!perms.pages.canDeletePages) throw new Error('You do not have permission to delete Pages.')
18 |
19 | const projection = getProjection(ast)
20 | events.emit('pre-delete-page', args._id)
21 |
22 | const removedPage = await Page
23 | .findByIdAndRemove(args._id, { select: projection })
24 | .exec()
25 |
26 | /* istanbul ignore if */
27 | if (!removedPage) throw new Error('Error removing page')
28 |
29 | socketEvent('delete-page', removedPage)
30 | events.emit('post-delete-page', removedPage)
31 | return removedPage
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/server/graphql/mutations/pages/updatePage.js:
--------------------------------------------------------------------------------
1 | const { GraphQLNonNull, GraphQLID } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { inputType, outputType } = require('../../types/Pages')
4 |
5 | const Page = mongoose.model('Page')
6 |
7 | module.exports = {
8 | type: new GraphQLNonNull(outputType),
9 | args: {
10 | _id: {
11 | name: '_id',
12 | type: new GraphQLNonNull(GraphQLID)
13 | },
14 | data: {
15 | name: 'data',
16 | type: new GraphQLNonNull(inputType)
17 | }
18 | },
19 | async resolve ({ events, perms, socketEvent }, { _id, data }) {
20 | if (!perms.pages.canEditPages) throw new Error('You do not have permission to edit Pages.')
21 | const foundPage = await Page.findById(_id).exec()
22 | if (!foundPage) throw new Error('There is no Page with this ID')
23 | if (!(foundPage.homepage || data.homepage) && (data.route || foundPage.route).startsWith('/admin')) {
24 | throw new Error('Routes starting with `/admin` are reserved for Flint.')
25 | }
26 |
27 | events.emit('pre-update-page', { _id, data })
28 |
29 | const updatedPage = await Page.findByIdAndUpdate(_id, data, { new: true })
30 |
31 | /* istanbul ignore if */
32 | if (!updatedPage) throw new Error('Error updating Page')
33 |
34 | socketEvent('update-page', updatedPage)
35 | events.emit('post-update-page', updatedPage)
36 | return updatedPage
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/server/graphql/mutations/sections/addSection.js:
--------------------------------------------------------------------------------
1 | const {
2 | GraphQLNonNull
3 | } = require('graphql')
4 | const mongoose = require('mongoose')
5 | const { inputType, outputType } = require('../../types/Sections')
6 | const h = require('../../../utils/helpers')
7 |
8 | const Section = mongoose.model('Section')
9 |
10 | module.exports = {
11 | type: new GraphQLNonNull(outputType),
12 | args: {
13 | data: {
14 | name: 'data',
15 | type: new GraphQLNonNull(inputType)
16 | }
17 | },
18 | async resolve ({ events, perms, socketEvent }, args) {
19 | if (!perms.sections.canAddSections) throw new Error('You do not have permission to create a new Section.')
20 |
21 | const { fields, title } = args.data
22 | if (fields === undefined || fields.length === 0) throw new Error('You must include at least one field.')
23 | if (!title) throw new Error('You must include a title.')
24 |
25 | const slug = h.slugify(title)
26 | if (await Section.findOne({ slug })) throw new Error('There is already a section with that slug.')
27 |
28 | const newSection = new Section(args.data)
29 |
30 | events.emit('pre-new-section', newSection)
31 |
32 | const savedSection = await newSection.save()
33 |
34 | /* istanbul ignore if */
35 | if (!savedSection) throw new Error('Could not save the section.')
36 |
37 | socketEvent('new-section', savedSection)
38 | events.emit('post-new-section', savedSection)
39 | return savedSection
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/server/graphql/mutations/sections/index.js:
--------------------------------------------------------------------------------
1 | const addSection = require('./addSection')
2 | const removeSection = require('./removeSection')
3 | const updateSection = require('./updateSection')
4 |
5 | module.exports = {
6 | addSection,
7 | removeSection,
8 | updateSection
9 | }
10 |
--------------------------------------------------------------------------------
/server/graphql/mutations/sections/removeSection.js:
--------------------------------------------------------------------------------
1 | const { GraphQLNonNull, GraphQLID } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { outputType } = require('../../types/Sections')
4 | const getProjection = require('../../get-projection')
5 |
6 | const Section = mongoose.model('Section')
7 | const Entry = mongoose.model('Entry')
8 |
9 | module.exports = {
10 | type: outputType,
11 | args: {
12 | _id: {
13 | name: '_id',
14 | type: new GraphQLNonNull(GraphQLID)
15 | }
16 | },
17 | async resolve ({ events, perms, socketEvent }, args, ctx, ast) {
18 | if (!perms.sections.canDeleteSections) throw new Error('You do not have permission to delete Sections.')
19 |
20 | const projection = getProjection(ast)
21 | events.emit('pre-delete-section', args._id)
22 |
23 | const removedSection = await Section
24 | .findByIdAndRemove(args._id, { select: projection })
25 | .exec()
26 |
27 | Entry.remove({ section: args._id }).exec()
28 |
29 | /* istanbul ignore if */
30 | if (!removedSection) throw new Error('Error removing section')
31 |
32 | socketEvent('delete-section', removedSection)
33 | events.emit('post-delete-section', removedSection)
34 | return removedSection
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/server/graphql/mutations/sections/updateSection.js:
--------------------------------------------------------------------------------
1 | const { GraphQLNonNull, GraphQLID } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { inputType, outputType } = require('../../types/Sections')
4 |
5 | const Section = mongoose.model('Section')
6 |
7 | module.exports = {
8 | type: new GraphQLNonNull(outputType),
9 | args: {
10 | _id: {
11 | name: '_id',
12 | type: new GraphQLNonNull(GraphQLID)
13 | },
14 | data: {
15 | name: 'data',
16 | type: new GraphQLNonNull(inputType)
17 | }
18 | },
19 | async resolve ({ events, perms, socketEvent }, { _id, data }) {
20 | if (!perms.sections.canEditSections) throw new Error('You do not have permission to edit Sections.')
21 | if (!await Section.findById(_id)) throw new Error('There is no Section with this ID')
22 |
23 | events.emit('pre-update-section', { _id, data })
24 |
25 | const updatedSection = await Section.findByIdAndUpdate(_id, data, { new: true })
26 |
27 | /* istanbul ignore if */
28 | if (!updatedSection) throw new Error('Error updating Section')
29 |
30 | socketEvent('update-section', updatedSection)
31 | events.emit('post-update-section', updatedSection)
32 | return updatedSection
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/server/graphql/mutations/site/index.js:
--------------------------------------------------------------------------------
1 | const updateSite = require('./updateSite')
2 |
3 | module.exports = {
4 | updateSite
5 | }
6 |
--------------------------------------------------------------------------------
/server/graphql/mutations/site/updateSite.js:
--------------------------------------------------------------------------------
1 | const { GraphQLNonNull } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { inputType, outputType } = require('../../types/Site')
4 |
5 | const Site = mongoose.model('Site')
6 |
7 | module.exports = {
8 | type: new GraphQLNonNull(outputType),
9 | args: {
10 | data: {
11 | name: 'data',
12 | type: new GraphQLNonNull(inputType)
13 | }
14 | },
15 | async resolve ({ events, perms, socketEvent }, { data }) {
16 | if (!perms.site.canManageSite) throw new Error('You do not have permission to manage site configuration.')
17 |
18 | events.emit('pre-update-site', data)
19 |
20 | const updatedSite = await Site.findOneAndUpdate({}, data, { new: true })
21 |
22 | /* istanbul ignore if */
23 | if (!updatedSite) throw new Error('Error updating site configuration')
24 |
25 | socketEvent('update-site', updatedSite)
26 |
27 | events.emit('post-update-site', updatedSite)
28 | return updatedSite
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/server/graphql/mutations/usergroups/addUserGroup.js:
--------------------------------------------------------------------------------
1 | const { GraphQLNonNull } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { inputType, outputType } = require('../../types/UserGroups')
4 |
5 | const UserGroup = mongoose.model('UserGroup')
6 |
7 | module.exports = {
8 | type: new GraphQLNonNull(outputType),
9 | args: {
10 | data: {
11 | name: 'data',
12 | type: new GraphQLNonNull(inputType)
13 | }
14 | },
15 | async resolve ({ events, perms, socketEvent }, { data }) {
16 | if (!perms.usergroups.canAddUserGroups) throw new Error('You do not have permission to add User Groups.')
17 |
18 | const newUserGroup = new UserGroup(data)
19 |
20 | events.emit('pre-new-usergroup', newUserGroup)
21 |
22 | const savedUserGroup = await newUserGroup.save()
23 |
24 | /* istanbul ignore if */
25 | if (!savedUserGroup) throw new Error('Error adding new entry')
26 |
27 | events.emit('post-new-usergroup', savedUserGroup)
28 | socketEvent('new-usergroup', savedUserGroup)
29 | return savedUserGroup
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/server/graphql/mutations/usergroups/index.js:
--------------------------------------------------------------------------------
1 | const addUserGroup = require('./addUserGroup')
2 | const removeUserGroup = require('./removeUserGroup')
3 | const updateUserGroup = require('./updateUserGroup')
4 |
5 | module.exports = {
6 | addUserGroup,
7 | removeUserGroup,
8 | updateUserGroup
9 | }
10 |
--------------------------------------------------------------------------------
/server/graphql/mutations/usergroups/removeUserGroup.js:
--------------------------------------------------------------------------------
1 | const { GraphQLNonNull, GraphQLID } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { outputType } = require('../../types/UserGroups')
4 | const getProjection = require('../../get-projection')
5 |
6 | const UserGroup = mongoose.model('UserGroup')
7 |
8 | module.exports = {
9 | type: outputType,
10 | args: {
11 | _id: {
12 | name: '_id',
13 | type: new GraphQLNonNull(GraphQLID)
14 | }
15 | },
16 | async resolve ({ events, perms, socketEvent }, { _id }, ctx, ast) {
17 | if (!perms.usergroups.canDeleteUserGroups) throw new Error('You do not have permission to delete User Groups.')
18 |
19 | const projection = getProjection(ast)
20 |
21 | const foundUserGroup = await UserGroup.findById(_id)
22 | events.emit('pre-delete-usergroup', foundUserGroup)
23 |
24 | const removedUserGroup = await UserGroup
25 | .findByIdAndRemove(_id, { select: projection })
26 | .exec()
27 |
28 | /* istanbul ignore if */
29 | if (!removedUserGroup) throw new Error('Error removing user group')
30 |
31 | events.emit('post-delete-usergroup', removedUserGroup)
32 | socketEvent('delete-usergroup', removedUserGroup)
33 | return removedUserGroup
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/server/graphql/mutations/usergroups/updateUserGroup.js:
--------------------------------------------------------------------------------
1 | const { GraphQLNonNull, GraphQLID } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { inputType, outputType } = require('../../types/UserGroups')
4 |
5 | const UserGroup = mongoose.model('UserGroup')
6 |
7 | module.exports = {
8 | type: new GraphQLNonNull(outputType),
9 | args: {
10 | _id: {
11 | name: '_id',
12 | type: new GraphQLNonNull(GraphQLID)
13 | },
14 | data: {
15 | name: 'data',
16 | type: new GraphQLNonNull(inputType)
17 | }
18 | },
19 | async resolve ({ events, perms, socketEvent }, { _id, data }) {
20 | if (!perms.usergroups.canEditUserGroups) throw new Error('You do not have permission to edit User Groups.')
21 |
22 | const foundUserGroup = await UserGroup.findById(_id)
23 | if (!foundUserGroup) throw new Error('There is no UserGroup with this ID')
24 | if (foundUserGroup.slug === 'admin') throw new Error('You cannot edit the Admin usergroup.')
25 |
26 | events.emit('pre-update-usergroup', { _id, data })
27 | const updatedUserGroup = await UserGroup.findByIdAndUpdate(_id, data, { new: true })
28 |
29 | /* istanbul ignore if */
30 | if (!updatedUserGroup) throw new Error('Error updating UserGroup')
31 |
32 | events.emit('post-update-usergroup', updatedUserGroup)
33 | socketEvent('update-usergroup', updatedUserGroup)
34 | return updatedUserGroup
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/server/graphql/mutations/users/deleteUser.js:
--------------------------------------------------------------------------------
1 | const { GraphQLNonNull, GraphQLID } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { outputType } = require('../../types/Users')
4 | const getProjection = require('../../get-projection')
5 |
6 | const User = mongoose.model('User')
7 |
8 | module.exports = {
9 | type: outputType,
10 | args: {
11 | _id: {
12 | name: '_id',
13 | type: new GraphQLNonNull(GraphQLID)
14 | }
15 | },
16 | async resolve ({ events, perms, socketEvent }, { _id }, ctx, ast) {
17 | if (_id === ctx.user._id) throw new Error('You cannot delete your own account.')
18 |
19 | if (!perms.users.canDeleteUsers) {
20 | throw new Error('You do not have permission to delete Users.')
21 | }
22 |
23 | const foundUser = await User.findById(_id).exec()
24 | if (!foundUser) throw new Error('There is no User with that id.')
25 |
26 | const projection = getProjection(ast)
27 | events.emit('pre-delete-User', _id)
28 |
29 | const removedUser = await User
30 | .findByIdAndRemove(_id, { select: projection })
31 | .exec()
32 |
33 | /* istanbul ignore if */
34 | if (!removedUser) throw new Error('Error removing User')
35 |
36 | events.emit('post-delete-user', removedUser)
37 | socketEvent('delete-user', removedUser)
38 | return removedUser
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/server/graphql/mutations/users/index.js:
--------------------------------------------------------------------------------
1 | const addUser = require('./addUser')
2 | const updateUser = require('./updateUser')
3 | const resetPassword = require('./resetPassword')
4 | const deleteUser = require('./deleteUser')
5 |
6 | module.exports = {
7 | addUser,
8 | updateUser,
9 | resetPassword,
10 | deleteUser
11 | }
12 |
--------------------------------------------------------------------------------
/server/graphql/mutations/users/resetPassword.js:
--------------------------------------------------------------------------------
1 | const { GraphQLNonNull, GraphQLID } = require('graphql')
2 | const randtoken = require('rand-token')
3 | const mongoose = require('mongoose')
4 | const { outputType } = require('../../types/Users')
5 | const sendEmail = require('../../../utils/emails/sendEmail')
6 |
7 | const User = mongoose.model('User')
8 |
9 | module.exports = {
10 | type: new GraphQLNonNull(outputType),
11 | args: {
12 | _id: {
13 | name: '_id',
14 | type: new GraphQLNonNull(GraphQLID)
15 | }
16 | },
17 | async resolve ({ events, perms, socketEvent }, { _id }) {
18 | if (!perms.users.canResetUserPasswords) throw new Error('You do not have permission to manage users.')
19 |
20 | const user = await User.findById(_id).exec()
21 | if (!user) throw new Error('There is no user with that id.')
22 |
23 | const token = user.token || await randtoken.generate(16)
24 | const data = { token, password: undefined }
25 |
26 | const updatedUser = await User.findByIdAndUpdate(_id, data, { new: true }).exec()
27 |
28 | /* istanbul ignore if */
29 | if (!updatedUser) throw new Error('Error updating user')
30 |
31 | const name = user.name.first || user.username
32 |
33 | sendEmail(user.email, 'reset-password', { subject: 'Reset your password', token, name })
34 |
35 | return updatedUser
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/server/graphql/queries/assets/index.js:
--------------------------------------------------------------------------------
1 | const asset = require('./single')
2 | const assets = require('./multiple')
3 |
4 | module.exports = {
5 | asset,
6 | assets
7 | }
8 |
--------------------------------------------------------------------------------
/server/graphql/queries/assets/multiple.js:
--------------------------------------------------------------------------------
1 | const { GraphQLList } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { outputType } = require('../../types/Assets')
4 | const getProjection = require('../../get-projection')
5 |
6 | const Asset = mongoose.model('Asset')
7 |
8 | module.exports = {
9 | type: new GraphQLList(outputType),
10 | args: {},
11 | resolve (root, args, ctx, ast) {
12 | const projection = getProjection(ast)
13 |
14 | return Asset
15 | .find()
16 | .sort({ dateCreated: 1 })
17 | .select(projection)
18 | .exec()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/server/graphql/queries/assets/single.js:
--------------------------------------------------------------------------------
1 | const {
2 | GraphQLID,
3 | GraphQLNonNull
4 | } = require('graphql')
5 | const mongoose = require('mongoose')
6 |
7 | const { outputType } = require('../../types/Assets')
8 | const getProjection = require('../../get-projection')
9 |
10 | const Asset = mongoose.model('Asset')
11 |
12 | module.exports = {
13 | type: outputType,
14 | args: {
15 | _id: {
16 | name: '_id',
17 | type: new GraphQLNonNull(GraphQLID)
18 | }
19 | },
20 | resolve (root, args, ctx, ast) {
21 | const projection = getProjection(ast)
22 |
23 | return Asset
24 | .findById(args._id)
25 | .select(projection)
26 | .exec()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/server/graphql/queries/entries/index.js:
--------------------------------------------------------------------------------
1 | const entry = require('./single')
2 | const entries = require('./multiple')
3 |
4 | module.exports = {
5 | entry,
6 | entries
7 | }
8 |
--------------------------------------------------------------------------------
/server/graphql/queries/entries/multiple.js:
--------------------------------------------------------------------------------
1 | const { GraphQLList, GraphQLString } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { outputType } = require('../../types/Entries')
4 | const getProjection = require('../../get-projection')
5 |
6 | const Entry = mongoose.model('Entry')
7 | const Section = mongoose.model('Section')
8 |
9 | module.exports = {
10 | type: new GraphQLList(outputType),
11 | args: {
12 | status: {
13 | name: 'status',
14 | type: GraphQLString
15 | },
16 | sectionSlug: {
17 | name: 'sectionSlug',
18 | type: GraphQLString
19 | }
20 | },
21 | async resolve (root, args, ctx, ast) {
22 | const isAUser = ctx !== undefined && ctx.user !== undefined
23 | const projection = getProjection(ast)
24 |
25 | const fargs = Object.assign({}, args)
26 |
27 | if (isAUser && root.perms && !root.perms.entries.canSeeDrafts) {
28 | fargs.status = 'live'
29 | }
30 |
31 | if (args.sectionSlug) {
32 | const section = await Section.findOne({ slug: args.sectionSlug }).select('_id').lean().exec()
33 | if (!section) throw new Error('There is no section with that slug.')
34 | delete fargs.sectionSlug
35 | fargs.section = section._id
36 | }
37 |
38 | return Entry
39 | .find(fargs)
40 | .sort({ dateCreated: 1 })
41 | .populate('author')
42 | .select(projection)
43 | .exec()
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/server/graphql/queries/entries/single.js:
--------------------------------------------------------------------------------
1 | const { GraphQLID, GraphQLString } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { outputType } = require('../../types/Entries')
4 | const getProjection = require('../../get-projection')
5 |
6 | const Entry = mongoose.model('Entry')
7 | const Section = mongoose.model('Section')
8 |
9 | module.exports = {
10 | type: outputType,
11 | args: {
12 | _id: {
13 | name: '_id',
14 | type: GraphQLID
15 | },
16 | slug: {
17 | name: 'slug',
18 | type: GraphQLString
19 | },
20 | status: {
21 | name: 'slug',
22 | type: GraphQLString
23 | },
24 | sectionSlug: {
25 | name: 'sectionSlug',
26 | type: GraphQLString
27 | }
28 | },
29 | async resolve (root, args, ctx, ast) {
30 | const isAUser = !!ctx && ctx.user !== undefined
31 | const projection = getProjection(ast)
32 |
33 | const fargs = Object.assign({}, args)
34 |
35 | if (isAUser && root.perms && !root.perms.entries.canSeeDrafts) {
36 | fargs.status = 'live'
37 | }
38 |
39 | if (args.slug && !args.sectionSlug) {
40 | throw new Error('When querying for an entry by slug, you must also query by sectionSlug.')
41 | }
42 |
43 | if (args.sectionSlug) {
44 | const section = await Section.findOne({ slug: args.sectionSlug }).select('_id').lean().exec()
45 | if (!section) throw new Error('That section does not exist.')
46 | fargs.section = section._id
47 | delete fargs.sectionSlug
48 | }
49 |
50 | return Entry
51 | .findOne(fargs)
52 | .populate('author')
53 | .select(projection)
54 | .exec()
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/server/graphql/queries/fields/index.js:
--------------------------------------------------------------------------------
1 | const field = require('./single')
2 | const fields = require('./multiple')
3 |
4 | module.exports = {
5 | field,
6 | fields
7 | }
8 |
--------------------------------------------------------------------------------
/server/graphql/queries/fields/multiple.js:
--------------------------------------------------------------------------------
1 | const { GraphQLList } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { outputType } = require('../../types/Fields')
4 | const getProjection = require('../../get-projection')
5 |
6 | const Field = mongoose.model('Field')
7 |
8 | module.exports = {
9 | type: new GraphQLList(outputType),
10 | args: {},
11 | resolve (root, args, ctx, ast) {
12 | const projection = getProjection(ast)
13 |
14 | return Field
15 | .find()
16 | .select(projection)
17 | .exec()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/server/graphql/queries/fields/single.js:
--------------------------------------------------------------------------------
1 | const { GraphQLID, GraphQLNonNull } = require('graphql')
2 | const mongoose = require('mongoose')
3 |
4 | const { outputType } = require('../../types/Fields')
5 | const getProjection = require('../../get-projection')
6 |
7 | const Field = mongoose.model('Field')
8 |
9 | module.exports = {
10 | type: outputType,
11 | args: {
12 | _id: {
13 | name: '_id',
14 | type: new GraphQLNonNull(GraphQLID)
15 | }
16 | },
17 | resolve (root, args, ctx, ast) {
18 | const projection = getProjection(ast)
19 |
20 | return Field
21 | .findById(args._id)
22 | .select(projection)
23 | .exec()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/server/graphql/queries/index.js:
--------------------------------------------------------------------------------
1 | const entries = require('./entries')
2 | const sections = require('./sections')
3 | const users = require('./users')
4 | const fields = require('./fields')
5 | const assets = require('./assets')
6 | const usergroups = require('./usergroups')
7 | const site = require('./site')
8 | const plugins = require('./plugins')
9 | const pages = require('./pages')
10 |
11 | module.exports = Object.assign({},
12 | entries,
13 | sections,
14 | users,
15 | fields,
16 | assets,
17 | usergroups,
18 | site,
19 | plugins,
20 | pages)
21 |
--------------------------------------------------------------------------------
/server/graphql/queries/pages/index.js:
--------------------------------------------------------------------------------
1 | const page = require('./single')
2 | const pages = require('./multiple')
3 |
4 | module.exports = {
5 | page,
6 | pages
7 | }
8 |
--------------------------------------------------------------------------------
/server/graphql/queries/pages/multiple.js:
--------------------------------------------------------------------------------
1 | const { GraphQLList } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { outputType } = require('../../types/Pages')
4 | const getProjection = require('../../get-projection')
5 |
6 | const Page = mongoose.model('Page')
7 |
8 | module.exports = {
9 | type: new GraphQLList(outputType),
10 | args: {},
11 | resolve (root, args, ctx, ast) {
12 | const projection = getProjection(ast)
13 |
14 | return Page
15 | .find()
16 | .sort({ dateCreated: 1 })
17 | .select(projection)
18 | .exec()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/server/graphql/queries/pages/single.js:
--------------------------------------------------------------------------------
1 | const { GraphQLID, GraphQLString } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { outputType } = require('../../types/Pages')
4 | const getProjection = require('../../get-projection')
5 |
6 | const Page = mongoose.model('Page')
7 |
8 | module.exports = {
9 | type: outputType,
10 | args: {
11 | _id: {
12 | name: '_id',
13 | type: GraphQLID
14 | },
15 | slug: {
16 | name: 'slug',
17 | type: GraphQLString
18 | }
19 | },
20 | resolve (root, args, ctx, ast) {
21 | const projection = getProjection(ast)
22 |
23 | return Page
24 | .findOne(args)
25 | .select(projection)
26 | .exec()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/server/graphql/queries/plugins/index.js:
--------------------------------------------------------------------------------
1 | const plugins = require('./multiple')
2 |
3 | module.exports = {
4 | plugins
5 | }
6 |
--------------------------------------------------------------------------------
/server/graphql/queries/plugins/multiple.js:
--------------------------------------------------------------------------------
1 | const { GraphQLList } = require('graphql')
2 |
3 | const mongoose = require('mongoose')
4 |
5 | const Plugin = mongoose.model('Plugin')
6 |
7 | const { outputType } = require('../../types/Plugins')
8 | const getProjection = require('../../get-projection')
9 |
10 | module.exports = {
11 | type: new GraphQLList(outputType),
12 | args: {},
13 | resolve (root, args, ctx, ast) {
14 | const projection = getProjection(ast)
15 |
16 | return Plugin
17 | .find()
18 | .select(projection)
19 | .exec()
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/server/graphql/queries/sections/index.js:
--------------------------------------------------------------------------------
1 | const section = require('./single')
2 | const sections = require('./multiple')
3 |
4 | module.exports = {
5 | section,
6 | sections
7 | }
8 |
--------------------------------------------------------------------------------
/server/graphql/queries/sections/multiple.js:
--------------------------------------------------------------------------------
1 | const { GraphQLList } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { outputType } = require('../../types/Sections')
4 | const getProjection = require('../../get-projection')
5 |
6 | const Section = mongoose.model('Section')
7 |
8 | module.exports = {
9 | type: new GraphQLList(outputType),
10 | args: {},
11 | resolve (root, args, ctx, ast) {
12 | const projection = getProjection(ast)
13 |
14 | return Section
15 | .find()
16 | .sort({ dateCreated: 1 })
17 | .select(projection)
18 | .exec()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/server/graphql/queries/sections/single.js:
--------------------------------------------------------------------------------
1 | const { GraphQLID, GraphQLString } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { outputType } = require('../../types/Sections')
4 | const getProjection = require('../../get-projection')
5 |
6 | const Section = mongoose.model('Section')
7 |
8 | module.exports = {
9 | type: outputType,
10 | args: {
11 | _id: {
12 | name: '_id',
13 | type: GraphQLID
14 | },
15 | slug: {
16 | name: 'slug',
17 | type: GraphQLString
18 | }
19 | },
20 | resolve (root, args, ctx, ast) {
21 | const projection = getProjection(ast)
22 |
23 | return Section
24 | .findOne(args)
25 | .select(projection)
26 | .exec()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/server/graphql/queries/site/index.js:
--------------------------------------------------------------------------------
1 | const site = require('./site')
2 |
3 | module.exports = {
4 | site
5 | }
6 |
--------------------------------------------------------------------------------
/server/graphql/queries/site/site.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const { outputType } = require('../../types/Site')
3 | const getProjection = require('../../get-projection')
4 |
5 | const Site = mongoose.model('Site')
6 |
7 | module.exports = {
8 | type: outputType,
9 | args: {},
10 | resolve (root, args, ctx, ast) {
11 | const projection = getProjection(ast)
12 |
13 | return Site
14 | .findOne()
15 | .select(projection)
16 | .lean()
17 | .exec()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/server/graphql/queries/usergroups/index.js:
--------------------------------------------------------------------------------
1 | const usergroup = require('./single')
2 | const usergroups = require('./multiple')
3 |
4 | module.exports = {
5 | usergroup,
6 | usergroups
7 | }
8 |
--------------------------------------------------------------------------------
/server/graphql/queries/usergroups/multiple.js:
--------------------------------------------------------------------------------
1 | const { GraphQLList, GraphQLString } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { outputType } = require('../../types/UserGroups')
4 | const getProjection = require('../../get-projection')
5 |
6 | const UserGroup = mongoose.model('UserGroup')
7 |
8 | module.exports = {
9 | type: new GraphQLList(outputType),
10 | args: {
11 | status: {
12 | name: 'status',
13 | type: GraphQLString
14 | }
15 | },
16 | resolve (root, args, ctx, ast) {
17 | const projection = getProjection(ast)
18 |
19 | return UserGroup
20 | .find(args)
21 | .sort({ dateCreated: 1 })
22 | .select(projection)
23 | .exec()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/server/graphql/queries/usergroups/single.js:
--------------------------------------------------------------------------------
1 | const { GraphQLID } = require('graphql')
2 | const mongoose = require('mongoose')
3 | const { outputType } = require('../../types/UserGroups')
4 | const getProjection = require('../../get-projection')
5 |
6 | const UserGroup = mongoose.model('UserGroup')
7 |
8 | module.exports = {
9 | type: outputType,
10 | args: {
11 | _id: {
12 | name: '_id',
13 | type: GraphQLID
14 | }
15 | },
16 | resolve (root, args, ctx, ast) {
17 | const projection = getProjection(ast)
18 |
19 | return UserGroup
20 | .findOne(args)
21 | .select(projection)
22 | .exec()
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/server/graphql/queries/users/index.js:
--------------------------------------------------------------------------------
1 | const user = require('./single')
2 | const users = require('./multiple')
3 |
4 | module.exports = {
5 | user,
6 | users
7 | }
8 |
--------------------------------------------------------------------------------
/server/graphql/queries/users/multiple.js:
--------------------------------------------------------------------------------
1 | const {
2 | GraphQLList
3 | } = require('graphql')
4 |
5 | const mongoose = require('mongoose')
6 |
7 | const User = mongoose.model('User')
8 |
9 | const { outputType } = require('../../types/Users')
10 | const getProjection = require('../../get-projection')
11 |
12 | module.exports = {
13 | type: new GraphQLList(outputType),
14 | args: {},
15 | resolve (root, args, ctx, ast) {
16 | const projection = getProjection(ast)
17 |
18 | return User
19 | .find()
20 | .sort({ dateCreated: 1 })
21 | .populate('usergroup')
22 | .select(projection)
23 | .lean()
24 | .exec()
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/server/graphql/queries/users/single.js:
--------------------------------------------------------------------------------
1 | const { GraphQLID } = require('graphql')
2 | const mongoose = require('mongoose')
3 |
4 | const { outputType } = require('../../types/Users')
5 | const getProjection = require('../../get-projection')
6 |
7 | const User = mongoose.model('User')
8 |
9 | module.exports = {
10 | type: outputType,
11 | args: {
12 | _id: {
13 | name: '_id',
14 | type: GraphQLID
15 | }
16 | },
17 | resolve (root, args, ctx, ast) {
18 | const _id = args._id || ctx.user._id
19 | const projection = getProjection(ast)
20 |
21 | return User
22 | .findById(_id)
23 | .populate('usergroup')
24 | .select(projection)
25 | .lean()
26 | .exec()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/server/graphql/types/CustomTypes/DateTime.js:
--------------------------------------------------------------------------------
1 | const { Kind } = require('graphql/language')
2 | const { GraphQLScalarType } = require('graphql')
3 |
4 | function serializeDate (value) {
5 | if (value instanceof Date) {
6 | return value.getTime()
7 | } else if (typeof value === 'number') {
8 | return Math.trunc(value)
9 | } else if (typeof value === 'string') {
10 | return Date.parse(value)
11 | }
12 | return null
13 | }
14 |
15 | function parseDate (value) {
16 | if (value === null) {
17 | return null
18 | }
19 |
20 | try {
21 | return new Date(value)
22 | } catch (err) {
23 | return null
24 | }
25 | }
26 |
27 | function parseDateFromLiteral (ast) {
28 | if (ast.kind === Kind.INT) {
29 | const num = parseInt(ast.value, 10)
30 | return new Date(num)
31 | } else if (ast.kind === Kind.STRING) {
32 | return parseDate(ast.value)
33 | }
34 | return null
35 | }
36 |
37 | module.exports = new GraphQLScalarType({
38 | name: 'Timestamp',
39 | description: 'The javascript `Date` as integer. Type represents date and time as number of milliseconds from start of UNIX epoch.',
40 | serialize: serializeDate,
41 | parseValue: parseDate,
42 | parseLiteral: parseDateFromLiteral
43 | })
44 |
--------------------------------------------------------------------------------
/server/graphql/types/CustomTypes/FieldType.js:
--------------------------------------------------------------------------------
1 | const {
2 | GraphQLInputObjectType,
3 | GraphQLObjectType,
4 | GraphQLNonNull,
5 | GraphQLString,
6 | GraphQLID
7 | } = require('graphql')
8 | const ObjectType = require('./ObjectType')
9 |
10 | exports.outputType = new GraphQLObjectType({
11 | name: 'EntryFields',
12 | fields: {
13 | fieldId: {
14 | type: new GraphQLNonNull(GraphQLID)
15 | },
16 | handle: {
17 | type: GraphQLString
18 | },
19 | value: {
20 | type: ObjectType
21 | }
22 | }
23 | })
24 |
25 | exports.inputType = new GraphQLInputObjectType({
26 | name: 'EntryFieldsInput',
27 | fields: {
28 | fieldId: {
29 | type: new GraphQLNonNull(GraphQLID)
30 | },
31 | handle: {
32 | type: GraphQLString
33 | },
34 | value: {
35 | type: ObjectType
36 | }
37 | }
38 | })
39 |
--------------------------------------------------------------------------------
/server/graphql/types/CustomTypes/ObjectType.js:
--------------------------------------------------------------------------------
1 | const { GraphQLScalarType } = require('graphql/type')
2 | const { GraphQLError } = require('graphql/error')
3 | const { Kind } = require('graphql/language')
4 |
5 | const ObjectType = new GraphQLScalarType({
6 | name: 'ObjectType',
7 | serialize: value => value,
8 | parseValue: value => value,
9 | parseLiteral: (ast) => {
10 | /* istanbul ignore if */
11 | if (ast.kind !== Kind.OBJECT) {
12 | throw new GraphQLError(`Query error: Can only parse object but got a: ${ast.kind}, ${[ast]}`)
13 | }
14 | return ast.value
15 | }
16 | })
17 |
18 | module.exports = ObjectType
19 |
--------------------------------------------------------------------------------
/server/graphql/types/CustomTypes/index.js:
--------------------------------------------------------------------------------
1 | const DateTime = require('./DateTime')
2 | const ObjectType = require('./ObjectType')
3 | const FieldType = require('./FieldType')
4 |
5 | module.exports = {
6 | DateTime,
7 | ObjectType,
8 | FieldType
9 | }
10 |
--------------------------------------------------------------------------------
/server/graphql/types/Plugins.js:
--------------------------------------------------------------------------------
1 | const {
2 | GraphQLObjectType,
3 | GraphQLNonNull,
4 | GraphQLString,
5 | GraphQLID
6 | } = require('graphql')
7 | const { DateTime } = require('./CustomTypes')
8 |
9 | exports.outputType = new GraphQLObjectType({
10 | name: 'Plugin',
11 | fields: {
12 | _id: {
13 | type: new GraphQLNonNull(GraphQLID),
14 | description: 'Mongo ID string.'
15 | },
16 | title: {
17 | type: new GraphQLNonNull(GraphQLString),
18 | description: 'Title of the plugin.'
19 | },
20 | version: {
21 | type: new GraphQLNonNull(GraphQLString),
22 | description: 'Version of the plugin, ideally a semver.'
23 | },
24 | uid: {
25 | type: new GraphQLNonNull(GraphQLString),
26 | description: 'UID of the plugin.'
27 | },
28 | name: {
29 | type: new GraphQLNonNull(GraphQLString),
30 | description: 'Name of the plugin class.'
31 | },
32 | icon: {
33 | type: new GraphQLObjectType({
34 | name: 'PluginIcon',
35 | fields: {
36 | path: {
37 | type: new GraphQLNonNull(GraphQLString),
38 | description: 'The path, from the plugin\'s entry point to the icon file.'
39 | },
40 | buffer: {
41 | type: new GraphQLNonNull(GraphQLString),
42 | resolve: plug => plug.buffer.toString('base64'),
43 | description: 'The buffer for the plugin\'s icon.'
44 | }
45 | }
46 | })
47 | },
48 | dateInstalled: {
49 | type: new GraphQLNonNull(DateTime),
50 | description: 'The date, in a UNIX timestamp, that the plugin was installed.'
51 | }
52 | }
53 | })
54 |
--------------------------------------------------------------------------------
/server/graphql/types/Site.js:
--------------------------------------------------------------------------------
1 | const { GraphQLInputObjectType, GraphQLObjectType, GraphQLString, GraphQLBoolean, GraphQLID } = require('graphql')
2 | const { ObjectType } = require('./CustomTypes')
3 |
4 | const fields = {
5 | defaultUserGroup: {
6 | type: GraphQLID,
7 | description: 'Mongo ID of the usergroup that new users will be assigned to by default.'
8 | },
9 | siteName: {
10 | type: GraphQLString,
11 | description: 'Name of the website.'
12 | },
13 | siteUrl: {
14 | type: GraphQLString,
15 | description: 'URL of the website.'
16 | },
17 | style: {
18 | type: GraphQLString,
19 | description: 'A string of CSS code that can be injected into templates.'
20 | },
21 | siteLogo: {
22 | type: ObjectType,
23 | description: 'A path to the site\'s logo.'
24 | },
25 | allowPublicRegistration: {
26 | type: GraphQLBoolean,
27 | description: 'Boolean to allow or disallow the public registration routes.'
28 | },
29 | enableCacheBusting: {
30 | type: GraphQLBoolean,
31 | description: 'Enable or disable hash generation for the CSS bundle.',
32 | defaultValue: false
33 | },
34 | cssHash: {
35 | type: GraphQLString,
36 | description: 'The hash used to cache bust for the CSS bundle.'
37 | }
38 | }
39 |
40 | exports.outputType = new GraphQLObjectType({
41 | name: 'Site',
42 | fields
43 | })
44 |
45 | exports.inputType = new GraphQLInputObjectType({
46 | name: 'SiteInput',
47 | fields
48 | })
49 |
--------------------------------------------------------------------------------
/server/models/AssetSchema.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 |
3 | const Schema = mongoose.Schema
4 |
5 | const AssetSchema = new Schema({
6 | title: {
7 | type: String,
8 | required: true
9 | },
10 | extension: {
11 | type: String,
12 | required: true
13 | },
14 | filename: {
15 | type: String,
16 | required: true
17 | },
18 | dateCreated: {
19 | type: Date,
20 | default: Date.now
21 | },
22 | width: {
23 | type: Number,
24 | required: true
25 | },
26 | height: {
27 | type: Number,
28 | required: true
29 | },
30 | size: {
31 | type: Number,
32 | required: true
33 | },
34 | mimetype: {
35 | type: String,
36 | required: true
37 | }
38 | })
39 |
40 | AssetSchema.name = 'Asset'
41 |
42 | // Can't use arrow function because of (this) binding
43 | // eslint-disable-next-line func-names
44 | AssetSchema.pre('validate', function (next) {
45 | const ext = this.filename.split(/[\s.]+/)
46 | this.extension = ext[ext.length - 1]
47 | next()
48 | })
49 |
50 | module.exports = AssetSchema
51 |
--------------------------------------------------------------------------------
/server/models/FieldSchema.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const camelcase = require('camelcase')
3 | const h = require('../utils/helpers')
4 |
5 | const Schema = mongoose.Schema
6 |
7 | const FieldSchema = new Schema({
8 | title: {
9 | type: String,
10 | required: true
11 | },
12 | slug: {
13 | type: String,
14 | required: true,
15 | unique: true
16 | },
17 | handle: {
18 | type: String,
19 | required: true,
20 | unique: true
21 | },
22 | instructions: {
23 | type: String
24 | },
25 | type: {
26 | type: String,
27 | required: true
28 | },
29 | required: {
30 | type: Boolean,
31 | required: true
32 | },
33 | options: {
34 | type: Schema.Types.Mixed
35 | },
36 | dateCreated: {
37 | type: Date,
38 | default: Date.now
39 | }
40 | })
41 |
42 | FieldSchema.name = 'Field'
43 |
44 | // Can't use arrow function because of (this) binding
45 | // eslint-disable-next-line func-names
46 | FieldSchema.pre('validate', function (next) {
47 | this.slug = h.slugify(this.title)
48 | this.handle = camelcase(this.title)
49 | next()
50 | })
51 |
52 | module.exports = FieldSchema
53 |
--------------------------------------------------------------------------------
/server/models/PageSchema.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const camelcase = require('camelcase')
3 | const h = require('../utils/helpers')
4 |
5 | const Schema = mongoose.Schema
6 |
7 | const PageSchema = new Schema({
8 | title: {
9 | type: String,
10 | required: true
11 | },
12 | slug: {
13 | type: String,
14 | required: true,
15 | unique: true
16 | },
17 | handle: {
18 | type: String,
19 | required: true,
20 | unique: true
21 | },
22 | template: {
23 | type: String,
24 | required: true
25 | },
26 | dateCreated: {
27 | type: Date,
28 | default: Date.now
29 | },
30 | fieldLayout: [{
31 | type: Schema.Types.ObjectId,
32 | ref: 'Field'
33 | }],
34 | fields: [{
35 | fieldId: {
36 | type: Schema.Types.ObjectId,
37 | ref: 'Field',
38 | required: true
39 | },
40 | handle: {
41 | type: String,
42 | required: true
43 | },
44 | value: {
45 | type: Schema.Types.Mixed,
46 | required: true
47 | }
48 | }],
49 | homepage: {
50 | type: Boolean,
51 | default: false
52 | },
53 | route: {
54 | type: String,
55 | required: true
56 | }
57 | })
58 |
59 | PageSchema.name = 'Page'
60 |
61 | // Can't use arrow function because of (this) binding
62 | // eslint-disable-next-line func-names
63 | PageSchema.pre('validate', function (next) {
64 | this.slug = h.slugify(this.title)
65 | this.handle = camelcase(this.title)
66 | next()
67 | })
68 |
69 | module.exports = PageSchema
70 |
--------------------------------------------------------------------------------
/server/models/PluginSchema.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 |
3 | const Schema = mongoose.Schema
4 |
5 | const PluginSchema = new Schema({
6 | name: {
7 | type: String,
8 | required: true,
9 | unique: true
10 | },
11 | title: {
12 | type: String,
13 | required: true,
14 | unique: true
15 | },
16 | uid: {
17 | type: String,
18 | required: true,
19 | unique: true
20 | },
21 | icon: {
22 | path: {
23 | type: String,
24 | required: true,
25 | default: 'icon.png'
26 | },
27 | buffer: {
28 | type: Buffer,
29 | required: true
30 | }
31 | },
32 | dateInstalled: {
33 | type: Date,
34 | default: Date.now
35 | },
36 | version: {
37 | type: String,
38 | required: true
39 | }
40 | }, { strict: false })
41 |
42 | PluginSchema.name = 'Plugin'
43 |
44 | module.exports = PluginSchema
45 |
--------------------------------------------------------------------------------
/server/models/SectionSchema.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const camelcase = require('camelcase')
3 | const h = require('../utils/helpers')
4 |
5 | const Schema = mongoose.Schema
6 |
7 | const SectionSchema = new Schema({
8 | title: {
9 | type: String,
10 | required: true
11 | },
12 | slug: {
13 | type: String,
14 | required: true,
15 | unique: true
16 | },
17 | handle: {
18 | type: String,
19 | required: true,
20 | unique: true
21 | },
22 | template: {
23 | type: String,
24 | required: true
25 | },
26 | dateCreated: {
27 | type: Date,
28 | default: Date.now
29 | },
30 | fields: [{
31 | type: Schema.Types.ObjectId,
32 | ref: 'Field'
33 | }]
34 | })
35 |
36 | SectionSchema.name = 'Section'
37 |
38 | // Can't use arrow function because of (this) binding
39 | // eslint-disable-next-line func-names
40 | SectionSchema.pre('validate', function (next) {
41 | this.slug = h.slugify(this.title)
42 | this.handle = camelcase(this.title)
43 | next()
44 | })
45 |
46 | module.exports = SectionSchema
47 |
--------------------------------------------------------------------------------
/server/models/SiteSchema.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 |
3 | const Schema = mongoose.Schema
4 |
5 | const SiteSchema = new Schema({
6 | defaultUserGroup: {
7 | type: Schema.Types.ObjectId,
8 | ref: 'UserGroup'
9 | },
10 | siteName: {
11 | type: String,
12 | default: 'Flint Site Name'
13 | },
14 | siteUrl: {
15 | type: String,
16 | default: 'https://flintcms.io'
17 | },
18 | siteLogo: {
19 | type: Schema.Types.Mixed
20 | },
21 | style: String,
22 | allowPublicRegistration: {
23 | type: Boolean,
24 | default: false
25 | },
26 | templatePath: String,
27 | scssPath: String,
28 | publicPath: String,
29 | configPath: String,
30 | pluginPath: String,
31 | scssEntryPoint: {
32 | type: String,
33 | default: 'main.scss'
34 | },
35 | enableCacheBusting: {
36 | type: Boolean,
37 | default: false
38 | },
39 | cssHash: String
40 | }, { strict: false })
41 |
42 | SiteSchema.name = 'Site'
43 |
44 | module.exports = SiteSchema
45 |
--------------------------------------------------------------------------------
/server/models/UserGroupSchema.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const reducePermissionsToObject = require('../utils/reduce-perms-to-object')
3 | const h = require('../utils/helpers')
4 |
5 | const Schema = mongoose.Schema
6 |
7 | function reducePermissionsForMongoose (previous, { name, defaultValue }) {
8 | return Object.assign({}, previous, { [name]: {
9 | type: Boolean,
10 | default: defaultValue
11 | } })
12 | }
13 | // Format the master list of permissions to be easily consumable in a Mongoose Schema
14 | const permissions = reducePermissionsToObject(reducePermissionsForMongoose)
15 |
16 | const UserGroupSchema = new Schema({
17 | title: {
18 | type: String,
19 | required: true
20 | },
21 | slug: {
22 | type: String,
23 | required: true,
24 | unique: true
25 | },
26 | dateCreated: {
27 | type: Date,
28 | default: Date.now
29 | },
30 | permissions
31 | })
32 |
33 | UserGroupSchema.name = 'UserGroup'
34 |
35 | // Can't use arrow function because of (this) binding
36 | // eslint-disable-next-line func-names
37 | UserGroupSchema.pre('validate', function (next) {
38 | this.slug = h.slugify(this.title)
39 | next()
40 | })
41 |
42 | module.exports = UserGroupSchema
43 |
--------------------------------------------------------------------------------
/server/utils/FlintPlugin.js:
--------------------------------------------------------------------------------
1 | const events = require('./events')
2 | const path = require('path')
3 | const logger = require('./logger')
4 |
5 | /**
6 | * Flint Plugin Class
7 | * @property {String} name
8 | */
9 | class FlintPlugin {
10 | constructor (schema) {
11 | this.init(schema, events)
12 | }
13 |
14 | static get uid () {
15 | logger.error('A plugin forgot to set a static getter for the uid. See https://flintcms.co/docs/plugins for more information.')
16 | return false
17 | }
18 |
19 | /**
20 | * @type {String}
21 | */
22 | static get title () { return '' }
23 |
24 | /**
25 | * @type {String}
26 | */
27 | static get icon () { return path.join(__dirname, 'icon.png') }
28 |
29 | /**
30 | * @type {object}
31 | */
32 | static get model () { return {} }
33 |
34 | init () {
35 | logger.error(`Welcome to the ${this.name} plugin! You have forgotten to create your init class method. Oh well :(`)
36 | logger.error('The init method of your plugin is the entry point for Flint to know how to deal with the plugin, set up hooks and generally deal with the plugin.')
37 | }
38 | }
39 |
40 | module.exports = FlintPlugin
41 |
--------------------------------------------------------------------------------
/server/utils/create-admin-usergroup.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const reducePermissionsToObject = require('./reduce-perms-to-object')
3 |
4 | /**
5 | * Creates an admin usergroup in the database
6 | */
7 | async function createAdminUserGroup () {
8 | const UserGroup = mongoose.model('UserGroup')
9 | if (await UserGroup.findOne({ slug: 'admin' })) return false
10 |
11 | const perms = reducePermissionsToObject((p, c) => Object.assign({}, p, { [c.name]: true }), {})
12 |
13 | const adminUserGroup = new UserGroup({
14 | title: 'Admin',
15 | slug: 'admin',
16 | permissions: perms
17 | })
18 |
19 | const savedAdminUserGroup = await adminUserGroup.save()
20 |
21 | /* istanbul ignore if */
22 | if (!savedAdminUserGroup) throw new Error('Could not create admin usergroup')
23 |
24 | return savedAdminUserGroup
25 | }
26 |
27 | module.exports = createAdminUserGroup
28 |
--------------------------------------------------------------------------------
/server/utils/emails/compile.js:
--------------------------------------------------------------------------------
1 | const sass = require('node-sass')
2 | const path = require('path')
3 | const { Inky } = require('inky')
4 | const juice = require('juice')
5 | const cheerio = require('cheerio')
6 | const nunjucks = require('nunjucks')
7 |
8 | const pathToTemplates = path.resolve(__dirname, 'templates')
9 | const nun = nunjucks.configure(pathToTemplates, {
10 | noCache: process.env.NODE_ENV !== 'production'
11 | })
12 |
13 | function renderSass (file) {
14 | return new Promise((resolve, reject) => {
15 | sass.render({ file }, (err, result) => {
16 | if (err) {
17 | reject(err)
18 | } else {
19 | resolve(result)
20 | }
21 | })
22 | })
23 | }
24 |
25 | /**
26 | * Compile an email template using Nunjucks/Inky
27 | * @param {String} template - Template's file name, minus the extension
28 | * @param {Object} data - Data object to compile with
29 | * @returns {String}
30 | */
31 | async function compile (template, data) {
32 | const templatePath = path.join(pathToTemplates, `${template}.html`)
33 |
34 | const nunCompiled = await nun.render(templatePath, data)
35 |
36 | const inky = new Inky({})
37 | const cheerioString = await cheerio.load(nunCompiled)
38 | const html = await inky.releaseTheKraken(cheerioString)
39 |
40 | const pathToSCSS = path.join(pathToTemplates, 'styles', 'emails.scss')
41 | const { css } = await renderSass(pathToSCSS)
42 |
43 | const ret = await html.replace('', ``)
44 | const juiced = await juice(ret)
45 | return juiced
46 | }
47 |
48 | module.exports = compile
49 |
--------------------------------------------------------------------------------
/server/utils/emails/flintlogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/server/utils/emails/flintlogo.png
--------------------------------------------------------------------------------
/server/utils/emails/index.js:
--------------------------------------------------------------------------------
1 | const nodemailer = require('nodemailer')
2 |
3 | const transporter = nodemailer.createTransport({
4 | host: process.env.MAIL_HOST || 'smtp.gmail.com',
5 | port: process.env.MAIL_PORT || 465,
6 | auth: {
7 | user: process.env.MAIL_USER,
8 | pass: process.env.MAIL_PASS
9 | },
10 | secure: process.env.MAIL_SECURE || true
11 | })
12 |
13 | function verifyNodemailer () {
14 | return new Promise((resolve, reject) => {
15 | transporter.verify((error) => {
16 | if (error) {
17 | switch (error.code) {
18 | case 'ECONNECTION':
19 | /* istanbul ignore next */
20 | reject(new Error('[Email Service] Connection could not be established, you may be offline.'))
21 | break
22 | default:
23 | reject(error)
24 | }
25 | }
26 | resolve('[Email Service] Server can send emails!')
27 | })
28 | })
29 | }
30 |
31 | exports.verifyNodemailer = verifyNodemailer
32 |
33 | exports.transporter = transporter
34 |
--------------------------------------------------------------------------------
/server/utils/emails/sendEmail.js:
--------------------------------------------------------------------------------
1 | const { transporter } = require('.')
2 | const htmlToText = require('html-to-text')
3 | const path = require('path')
4 | const compile = require('./compile')
5 |
6 | const pathToFlintLogo = path.join(__dirname, 'flintlogo.png')
7 |
8 | /**
9 | * Send an email
10 | * @param {String} to - To whom will the email be sent
11 | * @param {String} template - Email template
12 | * @param {Object} data - Data object
13 | */
14 | async function sendEmail (to, template, data) {
15 | if (process.env.NODE_ENV === 'test') return
16 | const html = await compile(template, data)
17 | const text = htmlToText.fromString(html)
18 |
19 | transporter.sendMail({
20 | from: 'FlintCMS - Do not reply ',
21 | to,
22 | subject: data.subject,
23 | html,
24 | text,
25 | attachments: [{
26 | filename: 'flintlogo.png',
27 | path: pathToFlintLogo,
28 | cid: 'flintlogo'
29 | }]
30 | }, (err) => {
31 | if (err) {
32 | console.error(err) // eslint-disable-line no-console
33 | }
34 | })
35 | }
36 |
37 | module.exports = sendEmail
38 |
--------------------------------------------------------------------------------
/server/utils/emails/templates/forgot-password.html:
--------------------------------------------------------------------------------
1 | {% extends "layouts/base.html" %}
2 |
3 | {% block body %}
4 |
5 |
6 |
7 |
8 | Forgot your password?
9 |
10 |
11 |
12 | You're receiving this email because someone has reset the password associated with this email on {{siteName}} . Don't even worry about it! We've reset your password, so you can go ahead and create a new one using the link below.
13 |
14 |
15 |
16 | Confirm
17 |
18 |
19 |
20 |
21 | {% endblock body %}
--------------------------------------------------------------------------------
/server/utils/emails/templates/layouts/base.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 | {{subject}}
9 |
10 |
11 |
12 |
13 | {% if description %}{% endif %}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {% block body %}
25 | {% endblock %}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/server/utils/emails/templates/new-account.html:
--------------------------------------------------------------------------------
1 | {% extends "layouts/base.html" %}
2 |
3 | {% block body %}
4 |
5 |
6 |
7 |
8 | Confirm your Account
9 |
10 |
11 |
12 | An administrator of {{siteName}} has created an account for you using the FlintCMS dashboard. You will have to confirm your account and create a password using the link below.
13 |
14 |
15 |
16 | Confirm
17 |
18 |
19 |
20 |
21 | {% endblock body %}
--------------------------------------------------------------------------------
/server/utils/emails/templates/reset-password.html:
--------------------------------------------------------------------------------
1 | {% extends "layouts/base.html" %}
2 |
3 | {% block body %}
4 |
5 |
6 |
7 |
8 | Reset your Password
9 |
10 |
11 |
12 | Hi {{name}}! An administrator of {{siteName}} has reset your password for you using the FlintCMS dashboard. You will have to create a new password using the link below.
13 |
14 |
15 |
16 | Create Password
17 |
18 |
19 |
20 |
21 | {% endblock body %}
--------------------------------------------------------------------------------
/server/utils/emails/templates/styles/emails.scss:
--------------------------------------------------------------------------------
1 | @import '../../../../../app/scss/tools/variables';
2 | @import 'settings';
3 | @import 'foundation/foundation';
4 |
5 | body, html, table.body {
6 | background: $gray-300 !important;
7 | }
8 |
9 | .container.main {
10 | border-radius: $global-radius;
11 | box-shadow: 0 2px 4px rgba(black, 0.1);
12 | }
13 |
14 | .container.no-bg {
15 | background: none;
16 |
17 | small { color: $gray-300 !important; }
18 | }
19 |
20 | .line {
21 | width: 50px !important;
22 | background-color: $gray-500;
23 | }
24 |
--------------------------------------------------------------------------------
/server/utils/emails/templates/styles/foundation/_foundation.scss:
--------------------------------------------------------------------------------
1 | // Foundation for Emails by ZURB
2 | // foundation.zurb.com
3 | // Licensed under MIT Open Source
4 |
5 | @import
6 | 'util/util',
7 | 'global',
8 | 'components/normalize',
9 | 'grid/grid',
10 | 'grid/block-grid',
11 | 'components/alignment',
12 | 'components/visibility',
13 | 'components/typography',
14 | 'components/button',
15 | 'components/callout',
16 | 'components/thumbnail',
17 | 'components/menu',
18 | 'components/outlook-first',
19 | 'components/media-query';
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/server/utils/emails/templates/styles/foundation/components/_alignment.scss:
--------------------------------------------------------------------------------
1 | // Foundation for Emails by ZURB
2 | // zurb.com/ink/
3 | // Licensed under MIT Open Source
4 |
5 | ////
6 | /// @group alignment
7 | ////
8 |
9 | table,
10 | th,
11 | td,
12 | h1,
13 | h2,
14 | h3,
15 | h4,
16 | h5,
17 | h6,
18 | p,
19 | span {
20 | &.text-center {
21 | text-align: center;
22 | }
23 |
24 | &.text-left {
25 | text-align: left;
26 | }
27 |
28 | &.text-right {
29 | text-align: right;
30 | }
31 | }
32 |
33 | span.text-center {
34 | display: block;
35 | width: 100%;
36 | text-align: center;
37 | }
38 |
39 | @media only screen and (max-width: #{$global-breakpoint}) {
40 | .small-float-center {
41 | margin: 0 auto !important;
42 | float: none !important;
43 | text-align: center !important;
44 | }
45 |
46 | .small-text-center {
47 | text-align: center !important;
48 | }
49 |
50 | .small-text-left {
51 | text-align: left !important;
52 | }
53 |
54 | .small-text-right {
55 | text-align: right !important;
56 | }
57 | }
58 |
59 | img.float-left {
60 | float: left;
61 | text-align: left;
62 | }
63 |
64 | img.float-right {
65 | float: right;
66 | text-align: right;
67 | }
68 |
69 | img.float-center,
70 | img.text-center {
71 | margin: 0 auto;
72 | Margin: 0 auto;
73 | float: none;
74 | text-align: center;
75 | }
76 |
77 | table,
78 | td,
79 | th {
80 | &.float-center {
81 | margin: 0 auto;
82 | Margin: 0 auto;
83 | float: none;
84 | text-align: center;
85 | }
86 | }
87 |
88 |
89 |
--------------------------------------------------------------------------------
/server/utils/emails/templates/styles/foundation/components/_code.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/server/utils/emails/templates/styles/foundation/components/_code.scss
--------------------------------------------------------------------------------
/server/utils/emails/templates/styles/foundation/components/_menu.scss:
--------------------------------------------------------------------------------
1 | // Foundation for Emails by ZURB
2 | // zurb.com/ink/
3 | // Licensed under MIT Open Source
4 |
5 | ////
6 | /// @group menu
7 | ////
8 |
9 | /// Padding inside a menu item.
10 | /// @type Length
11 | $menu-item-padding: 10px !default;
12 |
13 | /// Right-hand spacing of items in menus with the `.simple` class.
14 | /// @type Length
15 | $menu-item-gutter: 10px !default;
16 |
17 | /// This is the color of the menu item links.
18 | /// @type Color
19 | $menu-item-color: $primary-color !default;
20 |
21 | table.menu {
22 | width: $global-width;
23 |
24 | td.menu-item,
25 | th.menu-item {
26 | padding: $menu-item-padding;
27 | padding-right: $menu-item-gutter;
28 |
29 | a {
30 | color: $menu-item-color;
31 | }
32 | }
33 | }
34 |
35 | // Doesn't work on the pesky ESPs like outlook 2000
36 | table.menu.vertical {
37 | td.menu-item,
38 | th.menu-item {
39 | padding: $menu-item-padding;
40 | padding-right: 0;
41 | display: block;
42 |
43 | a {
44 | width: 100%;
45 | }
46 | }
47 |
48 | // Nested lists need some more padding to the left
49 | td.menu-item,
50 | th.menu-item {
51 | table.menu.vertical {
52 | td.menu-item,
53 | th.menu-item {
54 | padding-left: $menu-item-padding;
55 | }
56 | }
57 | }
58 | }
59 |
60 | table.menu.text-center a {
61 | text-align: center;
62 | }
63 |
64 | //Centers the menus!
65 | .menu[align="center"] {
66 | width: auto !important;
67 | }
--------------------------------------------------------------------------------
/server/utils/emails/templates/styles/foundation/components/_outlook-first.scss:
--------------------------------------------------------------------------------
1 | // Foundation for Emails by ZURB
2 | // zurb.com/ink/
3 | // Licensed under MIT Open Source
4 |
5 | ////
6 | /// @group outlook
7 | ////
8 |
9 | body.outlook p {
10 | display: inline !important;
11 | }
12 |
--------------------------------------------------------------------------------
/server/utils/emails/templates/styles/foundation/components/_thumbnail.scss:
--------------------------------------------------------------------------------
1 | // Foundation for Emails by ZURB
2 | // zurb.com/ink/
3 | // Licensed under MIT Open Source
4 |
5 | ////
6 | /// @group thumbnail
7 | ////
8 |
9 | /// Border around thumbnail images.
10 | /// @type Border
11 | $thumbnail-border: solid 4px $white !default;
12 |
13 | /// Bottom margin for thumbnail images.
14 | /// @type Length
15 | $thumbnail-margin-bottom: $global-margin !default;
16 |
17 | /// Box shadow under thumbnail images.
18 | /// @type Shadow
19 | $thumbnail-shadow: 0 0 0 1px rgba($black, 0.2) !default;
20 |
21 | /// Box shadow under thumbnail images.
22 | /// @type Shadow
23 | $thumbnail-shadow-hover: 0 0 6px 1px rgba($primary-color, 0.5) !default;
24 |
25 | /// Transition proprties for thumbnail images.
26 | /// @type Transition
27 | $thumbnail-transition: box-shadow 200ms ease-out !default;
28 |
29 | /// Default radius for thumbnail images.
30 | /// @type Number
31 | $thumbnail-radius: $global-radius !default;
32 |
33 | /// Adds thumbnail styles to an element.
34 | .thumbnail {
35 | border: $thumbnail-border;
36 | box-shadow: $thumbnail-shadow;
37 | display: inline-block;
38 | line-height: 0;
39 | max-width: 100%;
40 | transition: $thumbnail-transition;
41 | border-radius: $thumbnail-radius;
42 | margin-bottom: $thumbnail-margin-bottom;
43 |
44 | &:hover,
45 | &:focus {
46 | box-shadow: $thumbnail-shadow-hover;
47 | }
48 | }
49 |
50 |
--------------------------------------------------------------------------------
/server/utils/emails/templates/styles/foundation/grid/_block-grid.scss:
--------------------------------------------------------------------------------
1 | // Foundation for Emails by ZURB
2 | // zurb.com/ink/
3 | // Licensed under MIT Open Source
4 |
5 | ////
6 | /// @group block-grid
7 | ////
8 |
9 | /// The highest number of `.x-up` classes available when using the block grid CSS.
10 | /// @type Number
11 | $block-grid-max: 8 !default;
12 |
13 | /// Gutter between elements in a block grid.
14 | /// @type Number
15 | $block-grid-gutter: $global-gutter !default;
16 |
17 | .block-grid {
18 | width: 100%;
19 | max-width: $global-width;
20 |
21 | td {
22 | display: inline-block;
23 | padding: $block-grid-gutter / 2;
24 | }
25 | }
26 |
27 | // Sizing classes
28 | @for $i from 2 through $block-grid-max {
29 | .up-#{$i} td {
30 | width: floor(($global-width - $i * $block-grid-gutter) / $i) !important;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/server/utils/emails/templates/styles/foundation/util/_util.scss:
--------------------------------------------------------------------------------
1 | // Foundation for Emails by ZURB
2 | // foundation.zurb.com
3 | // Licensed under MIT Open Source
4 |
5 | /// Calculates a percentage value for a grid column width.
6 | /// @access private
7 | /// @param {number} $colNumber - Column count of the column.
8 | /// @param {number} $totalColumns - Column count of the entire row.
9 | /// @returns {number} A percentage width value.
10 | @function -zf-grid-calc-pct($colNumber, $totalColumns) {
11 | @return floor(percentage(($colNumber / $totalColumns)) * 1000000) / 1000000;
12 | }
13 |
14 | /// Calculates a pixel value for a grid column width.
15 | /// @access private
16 | /// @param {number} $columnNumber - Column count of the column.
17 | /// @param {number} $totalColumns - Column count of the entire row.
18 | /// @param {number} $containerWidth - Width of the surrounding container, in pixels.
19 | /// @returns {number} A pixel width value.
20 | @function -zf-grid-calc-px($columnNumber, $totalColumns, $containerWidth) {
21 | @return ($containerWidth / $totalColumns * $columnNumber - $global-gutter);
22 | }
23 |
--------------------------------------------------------------------------------
/server/utils/emit-socket-event.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Broadcasts a socket event
3 | * @param {Object} param0
4 | * @param {Object} param0.io
5 | * @param {Object} param0.req
6 | * @param {String} event
7 | * @param {Any} payload
8 | */
9 | function emitSocketEvent ({ io, req }, event, payload) {
10 | const socket = io.sockets.connected[req.body.socket]
11 | if (socket) socket.broadcast.emit(event, payload)
12 | }
13 |
14 | module.exports = emitSocketEvent
15 |
--------------------------------------------------------------------------------
/server/utils/events.js:
--------------------------------------------------------------------------------
1 | const EventEmitter = require('events')
2 |
3 | EventEmitter.defaultMaxListeners = 40
4 |
5 | class FlintEmitter extends EventEmitter {}
6 |
7 | const flintEmitter = new FlintEmitter()
8 |
9 | module.exports = flintEmitter
10 |
--------------------------------------------------------------------------------
/server/utils/four-oh-four-handler.js:
--------------------------------------------------------------------------------
1 | const compile = require('./compile')
2 |
3 | module.exports = async (req, res) => {
4 | const compiled = await compile('404')
5 | if (compiled === 'no-template') return res.redirect('/admin/error?r=no-template&p=404&t=404')
6 | return res.status(404).send(compiled)
7 | }
8 |
--------------------------------------------------------------------------------
/server/utils/get-asset-details.js:
--------------------------------------------------------------------------------
1 | const jimp = require('jimp')
2 | const { promisify } = require('util')
3 | const fs = require('fs')
4 |
5 | const statAsync = promisify(fs.stat)
6 |
7 | /**
8 | * Gets the mimetype, width, height and file size of an asset.
9 | * @param {String} pathToFile
10 | *
11 | * @typedef {Object} AssetDetails
12 | * @property {string} mimetype - MimeType of the asset
13 | * @property {number} width - Width of the asset
14 | * @property {number} height - Height of the asset
15 | * @property {number} size - File size in bytes
16 | *
17 | * @returns {AssetDetails}
18 | */
19 | async function getAssetDetails (pathToFile) {
20 | const { size } = await statAsync(pathToFile).catch(err => new Error(err))
21 | const {
22 | _originalMime: mimetype,
23 | bitmap: { width, height }
24 | } = await jimp.read(pathToFile)
25 |
26 | return { mimetype, width, height, size }
27 | }
28 |
29 | module.exports = getAssetDetails
30 |
--------------------------------------------------------------------------------
/server/utils/get-entry-data.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | const { graphql } = require('graphql')
4 | const schema = require('../graphql')
5 |
6 | /**
7 | * Query the database for the Entry data
8 | * @param {Object} entry
9 | * @param {String} entry.slug
10 | * @param {String} entry.section
11 | * @returns {Object|Boolean} Entry object or `false` if there is no Entry
12 | */
13 | async function getEntryData ({ slug, section }) {
14 | const query = `query ($slug: String!, $status: String!, $sectionSlug: String!) {
15 | entry (slug: $slug, status: $status, sectionSlug: $sectionSlug) {
16 | _id
17 | title
18 | slug
19 | status
20 | dateCreated
21 | section
22 | template
23 | author {
24 | name {
25 | first
26 | last
27 | }
28 | username
29 | email
30 | }
31 | fields {
32 | handle
33 | value
34 | }
35 | }
36 | }`
37 |
38 | const variables = {
39 | slug,
40 | status: 'live',
41 | sectionSlug: section
42 | }
43 |
44 | const { data } = await graphql(schema, query, null, null, variables)
45 |
46 | if (data.entry === undefined || data.entry === null) {
47 | return false
48 | }
49 |
50 | return data.entry
51 | }
52 |
53 | module.exports = getEntryData
54 |
--------------------------------------------------------------------------------
/server/utils/get-user-permissions.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const reducePermissionsToObject = require('./reduce-perms-to-object')
3 |
4 | const User = mongoose.model('User')
5 |
6 | /**
7 | * Gets an object of the users's permissions based on their User Group
8 | * @param {String} _id - Mongo ID of the User
9 | * @returns {Object} Object of the various booleans of permissions
10 | */
11 | async function getUserPermissions (_id) {
12 | if (!_id) {
13 | return reducePermissionsToObject((p, c) => Object.assign({}, p, { [c.name]: true }), {})
14 | }
15 |
16 | const user = await User.findById(_id).exec()
17 | const perms = await user.getPermissions()
18 |
19 | if (!perms) throw new Error('User permissions could not be found.')
20 | return perms
21 | }
22 |
23 | module.exports = getUserPermissions
24 |
--------------------------------------------------------------------------------
/server/utils/handle-compile-error-routes.js:
--------------------------------------------------------------------------------
1 | const fourOhFourHandler = require('./four-oh-four-handler')
2 |
3 | /**
4 | * Handles compilation errors
5 | * like when a template or page does not exist
6 | * @param {object} req - Request object
7 | * @param {object} res - Response object
8 | * @param {String} type - Type of error or a compiled HTML string
9 | * @param {String} [template] - Template that the error happened with
10 | */
11 | function handleCompileErrorRoutes (req, res, type, template) {
12 | switch (type) {
13 | case 'no-html':
14 | case 'no-template':
15 | case 'no-homepage': {
16 | const obj = {
17 | r: type,
18 | p: req.originalUrl
19 | }
20 |
21 | if (template) obj.t = template
22 |
23 | const queryString = Object.keys(obj).reduce((prev, curr, i) => {
24 | let queryParam = `${curr}=${obj[curr]}`
25 | if (i !== 0) queryParam = `&${queryParam}`
26 | return `${prev}${queryParam}`
27 | }, '')
28 |
29 | return res.redirect(`/admin/error?${queryString}`)
30 | }
31 | case 'no-exist':
32 | return fourOhFourHandler(req, res)
33 | default:
34 | res.set('Cache-Control', 'public, max-age=1200, s-maxage=3200')
35 | return res.send(type)
36 | }
37 | }
38 |
39 | module.exports = handleCompileErrorRoutes
40 |
--------------------------------------------------------------------------------
/server/utils/helpers.js:
--------------------------------------------------------------------------------
1 | const helpers = {
2 | /**
3 | * Express middleware to determine if the user is logged in
4 | * @param {Object} req
5 | * @param {Object} res
6 | * @param {Function} next
7 | */
8 | loggedIn (req, res, next) {
9 | if (!req.isAuthenticated() && !req.user) {
10 | res.json({ status: 401, redirect: '/admin/login' })
11 | } else {
12 | next()
13 | }
14 | },
15 | /**
16 | * Converts a String to a slug
17 | * @param {String} str
18 | * @returns {String}
19 | */
20 | slugify (str) {
21 | return str
22 | .toLowerCase()
23 | .replace(/^\s+|\s+$/g, '') // Trim leading/trailing whitespace
24 | .replace(/[-\s]+/g, '-') // Replace spaces with dashes
25 | .replace(/[^a-z0-9-]/g, '') // Remove disallowed symbols
26 | .replace(/--+/g, '-')
27 | },
28 | /**
29 | * Reduces an array of objects to one object using the key value pair parameters
30 | * @param {Array} arr
31 | * @param {String} key
32 | * @param {String} value
33 | * @param {Object} start
34 | * @returns {Object}
35 | */
36 | reduceToObj (arr, key, value, start = {}) {
37 | return arr
38 | .reduce((prev, curr) =>
39 | Object.assign({}, prev, { [curr[key]]: curr[value] }), start)
40 | },
41 | /**
42 | * Capitalizes the first character of a string
43 | * @param {String} str - String to capitalize
44 | * @returns {String}
45 | */
46 | capitalizeFirstChar (str) {
47 | return str.substring(0, 1).toUpperCase() + str.substring(1)
48 | }
49 | }
50 |
51 | module.exports = helpers
52 |
--------------------------------------------------------------------------------
/server/utils/log.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path')
3 | const logger = require('./logger')
4 | const pathToLog = path.join(global.FLINT.logsPath, 'flint.log')
5 | const stream = fs.createWriteStream(pathToLog, { flags: 'a' })
6 |
7 | /**
8 | * Logs a string to the console and adds it to the Flint log file.
9 | * @param {String} str - String to log to the console and the flint log file.
10 | * @param {Boolean} [prependTimestamp=true] - Prepend a timestamp to the string in the log file.
11 | * @returns {String} - Returns the string that was logged.
12 | */
13 | function log (str, prependTimestamp = true) {
14 | /* istanbul ignore else */
15 | if (process.env.NODE_ENV === 'test') {
16 | return str
17 | } else {
18 | logger.info(str)
19 | let string = str
20 |
21 | if (prependTimestamp) {
22 | const timestamp = new Date().toISOString()
23 | string = `[${timestamp}] - ${string}`
24 | }
25 |
26 | stream.write(`${string}\n`)
27 | return str
28 | }
29 | }
30 |
31 | module.exports = log
32 |
--------------------------------------------------------------------------------
/server/utils/logger.js:
--------------------------------------------------------------------------------
1 | const Logger = require('bunyan')
2 | const bunyanFormat = require('bunyan-format')
3 |
4 | function toBunyanLogLevel (level) {
5 | switch (level) {
6 | case 'info':
7 | case 'trace':
8 | case 'debug':
9 | case 'warn':
10 | case 'error':
11 | case 'fatal':
12 | case undefined:
13 | return level
14 | default:
15 | throw new Error('Invalid log level')
16 | }
17 | }
18 |
19 | function toBunyanFormat (format) {
20 | switch (format) {
21 | case 'short':
22 | case 'long':
23 | case 'simple':
24 | case 'json':
25 | case 'bunyan':
26 | case undefined:
27 | return format
28 | default:
29 | throw new Error('Invalid log format')
30 | }
31 | }
32 |
33 | const logger = new Logger({
34 | level: toBunyanLogLevel(process.env.LOG_LEVEL || 'info'),
35 | name: 'flintcms',
36 | stream: new bunyanFormat({ outputMode: toBunyanFormat(process.env.LOG_FORMAT || 'short') }) // eslint-disable-line
37 | })
38 |
39 | module.exports = logger
40 |
--------------------------------------------------------------------------------
/server/utils/nunjucks.js:
--------------------------------------------------------------------------------
1 | const nunjucks = require('nunjucks')
2 | const dateFilter = require('nunjucks-date-filter')
3 |
4 | /**
5 | * Returns the value of a field in an entry, or null
6 | * if the field does not exist.
7 | * @param {object} entry - Entry object
8 | * @param {string} handle - Handle of the target field
9 | * @returns {string|null}
10 | */
11 | function fieldFilter (entry, handle) {
12 | const { fields } = entry
13 | const fieldObj = fields.find(field => field.handle === handle)
14 | if (!fieldObj) return null
15 | return fieldObj.value
16 | }
17 |
18 | module.exports = (pathToTemplates) => {
19 | const nun = nunjucks.configure(pathToTemplates, {
20 | noCache: process.env.NODE_ENV !== 'production',
21 | autoescape: false
22 | })
23 |
24 | Object.keys(global.FLINT).forEach((key) => {
25 | if (key === 'nun') return
26 | nun.addGlobal(key, global.FLINT[key])
27 | })
28 |
29 | nun.addGlobal('getContext', () => this.ctx)
30 |
31 | nun.addFilter('json', obj => `${JSON.stringify(obj, null, 2)}
`)
32 | nun.addFilter('date', dateFilter)
33 | nun.addFilter('field', fieldFilter)
34 |
35 | nunjucks.precompile(pathToTemplates, { env: nun })
36 |
37 | return nun
38 | }
39 |
--------------------------------------------------------------------------------
/server/utils/passport.js:
--------------------------------------------------------------------------------
1 | const LocalStrategy = require('passport-local').Strategy
2 | const mongoose = require('mongoose')
3 |
4 | const User = mongoose.model('User')
5 |
6 | const strategyOptions = {
7 | usernameField: 'email',
8 | passwordField: 'password',
9 | passReqToCallback: true
10 | }
11 |
12 | module.exports = (passport) => {
13 | // Serialize user
14 | passport.serializeUser((user, done) => {
15 | done(null, user.id)
16 | })
17 |
18 | // Deserialize user
19 | passport.deserializeUser((id, done) => {
20 | User.findById(id, { password: 0 }, (err, user) => {
21 | done(err, user)
22 | })
23 | })
24 |
25 | passport.use('local-signup', new LocalStrategy(strategyOptions, (req, email, password, done) => {
26 | process.nextTick(async () => {
27 | const foundUser = await User.findOne({ email })
28 | if (foundUser) return done(null, false)
29 |
30 | const newUser = new User(req.body)
31 |
32 | newUser.password = newUser.generateHash(password)
33 |
34 | const savedUser = await newUser.save()
35 | if (!savedUser) throw new Error('Could not save the user!')
36 | return done(null, savedUser)
37 | })
38 | }))
39 |
40 | passport.use('local-login', new LocalStrategy(strategyOptions, async (req, email, password, done) => {
41 | const user = await User.findOne({ email }).populate('usergroup')
42 | if (!user) return done(null, false)
43 | if (user.token) return done(null, false)
44 | if (!user.validateHash(password)) return done(null, false)
45 | return done(null, user)
46 | }))
47 | }
48 |
--------------------------------------------------------------------------------
/server/utils/public-registration.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const passport = require('passport')
3 | const mongoose = require('mongoose')
4 |
5 | const Site = mongoose.model('Site')
6 |
7 | const strategyOptions = {
8 | passReqToCallback: true,
9 | successRedirect: '/admin'
10 | }
11 |
12 | async function publicMiddleware (req, res, next) {
13 | const site = await Site.findOne()
14 | if (site.allowPublicRegistration) {
15 | next()
16 | } else {
17 | res.redirect('/admin/login')
18 | }
19 | }
20 |
21 | module.exports = () => {
22 | const router = express.Router()
23 |
24 | if (global.FLINT.signupRoute) {
25 | router.post(global.FLINT.signupRoute, publicMiddleware, passport.authenticate('local-signup'))
26 | }
27 |
28 | if (global.FLINT.loginRoute) {
29 | router.post(global.FLINT.loginRoute, passport.authenticate('local-login', strategyOptions))
30 | }
31 |
32 | return router
33 | }
34 |
--------------------------------------------------------------------------------
/server/utils/reduce-perms-to-object.js:
--------------------------------------------------------------------------------
1 | const perms = require('./permissions.json')
2 |
3 | /**
4 | * Reduces the permissions object to one that is easier to format
5 | * @param {Function} reducer - Reducer to format the returned objects
6 | * @returns {Object}
7 | */
8 | function reducePermissionsToObject (reducer) {
9 | return Object.keys(perms).reduce((prev, curr) =>
10 | Object.assign({}, prev, { [curr]: perms[curr].reduce(reducer, {}) }), {})
11 | }
12 |
13 | module.exports = reducePermissionsToObject
14 |
--------------------------------------------------------------------------------
/server/utils/scaffold.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const { promisify } = require('util')
3 |
4 | const mkdirAsync = promisify(fs.mkdir)
5 | /**
6 | * Creates a directory at the given path if it does not already exist.
7 | * @param {String} path - Path to the directory
8 | * @returns {Promise} - Path to the directory
9 | */
10 | function scaffold (path) {
11 | return new Promise((resolve) => {
12 | if (!fs.existsSync(path)) {
13 | return mkdirAsync(path).then(() => resolve(path))
14 | }
15 | return resolve(path)
16 | })
17 | }
18 |
19 | module.exports = scaffold
20 |
--------------------------------------------------------------------------------
/server/utils/template-routes.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const express = require('express')
3 | const compile = require('./compile')
4 | const getEntryData = require('./get-entry-data')
5 | const handleCompileErrorRoutes = require('./handle-compile-error-routes')
6 |
7 | const Page = mongoose.model('Page')
8 | const router = express.Router()
9 |
10 | router.get('/', async (req, res) => {
11 | const homepage = await Page.findOne({ homepage: true }).lean().exec()
12 | if (!homepage) return handleCompileErrorRoutes(req, res, 'no-homepage')
13 |
14 | const compiled = await compile(homepage.template, homepage)
15 | return res.end(compiled)
16 | })
17 |
18 | router.get('*', async (req, res, next) => {
19 | const page = await Page.findOne({ route: req.originalUrl }).lean().exec()
20 | if (!page) return next()
21 |
22 | const compiled = await compile(page.template, page)
23 | return handleCompileErrorRoutes(req, res, compiled, page.template)
24 | })
25 |
26 | router.get('/:section/:slug', async (req, res, next) => {
27 | const entry = await getEntryData(req.params)
28 | if (!entry) return next()
29 |
30 | const compiled = await compile(entry.template, entry)
31 | return handleCompileErrorRoutes(req, res, compiled, entry.template)
32 | })
33 |
34 | router.use((req, res) => handleCompileErrorRoutes(req, res, 'no-exist'))
35 |
36 | module.exports = router
37 |
--------------------------------------------------------------------------------
/server/utils/update-site-config.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 |
3 | async function updateSiteConfig () {
4 | const Site = mongoose.model('Site')
5 | const site = await Site.findOne().exec()
6 | if (!site) {
7 | const newSite = new Site(global.FLINT)
8 | const savedSite = await newSite.save()
9 |
10 | /* istanbul ignore if */
11 | if (!savedSite) throw new Error('Could not save the site config to the database.')
12 | return savedSite
13 | }
14 |
15 | const updatedSite = await Site.findByIdAndUpdate(site._id, global.FLINT, { new: true }).exec()
16 |
17 | /* istanbul ignore if */
18 | if (!updatedSite) throw new Error('Could not save the site config to the database.')
19 | return updatedSite
20 | }
21 |
22 | module.exports = updateSiteConfig
23 |
--------------------------------------------------------------------------------
/server/utils/validate-env-variables.js:
--------------------------------------------------------------------------------
1 | const variables = [
2 | 'DB_HOST',
3 | 'SESSION_SECRET'
4 | ]
5 |
6 | /**
7 | * Ensures that an array of keys exist in the process.env object.
8 | * @param {String[]} vars - process.env variables to check for
9 | * @returns {String[]} - Array of keys that *are not* defined in process.env
10 | */
11 | function validateEnvVariables ({ vars = variables, log }) {
12 | const missingEnvVariables = vars.filter(v => process.env[v] === undefined || process.env[v] === '')
13 |
14 | missingEnvVariables.forEach(v => log.error(`Missing the ${v} variable in your .env file!`))
15 | return missingEnvVariables
16 | }
17 |
18 | module.exports = validateEnvVariables
19 |
--------------------------------------------------------------------------------
/test/fixtures/images/image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/test/fixtures/images/image.png
--------------------------------------------------------------------------------
/test/fixtures/images/image2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/test/fixtures/images/image2.png
--------------------------------------------------------------------------------
/test/fixtures/logs/flint.log:
--------------------------------------------------------------------------------
1 | This
2 | is
3 | a
4 | log
--------------------------------------------------------------------------------
/test/fixtures/logs/http-requests.log:
--------------------------------------------------------------------------------
1 | This
2 | is
3 | a
4 | log
--------------------------------------------------------------------------------
/test/fixtures/plugins/ConsolePlugin.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable class-methods-use-this */
2 |
3 | const Flint = require('../../../index')
4 | const path = require('path')
5 |
6 | const FlintPlugin = Flint.FlintPlugin
7 |
8 | class ConsolePlugin extends FlintPlugin {
9 | static get uid () { return 'console-plugin' }
10 | static get title () { return 'Console Plugin' }
11 | static get version () { return '1.0.0' }
12 | static get icon () { return path.join(__dirname, 'icon.png') }
13 |
14 | init () {}
15 | }
16 |
17 | module.exports = ConsolePlugin
18 |
--------------------------------------------------------------------------------
/test/fixtures/plugins/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/test/fixtures/plugins/icon.png
--------------------------------------------------------------------------------
/test/fixtures/scss/main.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: red; }
3 |
--------------------------------------------------------------------------------
/test/fixtures/scss/main.scss:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: red;
3 | }
4 |
--------------------------------------------------------------------------------
/test/fixtures/templates/404.njk:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 404!
8 |
9 |
10 | Oh no, you're lost!
11 |
12 |
--------------------------------------------------------------------------------
/test/fixtures/templates/404.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 404!
8 |
9 |
10 | Oh no, you're lost!
11 |
12 |
--------------------------------------------------------------------------------
/test/fixtures/templates/empty/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JasonEtco/flintcms/f5a1443121d76b82d8875072b2d4759d6ea7241b/test/fixtures/templates/empty/.gitkeep
--------------------------------------------------------------------------------
/test/fixtures/templates/entry.njk:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ this.title }}
8 |
9 |
10 | {{ this.title }}
11 |
12 |
--------------------------------------------------------------------------------
/test/fixtures/templates/entry.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ this.title }}
8 |
9 |
10 | {{ this.title }}
11 |
12 |
--------------------------------------------------------------------------------
/test/fixtures/templates/fieldFilter.njk:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 | {% for entry in flint.entries %}{{ entry | field('simpleText') }}{% endfor %}
11 |
12 |
--------------------------------------------------------------------------------
/test/fixtures/templates/fieldFilter.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 | I am working!
11 |
12 |
--------------------------------------------------------------------------------
/test/fixtures/templates/index.njk:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Pizza!
8 |
9 |
10 | Hello!
11 |
12 |
--------------------------------------------------------------------------------
/test/fixtures/templates/index.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Pizza!
8 |
9 |
10 | Hello!
11 |
12 |
--------------------------------------------------------------------------------
/test/fixtures/templates/page-with-vars.njk:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ flint.site.siteName }}
8 |
9 |
10 | {{ this.title }}
11 |
12 |
--------------------------------------------------------------------------------
/test/fixtures/templates/page-with-vars.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Example site title
8 |
9 |
10 | Page with vars
11 |
12 |
--------------------------------------------------------------------------------
/test/index.test.js:
--------------------------------------------------------------------------------
1 | const Flint = require('../index.js')
2 | const request = require('supertest')
3 | const mongoose = require('mongoose')
4 |
5 | describe('server', () => {
6 | let server
7 |
8 | beforeAll(async () => {
9 | const flintServer = new Flint({ listen: false })
10 | server = await flintServer.startServer()
11 | })
12 |
13 | describe('GET /ping', () => {
14 | it('returns a 200 response', (done) => {
15 | request(server).get('/ping').expect(200, 'PONG', done)
16 | })
17 | })
18 |
19 | describe('Plugin object', () => {
20 | it('returns the plugin object', () => {
21 | const FlintPlugin = new Flint.FlintPlugin()
22 | expect(typeof FlintPlugin).toBe('object')
23 | })
24 | })
25 |
26 | afterAll(() => mongoose.disconnect())
27 | })
28 |
--------------------------------------------------------------------------------
/test/mocks/assets.js:
--------------------------------------------------------------------------------
1 | module.exports = [{
2 | _id: '5926e863bd887652382edfe9',
3 | title: 'Image',
4 | extension: 'png',
5 | filename: 'image.png',
6 | dateCreated: 1497819215947,
7 | width: 100,
8 | height: 100,
9 | size: 200,
10 | mimetype: 'image/png'
11 | }, {
12 | _id: '5946e833bd887652382edfe9',
13 | title: 'Image Two',
14 | extension: 'png',
15 | filename: 'image2.png',
16 | dateCreated: 1497819225947,
17 | width: 100,
18 | height: 100,
19 | size: 200,
20 | mimetype: 'image/png'
21 | }]
22 |
--------------------------------------------------------------------------------
/test/mocks/entries.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | {
3 | _id: '591e63abe9e41f00113de131',
4 | slug: 'test-entry',
5 | title: 'Test Entry',
6 | section: '58b787fd28044618a8f7a737',
7 | author: '589a12b5a08e0c2f24ece4e8',
8 | dateCreated: 1488853901010,
9 | fields: []
10 | },
11 | {
12 | _id: '57be1b901810931d043d9fc6',
13 | title: 'Test Entry Two',
14 | slug: 'test-entry-two',
15 | section: '58b787fd28044618a8f7a737',
16 | author: '589a12b5a08e0c2f24ece4e8',
17 | dateCreated: 1488853902010,
18 | status: 'live',
19 | fields: []
20 | },
21 | {
22 | _id: '58be1b901810931d043d9fc1',
23 | title: 'I\'m a draft!',
24 | section: '58b787fd28044618a8f7a737',
25 | author: '589a12b5a08e0c2f24ece4e8',
26 | dateCreated: 1488853903010,
27 | status: 'draft',
28 | fields: []
29 | },
30 | {
31 | _id: '59be1b901810931d043d9fc1',
32 | title: 'I am always live!',
33 | slug: 'i-am-always-live',
34 | section: '58b787fd28044618a8f7a737',
35 | author: '59383c903e13393788eb01b2',
36 | dateCreated: 1488853904010,
37 | status: 'live',
38 | fields: []
39 | },
40 | {
41 | _id: '591e63abe8e41f00113de131',
42 | slug: 'field-filter',
43 | title: 'Field Filter',
44 | section: '58b787fd28044618a8f7a537',
45 | author: '589a12b5a08e0c2f24ece4e8',
46 | dateCreated: 1488853905010,
47 | status: 'live',
48 | fields: [{
49 | fieldId: '59376a4feef58d4a74e2fdcd',
50 | handle: 'simpleText',
51 | value: 'I am working!'
52 | }]
53 | }
54 | ]
55 |
--------------------------------------------------------------------------------
/test/mocks/fields.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | {
3 | _id: '59376a4feef58d4a74e2fdcd',
4 | handle: 'simpleText',
5 | slug: 'simple-text',
6 | title: 'Simple Text',
7 | required: false,
8 | type: 'Text',
9 | options: {
10 | placeholder: 'Example!'
11 | },
12 | dateCreated: 1496803919903
13 | }
14 | ]
15 |
--------------------------------------------------------------------------------
/test/mocks/index.js:
--------------------------------------------------------------------------------
1 | const assets = require('./assets')
2 | const entries = require('./entries')
3 | const sections = require('./sections')
4 | const usergroups = require('./usergroups')
5 | const site = require('./site')
6 | const user = require('./user')
7 | const users = require('./users')
8 | const pages = require('./pages')
9 | const fields = require('./fields')
10 | const plugins = require('./plugins')
11 |
12 | module.exports = {
13 | assets,
14 | entries,
15 | sections,
16 | usergroups,
17 | site,
18 | user,
19 | users,
20 | pages,
21 | fields,
22 | plugins
23 | }
24 |
--------------------------------------------------------------------------------
/test/mocks/pages.js:
--------------------------------------------------------------------------------
1 | module.exports = [{
2 | _id: '5946e863bd887652381ecfe9',
3 | handle: 'homepage',
4 | slug: 'homepage',
5 | route: '/',
6 | title: 'Homepage',
7 | template: 'index.njk',
8 | homepage: true,
9 | fields: [],
10 | fieldLayout: ['5946e850bd887652381ecfe8'],
11 | dateCreated: 1497819235747
12 | }, {
13 | _id: '5946e863bd887652381edfe9',
14 | handle: 'noTemplate',
15 | slug: 'no-template',
16 | route: '/no-template',
17 | title: 'No Template',
18 | template: 'template-no-exist',
19 | homepage: false,
20 | fields: [],
21 | fieldLayout: ['5946e850bd887652381ecfe8'],
22 | dateCreated: 1497819235847
23 | }, {
24 | _id: '5946e863bd887652382edfe9',
25 | slug: 'page-with-vars',
26 | handle: 'pageWithVars',
27 | route: '/page-with-vars',
28 | title: 'Page with vars',
29 | template: 'page-with-vars',
30 | homepage: false,
31 | fields: [],
32 | fieldLayout: ['5946e850bd887652381ecfe8'],
33 | dateCreated: 1497819235947
34 | }]
35 |
--------------------------------------------------------------------------------
/test/mocks/sections.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | {
3 | _id: '58b787fd28044618a8f7a737',
4 | title: 'Test Section',
5 | slug: 'test-section',
6 | handle: 'testSection',
7 | template: 'entry',
8 | fields: [
9 | '58a232588397e21c8c8c8828',
10 | '58a37f8feae5082628cd21e0'
11 | ],
12 | dateCreated: 1488422908431
13 | },
14 | {
15 | _id: '58b787fd28044618a7f7a737',
16 | title: 'Fixed Section',
17 | slug: 'fixed-section',
18 | handle: 'fixedSection',
19 | template: 'entry',
20 | fields: [
21 | '58a232588397e21c8c8c8828',
22 | '58a37f8feae5082628cd21e0'
23 | ],
24 | dateCreated: 1488422908432
25 | },
26 | {
27 | _id: '58b787fd28044618a8f7a537',
28 | title: 'Field Filter',
29 | slug: 'field-filter',
30 | handle: 'fieldFilter',
31 | template: 'fieldFilter',
32 | fields: [
33 | '58a232588397e21c8c8c8828',
34 | '58a37f8feae5082628cd21e0'
35 | ],
36 | dateCreated: 1488422908433
37 | }
38 | ]
39 |
--------------------------------------------------------------------------------
/test/mocks/site.js:
--------------------------------------------------------------------------------
1 | module.exports = [{
2 | logsPath: '/app/logs',
3 | templatePath: '/app/templates',
4 | scssPath: '/app/scss',
5 | publicPath: '/app/public',
6 | plugins: [],
7 | debugMode: true,
8 | appDir: '/app',
9 | scssEntryPoint: 'main.scss',
10 | allowPublicRegistration: false,
11 | siteUrl: 'https://example.com',
12 | siteName: 'Example site title',
13 | scssIncludePaths: [
14 | 'node_modules'
15 | ],
16 | style: '',
17 | defaultUserGroup: '592a74034a0a9b372c3bff9c'
18 | }]
19 |
--------------------------------------------------------------------------------
/test/mocks/user.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | email: 'user@user.com',
3 | username: 'testeroni',
4 | name: {
5 | first: 'Tester',
6 | last: 'McGee'
7 | },
8 | password: 'pass11'
9 | }
10 |
--------------------------------------------------------------------------------
/test/mocks/users.js:
--------------------------------------------------------------------------------
1 | module.exports = [
2 | {
3 | _id: '59383c903e13393788eb01b2',
4 | username: 'anothaone',
5 | dateCreated: 1486492331619,
6 | // password: 'password',
7 | password: '$2a$06$J.cW7gg/AuEl0W2qAF81hexOyPEmqorQVY1A7p8OQ8k8Fuqo2AysK',
8 | name: {
9 | first: 'Example',
10 | last: 'Userstein'
11 | },
12 | email: 'anothaone@anothaone.com',
13 | image: 'default_user.png',
14 | usergroup: '592a74034a0a9b372c3bff9c'
15 | },
16 | {
17 | _id: '589a12b5a08e0c2f24ece4e8',
18 | username: 'userstein',
19 | dateCreated: 1486492341619,
20 | // password: 'password',
21 | password: '$2a$06$J.cW7gg/AuEl0W2qAF81hexOyPEmqorQVY1A7p8OQ8k8Fuqo2AysK',
22 | name: {
23 | first: 'Example',
24 | last: 'Userstein'
25 | },
26 | email: 'example@userstein.com',
27 | image: 'default_user.png',
28 | usergroup: '592a74034a0a9b372c3bff9c',
29 | token: 'TOKEN'
30 | }
31 | ]
32 |
--------------------------------------------------------------------------------
/test/populatedb.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const mocks = require('./mocks')
3 |
4 | async function wipeDB (collections) {
5 | return collections.map(async ({ model }) => {
6 | const Model = mongoose.model(model)
7 | return Model.remove()
8 | })
9 | }
10 |
11 | const collections = [
12 | { model: 'Plugin', mocks: mocks.plugins, collection: 'plugins' },
13 | { model: 'UserGroup', mocks: mocks.usergroups, collection: 'usergroups' },
14 | { model: 'User', mocks: mocks.users, collection: 'users' },
15 | { model: 'Section', mocks: mocks.sections, collection: 'sections' },
16 | { model: 'Entry', mocks: mocks.entries, collection: 'entries' },
17 | { model: 'Field', mocks: mocks.fields, collection: 'fields' },
18 | { model: 'Page', mocks: mocks.pages, collection: 'pages' },
19 | { model: 'Site', mocks: mocks.site, collection: 'sites' },
20 | { model: 'Asset', mocks: mocks.assets, collection: 'assets' }
21 | ]
22 |
23 | const addModel = async (modelName) => {
24 | const { mocks: mockData } = collections.find(obj => obj.model === modelName)
25 | const Model = mongoose.model(modelName)
26 | const done = await Model.create(mockData)
27 | return done
28 | }
29 |
30 | module.exports = async () => {
31 | await wipeDB(collections)
32 |
33 | await addModel('Field')
34 | await addModel('UserGroup')
35 | await addModel('Section')
36 | await addModel('Entry')
37 | await addModel('Page')
38 | await addModel('Asset')
39 | await addModel('Site')
40 | await addModel('Plugin')
41 | return addModel('User')
42 | }
43 |
--------------------------------------------------------------------------------
/test/server/apps/admin.test.js:
--------------------------------------------------------------------------------
1 | const Flint = require('../../../index.js')
2 | const request = require('supertest')
3 | const mongoose = require('mongoose')
4 |
5 | describe('admin routes', () => {
6 | let server
7 |
8 | beforeAll(async () => {
9 | const flintServer = new Flint({ listen: false })
10 | server = await flintServer.startServer()
11 | })
12 |
13 | it('returns the correct response for /admin/login', async () => {
14 | const res = await request(server).get('/admin/login')
15 | expect(res.status).toBe(200)
16 | expect(res.text).toContain('')
17 | })
18 |
19 | it('returns the correct response for any route', async () => {
20 | const res = await request(server).get('/admin/asdfsdfsadfsadf')
21 | expect(res.status).toBe(200)
22 | expect(res.text).toContain('')
23 | })
24 |
25 | afterAll(() => mongoose.disconnect())
26 | })
27 |
--------------------------------------------------------------------------------
/test/server/apps/common.js:
--------------------------------------------------------------------------------
1 | const Flint = require('../../../')
2 | const mocks = require('../../mocks')
3 | const populateDB = require('../../populatedb')
4 | const supertest = require('supertest')
5 | const mongoose = require('mongoose')
6 |
7 | exports.before = async function before (plugins = []) {
8 | const flintServer = new Flint({ listen: false, plugins })
9 | const server = await flintServer.startServer()
10 | const agent = supertest.agent(server)
11 |
12 | await populateDB()
13 |
14 | return agent
15 | }
16 |
17 | exports.setNonAdmin = async function setNonAdmin (agent) {
18 | const res = await agent
19 | .post('/graphql')
20 | .send({
21 | query: `mutation ($_id: ID!, $data: UserInput!) {
22 | updateUser (_id: $_id, data: $data) {
23 | usergroup {
24 | _id
25 | slug
26 | }
27 | }
28 | }`,
29 | variables: {
30 | _id: mocks.users[0]._id,
31 | data: {
32 | email: mocks.users[0].email,
33 | username: mocks.users[0].username,
34 | usergroup: mocks.usergroups[2]._id
35 | }
36 | }
37 | })
38 | expect(res.body).toEqual({
39 | data: {
40 | updateUser: {
41 | usergroup: {
42 | _id: mocks.usergroups[2]._id,
43 | slug: mocks.usergroups[2].slug
44 | }
45 | }
46 | }
47 | })
48 | }
49 |
50 | exports.setAdmin = function setAdmin () {
51 | const User = mongoose.model('User')
52 | return User.findByIdAndUpdate(mocks.users[0]._id, {
53 | $set: { usergroup: mocks.usergroups[0]._id }
54 | }).exec()
55 | }
56 |
--------------------------------------------------------------------------------
/test/server/apps/plugins.test.js:
--------------------------------------------------------------------------------
1 | const mocks = require('../../mocks')
2 | const mongoose = require('mongoose')
3 | const ConsolePlugin = require('../../fixtures/plugins/ConsolePlugin')
4 | const common = require('./common')
5 |
6 | describe('Plugin system', () => {
7 | let agent
8 |
9 | beforeAll(async () => {
10 | agent = await common.before([ConsolePlugin])
11 | })
12 |
13 | it('returns a list of plugins', async () => {
14 | const res = await agent
15 | .post('/graphql')
16 | .send({
17 | query: `{
18 | plugins {
19 | title
20 | name
21 | version
22 | uid
23 | }
24 | }`
25 | })
26 |
27 | expect(res.body).toEqual({
28 | data: {
29 | plugins: [
30 | {
31 | uid: mocks.plugins[0].uid,
32 | version: mocks.plugins[0].version,
33 | name: mocks.plugins[0].name,
34 | title: mocks.plugins[0].title
35 | }
36 | ]
37 | }
38 | })
39 | })
40 |
41 | afterAll(() => mongoose.disconnect())
42 | })
43 |
--------------------------------------------------------------------------------
/test/server/apps/routes/logs.test.js:
--------------------------------------------------------------------------------
1 | const Flint = require('../../../../index.js')
2 | const request = require('supertest')
3 | const mongoose = require('mongoose')
4 |
5 | describe('logs app', () => {
6 | let server
7 |
8 | beforeAll(async () => {
9 | const flintServer = new Flint({ logsPath: 'test/fixtures/logs', listen: false })
10 | server = await flintServer.startServer()
11 | return server
12 | })
13 |
14 | it('returns a 200 response for /admin/api/logs', (done) => {
15 | request(server).get('/admin/api/logs').expect(200, done)
16 | })
17 |
18 | it('returns the correct logs as an array', async () => {
19 | const res = await request(server).get('/admin/api/logs')
20 | const { flint, http } = res.body
21 | expect(Array.isArray(flint)).toBe(true)
22 | expect(Array.isArray(http)).toBe(true)
23 |
24 | expect(flint).toEqual(['This', 'is', 'a', 'log'])
25 | })
26 |
27 | afterAll(() => mongoose.disconnect())
28 | })
29 |
--------------------------------------------------------------------------------
/test/server/apps/routes/site.test.js:
--------------------------------------------------------------------------------
1 | const Flint = require('../../../../index.js')
2 | const request = require('supertest')
3 | const mongoose = require('mongoose')
4 |
5 | describe('site endpoint', () => {
6 | let server
7 |
8 | beforeAll(async () => {
9 | const flintServer = new Flint({ listen: false })
10 | server = await flintServer.startServer()
11 | })
12 |
13 | it('returns a 200 response for /admin/api/site', (done) => {
14 | request(server).get('/admin/api/site').expect(200, done)
15 | })
16 |
17 | it('returns a 200 response for /admin/api/hasUpdate', (done) => {
18 | request(server).get('/admin/api/hasUpdate').expect(200, done)
19 | })
20 |
21 | it('GET /admin/api/hasUpdate returns an object', async () => {
22 | const res = await request(server).get('/admin/api/hasUpdate')
23 | expect(typeof res.body.hasUpdate).toBe('boolean')
24 | })
25 |
26 | afterAll(() => mongoose.disconnect())
27 | })
28 |
--------------------------------------------------------------------------------
/test/server/utils/FlintPlugin.test.js:
--------------------------------------------------------------------------------
1 | const FlintPlugin = require('../../../server/utils/FlintPlugin')
2 |
3 | describe('FlintPlugin', () => {
4 | class MyPlugin extends FlintPlugin {
5 | static get uid () { return 'my-plugin' }
6 | }
7 |
8 | class MyBadPlugin extends FlintPlugin {}
9 |
10 | it('returns the correct uid', () => expect(MyPlugin.uid).toBe('my-plugin'))
11 |
12 | it('returns false when a uid has not been set', () => expect(MyBadPlugin.uid).toBe(false))
13 |
14 | it('returns the correct title', () => expect(MyPlugin.title).toBe(''))
15 |
16 | it('returns the correct name', () => expect(MyPlugin.name).toBe('MyPlugin'))
17 |
18 | it('returns the correct icon', () => expect(typeof MyPlugin.icon).toBe('string'))
19 |
20 | it('returns the correct model', () => expect(MyPlugin.model).toEqual({}))
21 | })
22 |
--------------------------------------------------------------------------------
/test/server/utils/create-admin-usergroup.test.js:
--------------------------------------------------------------------------------
1 | const Flint = require('../../../index.js')
2 | const mongoose = require('mongoose')
3 |
4 | describe('createAdminUserGroup', () => {
5 | let UserGroup
6 | let createAdminUserGroup
7 |
8 | beforeAll(async () => {
9 | const flintServer = new Flint({ templatePath: 'test/fixtures/templates/empty', listen: false })
10 | await flintServer.startServer()
11 |
12 | UserGroup = mongoose.model('UserGroup')
13 |
14 | // eslint-disable-next-line global-require
15 | createAdminUserGroup = require('../../../server/utils/create-admin-usergroup')
16 |
17 | await UserGroup.remove()
18 | })
19 |
20 | it('creates a new admin user group', async () => {
21 | const AdminUserGroup = await createAdminUserGroup()
22 | expect(typeof AdminUserGroup).toBe('object')
23 | })
24 |
25 | it('returns false if an admin user group already exists', async () => {
26 | const AdminUserGroup = await createAdminUserGroup()
27 | return expect(AdminUserGroup).toBe(false)
28 | })
29 |
30 | afterAll(() => mongoose.disconnect())
31 | })
32 |
--------------------------------------------------------------------------------
/test/server/utils/generate-env-file.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-underscore-dangle */
2 |
3 | const path = require('path')
4 | const fs = require('fs')
5 | const { generateEnvFile, generateSecret } = require('../../../server/utils/generate-env-file')
6 |
7 | describe('generateSecret', () => {
8 | it('should generate a secret', () => {
9 | const secret = generateSecret()
10 | expect(typeof secret).toBe('string')
11 | })
12 |
13 | it('should generate three different secrets', () => {
14 | const s1 = generateSecret()
15 | const s2 = generateSecret()
16 | const s3 = generateSecret()
17 | expect(s1).not.toBe(s2)
18 | expect(s1).not.toBe(s3)
19 | expect(s2).not.toBe(s3)
20 | })
21 | })
22 |
23 | describe('generateEnvFile', () => {
24 | const oldHost = process.env.DB_HOST
25 | let logger
26 |
27 | beforeAll(async () => {
28 | const pathToEnv = path.join(__dirname, '..', '..', 'fixtures', '.env')
29 | fs.unlink(pathToEnv, f => f)
30 | })
31 |
32 | beforeEach(() => {
33 | logger = {
34 | info: jest.fn()
35 | }
36 | })
37 |
38 | it('should not generate a new .env file without DB_HOST', async () => {
39 | process.env.DB_HOST = 'example'
40 | const generatedFile = await generateEnvFile('', logger)
41 | return expect(generatedFile).toBe(false)
42 | })
43 |
44 | it('should generate a new .env file', async () => {
45 | delete process.env.DB_HOST
46 | const generatedFile = await generateEnvFile(path.join(__dirname, '..', '..', 'fixtures'), logger)
47 | return expect(generatedFile).toBe(true)
48 | })
49 |
50 | afterAll(() => {
51 | process.env.DB_HOST = oldHost
52 | })
53 | })
54 |
--------------------------------------------------------------------------------
/test/server/utils/get-entry-data.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable func-names, prefer-arrow-callback */
2 |
3 | const Flint = require('../../../index.js')
4 | const populateDB = require('../../populatedb')
5 | const mongoose = require('mongoose')
6 | const mocks = require('../../mocks')
7 |
8 | describe('getEntryData', () => {
9 | beforeAll(async () => {
10 | const flintServer = new Flint({ listen: false, templatePath: 'test/fixtures' })
11 | await flintServer.startServer()
12 | await populateDB()
13 | })
14 |
15 | it('returns an entry\'s data', async () => {
16 | // eslint-disable-next-line global-require
17 | const getEntryData = require('../../../server/utils/get-entry-data')
18 | const section = mocks.sections.find(s => mocks.entries[3].section === s._id)
19 | const entry = await getEntryData({ slug: mocks.entries[3].slug, section: section.slug })
20 | const author = mocks.users.find(u => mocks.entries[3].author === u._id)
21 |
22 | expect(entry).toEqual({
23 | _id: mocks.entries[3]._id,
24 | title: mocks.entries[3].title,
25 | fields: mocks.entries[3].fields,
26 | status: mocks.entries[3].status,
27 | slug: mocks.entries[3].slug,
28 | dateCreated: mocks.entries[3].dateCreated,
29 | section: mocks.entries[3].section,
30 | template: section.template,
31 | author: {
32 | email: author.email,
33 | username: author.username,
34 | name: author.name
35 | }
36 | })
37 | })
38 |
39 | afterAll(() => mongoose.disconnect())
40 | })
41 |
--------------------------------------------------------------------------------
/test/server/utils/public-registration.test.js:
--------------------------------------------------------------------------------
1 | const Flint = require('../../../index.js')
2 | const supertest = require('supertest')
3 | const mongoose = require('mongoose')
4 |
5 | describe('publicRegistration', () => {
6 | let server
7 | let agent
8 | const signupRoute = '/p/signup'
9 | const loginRoute = '/p/login'
10 |
11 | beforeAll(async () => {
12 | const flintServer = new Flint({
13 | listen: false,
14 | signupRoute,
15 | loginRoute
16 | })
17 |
18 | server = await flintServer.startServer()
19 | agent = supertest.agent(server)
20 |
21 | const Site = mongoose.model('Site')
22 | await Site.findOneAndUpdate({}, { $set: { allowPublicRegistration: true } }).exec()
23 | })
24 |
25 | it('can sign up a new user', async () => {
26 | await agent
27 | .post(signupRoute)
28 | .send({
29 | username: 'exampler',
30 | email: 'example@example.com',
31 | password: 'password'
32 | })
33 |
34 | const User = mongoose.model('User')
35 | const foundNewUser = await User.findOne({ username: 'exampler' }).exec()
36 |
37 | expect(typeof foundNewUser).toBe('object')
38 | })
39 |
40 | it('can log in that new user', async () => {
41 | const res = await agent
42 | .post(loginRoute)
43 | .send({
44 | email: 'example@example.com',
45 | password: 'password'
46 | })
47 |
48 | expect(res.status).toBe(302)
49 | expect(res.header).toHaveProperty('location', '/admin')
50 | })
51 |
52 | afterAll(() => mongoose.disconnect())
53 | })
54 |
--------------------------------------------------------------------------------
/test/server/utils/update-site-config.test.js:
--------------------------------------------------------------------------------
1 | const Flint = require('../../../index.js')
2 | const populateDB = require('../../populatedb')
3 | const mongoose = require('mongoose')
4 |
5 | describe('updateSiteConfig', () => {
6 | beforeAll(async () => {
7 | const flintServer = new Flint({ listen: false })
8 | await flintServer.startServer()
9 | await populateDB()
10 | })
11 |
12 | it('updates the site config in the db', async () => {
13 | // eslint-disable-next-line global-require
14 | const updateSiteConfig = require('../../../server/utils/update-site-config')
15 | const updatedSite = await updateSiteConfig()
16 | expect(typeof updatedSite).toBe('object')
17 | })
18 |
19 | it('adds a new site config to the db', async () => {
20 | const Site = mongoose.model('Site')
21 |
22 | await Site.remove()
23 | // eslint-disable-next-line global-require
24 | const updateSiteConfig = require('../../../server/utils/update-site-config')
25 | const updatedSite = await updateSiteConfig()
26 | expect(typeof updatedSite).toBe('object')
27 | })
28 |
29 | afterAll(() => mongoose.disconnect())
30 | })
31 |
--------------------------------------------------------------------------------
/test/setup.js:
--------------------------------------------------------------------------------
1 | process.env.DEBUG = 'none'
2 | process.env.LOG_LEVEL = 'fatal'
3 |
--------------------------------------------------------------------------------