mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
524d432ec1 | ||
|
|
99ca2cd631 | ||
|
|
c91727e22f | ||
|
|
7933948a89 | ||
|
|
997675cac5 |
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
["es2015", {"modules": false}]
|
||||
],
|
||||
"plugins": [
|
||||
"transform-class-properties",
|
||||
"transform-decorators-legacy",
|
||||
"transform-object-assign",
|
||||
"transform-object-rest-spread",
|
||||
"transform-async-to-generator",
|
||||
"transform-react-jsx",
|
||||
"syntax-dynamic-import"
|
||||
],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": [
|
||||
["transform-es2015-modules-commonjs", "dynamic-import-node"]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,317 +0,0 @@
|
||||
|
||||
# job_environment will setup the environment for any job being executed.
|
||||
job_environment: &job_environment
|
||||
NODE_ENV: test
|
||||
DISABLE_CREATE_MONGO_INDEXES: TRUE
|
||||
|
||||
# job_defaults applies all the defaults for each job.
|
||||
job_defaults: &job_defaults
|
||||
working_directory: ~/coralproject/talk
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
environment:
|
||||
<<: *job_environment
|
||||
|
||||
# create_indexes will create the mongo indexes and wait until they have been
|
||||
# built.
|
||||
create_indexes: &create_indexes
|
||||
run:
|
||||
name: Create the database indexes and wait until they are built
|
||||
command: ./bin/cli db createIndexes
|
||||
|
||||
# integration_environment is the environment that configures the tests.
|
||||
integration_environment: &integration_environment
|
||||
<<: *job_environment
|
||||
CIRCLE_TEST_REPORTS: /tmp/circleci-test-results
|
||||
E2E_MAX_RETRIES: 3
|
||||
|
||||
# integration_job runs the integration tests and saves the test results.
|
||||
integration_job: &integration_job
|
||||
<<: *job_defaults
|
||||
environment:
|
||||
<<: *integration_environment
|
||||
docker:
|
||||
# TODO: replace with node:8-browsers when build issues are resolved.
|
||||
# - image: circleci/node:8-browsers
|
||||
- image: coralproject/ci
|
||||
- image: circleci/mongo:3
|
||||
- image: circleci/redis:4-alpine
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/coralproject/talk
|
||||
- <<: *create_indexes
|
||||
# - run:
|
||||
# name: Setup the database with defaults
|
||||
# command: ./bin/cli setup --defaults
|
||||
- run:
|
||||
name: Run the integration tests
|
||||
command: bash .circleci/e2e.sh
|
||||
- store_test_results:
|
||||
when: always
|
||||
path: /tmp/circleci-test-results
|
||||
- store_artifacts:
|
||||
when: always
|
||||
path: /tmp/circleci-test-results
|
||||
|
||||
|
||||
version: 2
|
||||
jobs:
|
||||
# npm_dependencies will install the dependencies used by all other steps.
|
||||
npm_dependencies:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/coralproject/talk
|
||||
- restore_cache:
|
||||
key: dependency-cache-{{ checksum "yarn.lock" }}
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: |
|
||||
yarn global add node-gyp &&
|
||||
yarn install --frozen-lockfile
|
||||
- save_cache:
|
||||
key: dependency-cache-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- ./node_modules
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths: node_modules
|
||||
|
||||
# lint will perform file linting.
|
||||
lint:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/coralproject/talk
|
||||
- run:
|
||||
name: Perform linting
|
||||
command: yarn lint
|
||||
|
||||
# build_assets will build the static assets.
|
||||
build_assets:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/coralproject/talk
|
||||
- restore_cache:
|
||||
keys:
|
||||
- build-cache-{{ .Branch }}-{{ .Revision }}
|
||||
- build-cache-{{ .Branch }}-
|
||||
- build-cache-
|
||||
- run:
|
||||
name: Build static assets
|
||||
command: yarn build
|
||||
- save_cache:
|
||||
key: build-cache-{{ .Branch }}-{{ .Revision }}
|
||||
paths:
|
||||
- ./node_modules/.cache/babel-loader
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths: dist
|
||||
|
||||
# test_unit will run the unit tests.
|
||||
test_unit:
|
||||
<<: *job_defaults
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
- image: circleci/mongo:3
|
||||
- image: circleci/redis:4-alpine
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/coralproject/talk
|
||||
- run:
|
||||
name: Setup the test results directory
|
||||
command: mkdir -p /tmp/circleci-test-results
|
||||
- run:
|
||||
name: Run the client unit tests
|
||||
command: yarn test:client --ci
|
||||
environment:
|
||||
JEST_JUNIT_OUTPUT: /tmp/circleci-test-results/jest/test-results.xml
|
||||
JEST_REPORTER: jest-junit
|
||||
- <<: *create_indexes
|
||||
- run:
|
||||
name: Run the server unit tests
|
||||
command: yarn test:server
|
||||
environment:
|
||||
MOCHA_FILE: /tmp/circleci-test-results/mocha/test-results.xml
|
||||
MOCHA_REPORTER: mocha-junit-reporter
|
||||
- store_test_results:
|
||||
when: always
|
||||
path: /tmp/circleci-test-results
|
||||
|
||||
# test_integration_chrome_local will run the integration tests locally with
|
||||
# chrome headless.
|
||||
test_integration_chrome_local:
|
||||
<<: *integration_job
|
||||
environment:
|
||||
<<: *integration_environment
|
||||
E2E_BROWSERS: chrome
|
||||
|
||||
# test_integration_firefox_local will run the integration tests locally with
|
||||
# firefox headless.
|
||||
test_integration_firefox_local:
|
||||
<<: *integration_job
|
||||
environment:
|
||||
<<: *integration_environment
|
||||
E2E_BROWSERS: firefox
|
||||
|
||||
# test_integration_chrome will run the integration tests with chrome in
|
||||
# browserstack.
|
||||
test_integration_chrome:
|
||||
<<: *integration_job
|
||||
environment:
|
||||
<<: *integration_environment
|
||||
BROWSERSTACK: true
|
||||
E2E_BROWSERS: chrome
|
||||
|
||||
# test_integration_firefox will run the integration tests with firefox in
|
||||
# browserstack.
|
||||
test_integration_firefox:
|
||||
<<: *integration_job
|
||||
environment:
|
||||
<<: *integration_environment
|
||||
BROWSERSTACK: true
|
||||
E2E_BROWSERS: firefox
|
||||
|
||||
# test_integration_edge will run the integration tests with edge in
|
||||
# browserstack.
|
||||
test_integration_edge:
|
||||
<<: *integration_job
|
||||
environment:
|
||||
<<: *integration_environment
|
||||
BROWSERSTACK: true
|
||||
E2E_BROWSERS: edge
|
||||
|
||||
# test_integration_ie will run the integration tests with ie in
|
||||
# browserstack.
|
||||
test_integration_ie:
|
||||
<<: *integration_job
|
||||
environment:
|
||||
<<: *integration_environment
|
||||
BROWSERSTACK: true
|
||||
E2E_BROWSERS: ie
|
||||
# TODO: remove when more reliable
|
||||
E2E_MAX_RETRIES: 1
|
||||
|
||||
# test_integration_safari will run the integration tests with safari in
|
||||
# browserstack.
|
||||
test_integration_safari:
|
||||
<<: *integration_job
|
||||
environment:
|
||||
<<: *integration_environment
|
||||
BROWSERSTACK: true
|
||||
E2E_BROWSERS: safari
|
||||
# TODO: remove when more reliable
|
||||
E2E_MAX_RETRIES: 1
|
||||
|
||||
# deploy will deploy the application as a docker image.
|
||||
deploy:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
- run:
|
||||
name: Deploy the code
|
||||
command: bash ./scripts/docker.sh deploy
|
||||
|
||||
# filter_deploy will add the filters for a deploy job in a workflow to make it
|
||||
# only execute on a deploy related job.
|
||||
filter_deploy: &filter_deploy
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- next
|
||||
tags:
|
||||
only: /v[0-9]+(\.[0-9]+)*/
|
||||
|
||||
# filter_develop will add the filters for a development related commit.
|
||||
filter_develop: &filter_develop
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
- master
|
||||
- next
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
||||
# All PR's will hit this workflow.
|
||||
build-and-test:
|
||||
jobs:
|
||||
- npm_dependencies:
|
||||
<<: *filter_develop
|
||||
- lint:
|
||||
<<: *filter_develop
|
||||
requires:
|
||||
- npm_dependencies
|
||||
- test_unit:
|
||||
<<: *filter_develop
|
||||
requires:
|
||||
- npm_dependencies
|
||||
- build_assets:
|
||||
<<: *filter_develop
|
||||
requires:
|
||||
- npm_dependencies
|
||||
- test_integration_chrome_local:
|
||||
<<: *filter_develop
|
||||
requires:
|
||||
- build_assets
|
||||
# TODO: uncomment when more reliable
|
||||
# - test_integration_firefox_local:
|
||||
# <<: *filter_develop
|
||||
# requires:
|
||||
# - build_assets
|
||||
deploy-tagged:
|
||||
jobs:
|
||||
- npm_dependencies:
|
||||
<<: *filter_deploy
|
||||
- lint:
|
||||
<<: *filter_deploy
|
||||
requires:
|
||||
- npm_dependencies
|
||||
- test_unit:
|
||||
<<: *filter_deploy
|
||||
requires:
|
||||
- npm_dependencies
|
||||
- build_assets:
|
||||
<<: *filter_deploy
|
||||
requires:
|
||||
- npm_dependencies
|
||||
- test_integration_chrome:
|
||||
<<: *filter_deploy
|
||||
requires:
|
||||
- build_assets
|
||||
- test_integration_firefox:
|
||||
<<: *filter_deploy
|
||||
requires:
|
||||
- build_assets
|
||||
- test_integration_edge:
|
||||
<<: *filter_deploy
|
||||
requires:
|
||||
- build_assets
|
||||
# TODO: uncomment when more reliable
|
||||
# - test_integration_ie:
|
||||
# <<: *filter_deploy
|
||||
# requires:
|
||||
# - build_assets
|
||||
# - test_integration_safari:
|
||||
# <<: *filter_deploy
|
||||
# requires:
|
||||
# - build_assets
|
||||
- deploy:
|
||||
<<: *filter_deploy
|
||||
requires:
|
||||
- lint
|
||||
- test_unit
|
||||
- test_integration_chrome
|
||||
- test_integration_firefox
|
||||
- test_integration_edge
|
||||
# TODO: uncomment when more reliable
|
||||
# - test_integration_ie
|
||||
# - test_integration_safari
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
REPORTS_FOLDER=${CIRCLE_TEST_REPORTS:-./test/e2e/reports}
|
||||
CIRCLE_BRANCH=${CIRCLE_BRANCH:-master}
|
||||
E2E_DISABLE=${E2E_DISABLE:-false}
|
||||
|
||||
# Amount of retries before failure.
|
||||
E2E_MAX_RETRIES=${E2E_MAX_RETRIES:-1}
|
||||
|
||||
# Timeout for WaitForConditions.
|
||||
E2E_WAIT_FOR_TIMEOUT=${E2E_WAIT_FOR_TIMEOUT:-10000}
|
||||
|
||||
# Safari >= 8 has issues connecting to browserstack-local. Safari < 8 is too old.
|
||||
# IE 64bit has issues with receiving keyboard input. Let's wait for them to fix it.
|
||||
E2E_BROWSERS=${E2E_BROWSERS:-chrome,firefox,edge} #ie safari
|
||||
|
||||
if [[ "${E2E_DISABLE}" == "true" ]]; then
|
||||
echo E2E is disabled.
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ "$BROWSERSTACK" == "true" && -n "$BROWSERSTACK_KEY" ]]; then
|
||||
echo Testing on browserstack
|
||||
node scripts/e2e.js --reports-folder "$REPORTS_FOLDER" --retries "$E2E_MAX_RETRIES" --timeout "$E2E_WAIT_FOR_TIMEOUT" --browsers "$E2E_BROWSERS" --browserstack
|
||||
else
|
||||
# When browserstack is not available test locally.
|
||||
echo Testing locally
|
||||
node scripts/e2e.js --reports-folder "$REPORTS_FOLDER" --retries "$E2E_MAX_RETRIES" --timeout "$E2E_WAIT_FOR_TIMEOUT" --browsers "$E2E_BROWSERS" --headless
|
||||
fi
|
||||
+2
-4
@@ -1,12 +1,11 @@
|
||||
# excluded because we'll likely need to rebuild this.
|
||||
node_modules
|
||||
|
||||
# most scripts are used during development and testing, not
|
||||
# scripts are used during development and testing, not
|
||||
# production.
|
||||
scripts
|
||||
!scripts/generateIntrospectionResult.js
|
||||
|
||||
# documentation should not be visible in production.
|
||||
# documentation should not be visable in production.
|
||||
docs
|
||||
|
||||
# static assets are rebuild in the docker container.
|
||||
@@ -14,7 +13,6 @@ dist
|
||||
|
||||
# tests are not run in the docker container.
|
||||
test
|
||||
__tests__
|
||||
|
||||
# we won't use the .git folder in production.
|
||||
.git
|
||||
|
||||
+15
-3
@@ -1,5 +1,17 @@
|
||||
**/*.html
|
||||
dist
|
||||
docs
|
||||
client/lib
|
||||
**/*.html
|
||||
plugins/*
|
||||
!plugins/coral-plugin-facebook-auth
|
||||
!plugins/coral-plugin-auth
|
||||
!plugins/coral-plugin-respect
|
||||
!plugins/coral-plugin-offtopic
|
||||
!plugins/coral-plugin-like
|
||||
!plugins/coral-plugin-mod
|
||||
!plugins/coral-plugin-love
|
||||
!plugins/coral-plugin-viewing-options
|
||||
!plugins/coral-plugin-comment-content
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-featured
|
||||
node_modules
|
||||
public
|
||||
**/*.min.js
|
||||
|
||||
+63
-8
@@ -1,11 +1,66 @@
|
||||
{
|
||||
"env": {
|
||||
"jest": true
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "15.0"
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2017
|
||||
},
|
||||
"rules": {
|
||||
"indent": ["error",
|
||||
2
|
||||
],
|
||||
"no-console": [
|
||||
0
|
||||
],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["error", "single"],
|
||||
"semi": ["error", "always"],
|
||||
"no-template-curly-in-string": [1],
|
||||
"no-unsafe-negation": [1],
|
||||
"array-callback-return": [1],
|
||||
"arrow-parens": ["warn", "always"],
|
||||
"template-curly-spacing": "warn",
|
||||
"eqeqeq": [2, "smart"],
|
||||
"no-eval": [2],
|
||||
"no-global-assign": [2],
|
||||
"no-implied-eval": [2],
|
||||
"lines-around-comment": ["warn", {"beforeLineComment": true}],
|
||||
"spaced-comment": ["warn", "always", { "line": { "exceptions": ["-", "="] } }],
|
||||
"no-script-url": [2],
|
||||
"no-throw-literal": [2],
|
||||
"yoda": [1],
|
||||
"no-path-concat": [2],
|
||||
"eol-last": [1],
|
||||
"no-nested-ternary": [1],
|
||||
"no-tabs": [2],
|
||||
"no-unneeded-ternary": [1],
|
||||
"object-curly-spacing": [1],
|
||||
"space-infix-ops": ["error"],
|
||||
"space-in-parens": ["error", "never"],
|
||||
"space-unary-ops": ["error", {
|
||||
"words": true,
|
||||
"nonwords": false
|
||||
}],
|
||||
"no-const-assign": [2],
|
||||
"no-duplicate-imports": [2],
|
||||
"prefer-template": [1],
|
||||
"comma-spacing": ["error", {
|
||||
"after": true
|
||||
}],
|
||||
"no-var": [2],
|
||||
"no-lonely-if": [2],
|
||||
"curly": [2],
|
||||
"no-unused-vars": ["error", {
|
||||
"argsIgnorePattern": "^_|next",
|
||||
"varsIgnorePattern": "^_"
|
||||
}],
|
||||
"no-multiple-empty-lines": ["error", {
|
||||
"max": 1
|
||||
}],
|
||||
"newline-per-chained-call": ["error", {
|
||||
"ignoreChainWithDepth": 2
|
||||
}]
|
||||
}
|
||||
},
|
||||
"extends": "@coralproject/eslint-config-talk"
|
||||
}
|
||||
|
||||
@@ -1,34 +1,5 @@
|
||||
<!--
|
||||
### Expected behavior
|
||||
|
||||
Thank you for filing an issue on Coral Talk!
|
||||
### Actual behavior
|
||||
|
||||
Please fill out the questions below so we can take action on your issue as soon as we can.
|
||||
|
||||
If you're filing a feature request, you do not need to follow the outline below. Instead please include "Feature Idea" in your issue title and explain a specific example in which that feature would be useful.
|
||||
|
||||
-->
|
||||
|
||||
#### Do you want to request a **feature** or report a **bug**?
|
||||
|
||||
|
||||
#### Intended outcome:
|
||||
<!--
|
||||
What you were trying to accomplish when the bug occurred?
|
||||
-->
|
||||
|
||||
#### Actual outcome:
|
||||
<!--
|
||||
What happened instead?
|
||||
|
||||
Please provide as much detail as possible, including a screenshot or copy-paste of any related error messages, logs, or other output that might be related. Places to look for information include your browser console, server console, and network logs. The more information you can give the better.
|
||||
-->
|
||||
|
||||
#### How to reproduce the issue:
|
||||
<!--
|
||||
Instructions for how the issue can be reproduced by someone from our team or by a contributor. Be as specific as possible, and only mention what is necessary to reproduce the bug. If possible, try to isolate the exact circumstances in which the bug occurs and avoid speculation over what the cause might be.
|
||||
-->
|
||||
|
||||
#### Version and environment
|
||||
<!--
|
||||
List what version of Talk you're using, as well as any other relevant environment information, such as operating system or browser
|
||||
-->
|
||||
### Steps to reproduce behavior
|
||||
|
||||
+11
-54
@@ -5,70 +5,27 @@ dist
|
||||
npm-debug.log*
|
||||
dump.rdb
|
||||
|
||||
client/coral-framework/graphql/introspection.json
|
||||
docs/source/_data/introspection.json
|
||||
|
||||
.env
|
||||
*.cfg
|
||||
|
||||
.idea/
|
||||
*.swp
|
||||
*.DS_STORE
|
||||
.prettierrc.json
|
||||
.vscode
|
||||
|
||||
test/e2e/reports
|
||||
coverage/
|
||||
test/e2e/reports/
|
||||
test/e2e/bslocal.log
|
||||
test/e2e/selenium-debug.log
|
||||
browserstack.err
|
||||
|
||||
plugins.json
|
||||
|
||||
plugins/*
|
||||
!plugins/talk-plugin-akismet
|
||||
!plugins/talk-plugin-auth
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-comment-content
|
||||
!plugins/talk-plugin-deep-reply-count
|
||||
!plugins/talk-plugin-downvote
|
||||
!plugins/talk-plugin-facebook-auth
|
||||
!plugins/talk-plugin-featured-comments
|
||||
!plugins/talk-plugin-flag-details
|
||||
!plugins/talk-plugin-google-auth
|
||||
!plugins/talk-plugin-ignore-user
|
||||
!plugins/talk-plugin-like
|
||||
!plugins/talk-plugin-local-auth
|
||||
!plugins/talk-plugin-love
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-mod
|
||||
!plugins/talk-plugin-moderation-actions
|
||||
!plugins/talk-plugin-notifications
|
||||
!plugins/talk-plugin-notifications-category-featured
|
||||
!plugins/talk-plugin-notifications-category-moderation-actions
|
||||
!plugins/talk-plugin-notifications-category-reply
|
||||
!plugins/talk-plugin-notifications-category-staff
|
||||
!plugins/talk-plugin-notifications-digest-daily
|
||||
!plugins/talk-plugin-notifications-digest-hourly
|
||||
!plugins/talk-plugin-offtopic
|
||||
!plugins/coral-plugin-facebook-auth
|
||||
!plugins/coral-plugin-auth
|
||||
!plugins/coral-plugin-respect
|
||||
!plugins/coral-plugin-offtopic
|
||||
!plugins/coral-plugin-like
|
||||
!plugins/coral-plugin-mod
|
||||
!plugins/coral-plugin-love
|
||||
!plugins/coral-plugin-viewing-options
|
||||
!plugins/coral-plugin-comment-content
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-profile-data
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-respect
|
||||
!plugins/talk-plugin-rich-text
|
||||
!plugins/talk-plugin-slack-notifications
|
||||
!plugins/talk-plugin-sort-most-downvoted
|
||||
!plugins/talk-plugin-sort-most-liked
|
||||
!plugins/talk-plugin-sort-most-loved
|
||||
!plugins/talk-plugin-sort-most-replied
|
||||
!plugins/talk-plugin-sort-most-respected
|
||||
!plugins/talk-plugin-sort-most-upvoted
|
||||
!plugins/talk-plugin-sort-newest
|
||||
!plugins/talk-plugin-sort-oldest
|
||||
!plugins/talk-plugin-subscriber
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-upvote
|
||||
!plugins/talk-plugin-viewing-options
|
||||
!plugins/talk-plugin-featured
|
||||
|
||||
**/node_modules/*
|
||||
yarn-error.log
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"linters": {
|
||||
"*.js": [
|
||||
"git-exec-and-restage eslint --fix --"
|
||||
],
|
||||
"bin/cli*": [
|
||||
"git-exec-and-restage eslint --fix --"
|
||||
],
|
||||
"*.yml": [
|
||||
"yamllint"
|
||||
]
|
||||
}
|
||||
}
|
||||
+3
-8
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"exec": "npm-run-all --parallel generate-introspection start:development",
|
||||
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client", "docs/*"],
|
||||
"ext": "js,json,graphql,yml",
|
||||
"watch": [
|
||||
".",
|
||||
"bin/cli",
|
||||
"bin/cli-serve"
|
||||
]
|
||||
"verbose": true,
|
||||
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
|
||||
"ext": "js,json,graphql"
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"exceptions": [
|
||||
"https://nodesecurity.io/advisories/531",
|
||||
"https://nodesecurity.io/advisories/532",
|
||||
"https://nodesecurity.io/advisories/566",
|
||||
"https://nodesecurity.io/advisories/577",
|
||||
"https://nodesecurity.io/advisories/594",
|
||||
"https://nodesecurity.io/advisories/603",
|
||||
"https://nodesecurity.io/advisories/611",
|
||||
"https://nodesecurity.io/advisories/612",
|
||||
"https://nodesecurity.io/advisories/654"
|
||||
]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
overrides:
|
||||
- files: "bin/cli*"
|
||||
options:
|
||||
parser: babylon
|
||||
@@ -1,7 +0,0 @@
|
||||
# Vox Media Code of Conduct
|
||||
|
||||
## Introduction
|
||||
|
||||
This code of conduct governs the environment of the Vox Product team. We created it not because we anticipate bad behavior, but because we believe that articulating our values and obligations to one another reinforces the already exceptional level of respect among the team and because having a code provides us with clear avenues to correct our culture should it ever stray from that course. We make this code public in the hopes of contributing to the ongoing conversation about inclusion in the tech, design, and media communities and encourage other teams to fork it and make it their own. To our team, we commit to enforce and evolve this code as our team grows.
|
||||
|
||||
Read the rest here: http://code-of-conduct.voxmedia.com/
|
||||
+29
-16
@@ -4,7 +4,7 @@ Welcome! We are very excited that you are interested in contributing to Talk.
|
||||
|
||||
This document is a companion to help you approach contributing. If it does not do so, please [let us know how we can improve it](https://github.com/coralproject/talk/issues)!
|
||||
|
||||
By contributing to this project you agree to the [Code of Conduct](CODE_OF_CONDUCT.md).
|
||||
By contributing to this project you agree to the [Code of Conduct](https://coralproject.net/code-of-conduct.html).
|
||||
|
||||
## What should I Contribute?
|
||||
|
||||
@@ -18,7 +18,7 @@ There are at least three ways to contribute to Talk:
|
||||
|
||||
Conversation surrounding contributions begins in [issues](https://github.com/coralproject/talk/issues).
|
||||
|
||||
### When should I create an issue?
|
||||
### When should I Create an Issue?
|
||||
|
||||
File an issue as soon as you have an idea of something you'd like to contribute. We would love to hear what you're thinking and help refine the idea to make it into the Talk ecosystem.
|
||||
|
||||
@@ -30,9 +30,9 @@ Please file issues if:
|
||||
|
||||
### What should I include?
|
||||
|
||||
Coral has adopted an iterative, agile development philosophy. All contributions that make it into the Talk repository should start with a user story in this form:
|
||||
Coral has adopted an iterative, agile development philosophy. All contributions that make it into the Talk repo should start with a story or this form:
|
||||
|
||||
> As a [type of Coral user] I'd like to [do something] so that I can [get some result/value].
|
||||
`As a [type of person] I'd like to be able to [do something] so that I can [get some result].`
|
||||
|
||||
This exercise does two things:
|
||||
|
||||
@@ -53,21 +53,34 @@ We are looking for _documentarians_ to:
|
||||
* create new / missing sections, and
|
||||
* take the lead in making sections, or the over all structure better.
|
||||
|
||||
Our documentation is stored in markdown files in the [docs](docs) directory. We
|
||||
use [Hexo](https://hexo.io/) to provide our docs. To preview:
|
||||
|
||||
```shell
|
||||
cd docs
|
||||
yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
Then visit http://127.0.0.1:4000/talk/.
|
||||
Information about how to update docs can be found in our [FAQ](faq.html#how-do-i-contribute-to-these-docs).
|
||||
|
||||
If you'd like to discuss a contribution, please [file an issue](https://github.com/coralproject/talk/issues) describing the changes you would like to see.
|
||||
|
||||
## Contributing Translations
|
||||
|
||||
Talk's translations are stored in `.yml` files [here](https://github.com/coralproject/talk/tree/master/locales).
|
||||
Talk's tranlations are stored in `.yml` files [here](https://github.com/coralproject/talk/tree/master/locales).
|
||||
|
||||
Translations can be submitted via pull request. If you do not use github, you can use 'en.yml' as a template and [email](https://coralproject.net/contact) the translations to us. We can import it into the repository.
|
||||
Translations can be submitted via pull request. If you do not use github, you can use 'en.yml' as a template and [email](https://coralproject.net/contact.html) the translations to us. We can import it into the repo.
|
||||
|
||||
## I want to contribute but I'm not sure what to do!
|
||||
|
||||
If you want to contribute but don't have a clear idea of exactly what that may be, here are some resources that may help:
|
||||
|
||||
### Product Roadmap
|
||||
|
||||
Please visit our product roadmap here: https://www.pivotaltracker.com/n/projects/1863625. If you'd like to take on any of our scheduled tasks we'd be forever grateful!
|
||||
|
||||
### Discussion Forum
|
||||
|
||||
If you'd like to discuss what we're up to, please visit or [community](https://community.coralproject.net/) or [contact us](https://coralproject.net/contact.html).
|
||||
|
||||
### Integrations
|
||||
|
||||
Have a favorite analytics engine? Data science service? CMS? Auth platform? Deployment platform or pipeline? Pet project? Consider building a plugin to integrate them!
|
||||
|
||||
### Favorite Features?
|
||||
|
||||
Do you have a favorite feature of an existing platform that's not yet been done in Talk? Sounds like Talk needs that feature.
|
||||
|
||||
## Thanks!
|
||||
|
||||
+6
-8
@@ -1,4 +1,4 @@
|
||||
FROM node:8-alpine
|
||||
FROM node:7.10.1
|
||||
|
||||
# Create app directory
|
||||
RUN mkdir -p /usr/src/app
|
||||
@@ -12,17 +12,15 @@ EXPOSE 5000
|
||||
# Bundle app source
|
||||
COPY . /usr/src/app
|
||||
|
||||
# Ensure the runtime of the container is in production mode.
|
||||
ENV NODE_ENV production
|
||||
|
||||
# Store the current git revision.
|
||||
ARG REVISION_HASH
|
||||
ENV REVISION_HASH=${REVISION_HASH}
|
||||
|
||||
# Install app dependencies and build static assets.
|
||||
RUN yarn global add node-gyp && \
|
||||
yarn install --frozen-lockfile && \
|
||||
cli plugins reconcile && \
|
||||
yarn build && \
|
||||
yarn install --production && \
|
||||
yarn cache clean
|
||||
|
||||
# Ensure the runtime of the container is in production mode.
|
||||
ENV NODE_ENV production
|
||||
|
||||
CMD ["yarn", "start"]
|
||||
|
||||
+8
-21
@@ -1,27 +1,14 @@
|
||||
FROM coralproject/talk:latest
|
||||
|
||||
# Setup the build arguments
|
||||
ONBUILD ARG TALK_ADDTL_COMMENTS_ON_LOAD_MORE=10
|
||||
ONBUILD ARG TALK_ASSET_COMMENTS_LOAD_DEPTH=10
|
||||
ONBUILD ARG TALK_REPLY_COMMENTS_LOAD_DEPTH=3
|
||||
ONBUILD ARG TALK_ADDTL_REPLIES_ON_LOAD_MORE=999999
|
||||
ONBUILD ARG TALK_THREADING_LEVEL=3
|
||||
ONBUILD ARG TALK_DEFAULT_STREAM_TAB=all
|
||||
ONBUILD ARG TALK_DISABLE_EMBED_POLYFILL=FALSE
|
||||
ONBUILD ARG TALK_DEFAULT_LANG=en
|
||||
ONBUILD ARG TALK_WHITELISTED_LANGUAGES
|
||||
ONBUILD ARG TALK_PLUGINS_JSON
|
||||
ONBUILD ARG TALK_WEBPACK_SOURCE_MAP
|
||||
ONBUILD ARG TALK_DEFAULT_LAZY_RENDER
|
||||
|
||||
# Bundle app source
|
||||
ONBUILD COPY . /usr/src/app
|
||||
|
||||
# At this stage, we need to install the development dependencies again because
|
||||
# we need to have webpack available. We then build the new dependencies and
|
||||
# clear out the development dependencies again. After this we of course need to
|
||||
# At this stage, we need to install the development dependancies again because
|
||||
# we need to have webpack available. We then build the new dependancies and
|
||||
# clear out the development dependancies again. After this we of course need to
|
||||
# clear out the yarn cache, this saves quite a lot of size.
|
||||
ONBUILD RUN cli plugins reconcile && \
|
||||
yarn && \
|
||||
yarn build && \
|
||||
yarn cache clean
|
||||
ONBUILD RUN NODE_ENV=development yarn install --frozen-lockfile && \
|
||||
NODE_ENV=production cli plugins reconcile && \
|
||||
NODE_ENV=production yarn build && \
|
||||
NODE_ENV=production yarn install --production --force && \
|
||||
yarn cache clean
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
## Contents
|
||||
|
||||
- [Installation](#installation) - install the application on a machine
|
||||
- [Via Docker](#installation-from-docker)
|
||||
- [Via Source](#installation-from-source)
|
||||
- [Setup](#setup) - setup the application for first use
|
||||
- [Usage](#usage) - connect the application to a website
|
||||
|
||||
# Installation
|
||||
|
||||
## Requirements
|
||||
|
||||
- Any flavour of Linux, OSX or Windows
|
||||
- 1GB memory (minimum)
|
||||
- 5GB storage (minimum)
|
||||
- [MongoDB](https://www.mongodb.com/) v3.4 or later
|
||||
- [Redis](https://redis.io/) v3.2 or later
|
||||
- SSL Certificate
|
||||
- This application assumes that you will be serving this application in a
|
||||
production environment, and therefore requires that you serve it behind a
|
||||
webserver with a valid SSL certificate. This is chosen in order to secure
|
||||
user's sessions.
|
||||
|
||||
## Installation From Docker
|
||||
|
||||
We currently support packaging the Talk application via Docker, which automates
|
||||
the dependency install and asset build process. This is the recommended way to
|
||||
deploy the application when used in production.
|
||||
|
||||
Available as [coralproject/talk](https://hub.docker.com/r/coralproject/talk/) on Docker Hub.
|
||||
|
||||
Images are tagged using the following notation:
|
||||
|
||||
- `x` (where `x` is the major version number): any minor or patch updates will be included in this. If you're ok getting
|
||||
new features occasionally and all the bug fixes, this is the tag for you.
|
||||
- `x.y` (where `y` is the minor version number): any patch updates will be
|
||||
included with this tag. If you like getting fixes and having features change
|
||||
only when you want, this is the tag for you. **(recommended)**
|
||||
- `x.y.z` (where `z` is the patch version): this tag never gets updated, and
|
||||
essentially freezes your version, this should only be used when you are either
|
||||
extending Talk or are sure of a specific version you want to freeze.
|
||||
|
||||
We provide tags with `*-onbuild` that can be used for easy plugin integration and
|
||||
acts as a customization endpoint. Instructions are provided in the `PLUGINS.md`
|
||||
document as to how to use it.
|
||||
|
||||
### Requirements
|
||||
|
||||
There are some runtime requirements for running Talk for Docker:
|
||||
|
||||
- [Docker](https://www.docker.com/) v1.13.0 or later
|
||||
- [Docker Compose](https://docs.docker.com/compose/) v1.10.0 or later
|
||||
|
||||
_Please be sure to check the versions of these requirements. Incorrect versions
|
||||
of these may lead to unexpected errors!_
|
||||
|
||||
### Installing
|
||||
|
||||
An example docker-compose.yml:
|
||||
|
||||
```yaml
|
||||
version: '2'
|
||||
services:
|
||||
talk:
|
||||
image: coralproject/talk:1.5
|
||||
restart: always
|
||||
ports:
|
||||
- "5000:5000"
|
||||
depends_on:
|
||||
- mongo
|
||||
- redis
|
||||
environment:
|
||||
- TALK_MONGO_URL=mongodb://mongo/talk
|
||||
- TALK_REDIS_URL=redis://redis
|
||||
mongo:
|
||||
image: mongo:3.2
|
||||
restart: always
|
||||
volumes:
|
||||
- mongo:/data/db
|
||||
redis:
|
||||
image: redis:3.2
|
||||
restart: always
|
||||
volumes:
|
||||
- redis:/data
|
||||
volumes:
|
||||
mongo:
|
||||
external: false
|
||||
redis:
|
||||
external: false
|
||||
```
|
||||
|
||||
At this stage, you should refer to the `README.md` for configuration variables
|
||||
that are specific to your installation. Some pre-defined fields have been filled
|
||||
in the above example which are consistent with Docker Compose naming conventions
|
||||
for [Docker Links](https://docs.docker.com/compose/networking/#links).
|
||||
|
||||
### Scaling
|
||||
|
||||
If you are interested in splitting apart services, you can simply adjust the
|
||||
command being executed in the container to optimize for your use case. An
|
||||
example would be if you wanted to run the API server and the job processor
|
||||
on different machines. You can achieve this easily with docker compose:
|
||||
|
||||
```yaml
|
||||
version: '2'
|
||||
services:
|
||||
talk-api:
|
||||
image: coralproject/talk:1.5
|
||||
command: cli serve
|
||||
restart: always
|
||||
ports:
|
||||
- "5000:5000"
|
||||
depends_on:
|
||||
- mongo
|
||||
- redis
|
||||
environment:
|
||||
- TALK_MONGO_URL=mongodb://mongo/talk
|
||||
- TALK_REDIS_URL=redis://redis
|
||||
talk-jobs:
|
||||
image: coralproject/talk:1.5
|
||||
command: cli jobs process
|
||||
restart: always
|
||||
ports:
|
||||
- "5001:5000"
|
||||
depends_on:
|
||||
- mongo
|
||||
- redis
|
||||
environment:
|
||||
- TALK_MONGO_URL=mongodb://mongo/talk
|
||||
- TALK_REDIS_URL=redis://redis
|
||||
mongo:
|
||||
image: mongo:3.2
|
||||
restart: always
|
||||
volumes:
|
||||
- mongo:/data/db
|
||||
redis:
|
||||
image: redis:3.2
|
||||
restart: always
|
||||
volumes:
|
||||
- redis:/data
|
||||
volumes:
|
||||
mongo:
|
||||
external: false
|
||||
redis:
|
||||
external: false
|
||||
```
|
||||
|
||||
Note that the only difference is in the `command` key. From this, you are able
|
||||
to discretely control which modules are running in order to have the maximum
|
||||
flexibility when managing your application.
|
||||
|
||||
### Running
|
||||
|
||||
If you're using docker compose:
|
||||
|
||||
```bash
|
||||
# Start the services using compose
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
If you're using plain docker:
|
||||
|
||||
```bash
|
||||
docker run -d -P coralproject/talk:latest
|
||||
```
|
||||
|
||||
## Installation From Source
|
||||
|
||||
This provides information on how to setup the application from source. Note that
|
||||
this is not recommended for production deploys, but will work for development
|
||||
and testing purposes.
|
||||
|
||||
### Requirements
|
||||
|
||||
There are some runtime requirements for running Talk from source:
|
||||
|
||||
- [Node](https://nodejs.org/) ~7.8
|
||||
- [Yarn](https://yarnpkg.com/) ^0.22.0
|
||||
|
||||
_Please be sure to check the versions of these requirements. Incorrect versions
|
||||
of these may lead to unexpected errors!_
|
||||
|
||||
### Installing
|
||||
|
||||
#### Download
|
||||
|
||||
It is highly recommended that you download a released version as the code
|
||||
available in `master` may not be stable. You can download the latest release
|
||||
from the [releases page](https://github.com/coralproject/talk/releases).
|
||||
|
||||
You can also clone the git repository via:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/coralproject/talk.git
|
||||
```
|
||||
|
||||
#### Building
|
||||
|
||||
We now have to install the dependencies and build the static assets.
|
||||
|
||||
```bash
|
||||
# Install package dependancies
|
||||
yarn
|
||||
|
||||
# Build static files
|
||||
yarn build
|
||||
```
|
||||
|
||||
After you create/modify the `plugins.json` (refer to `PLUGINS.md` for plugin
|
||||
docs) file, you can re-run the following to install their dependencies:
|
||||
|
||||
```bash
|
||||
# Reconcile plugins
|
||||
./bin/cli plugins reconcile
|
||||
|
||||
# Build static files
|
||||
yarn build
|
||||
```
|
||||
|
||||
### Running
|
||||
|
||||
Refer to the `README.md` file for required configuration variables to add to the
|
||||
environment.
|
||||
|
||||
You can start the server after configuring the server using the command:
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
```
|
||||
|
||||
This will setup the server to serve everything on a single node.js process and
|
||||
is designed to be used in production.
|
||||
|
||||
You can see other scripts we've made available by consulting the `package.json`
|
||||
file under the `scripts` key including:
|
||||
|
||||
- `yarn test` run unit tests
|
||||
- `yarn e2e` run end to end tests
|
||||
- `yarn build-watch` watch for changes to client files and build static assets
|
||||
- `yarn dev-start` watch for changes to server files and reload the server while
|
||||
also sourcing a `.env` file in your local directory for configuration
|
||||
|
||||
# Setup
|
||||
|
||||
Once you've installed Talk (either via Docker or source), you still need to
|
||||
setup the application. If you are unfamiliar with any terminology used in the
|
||||
setup process, refer to the `TERMINOLOGY.md` document.
|
||||
|
||||
## Via Web
|
||||
|
||||
If you want to perform your setup via the web, you can navigate to your
|
||||
installation of Talk at the path `/admin/install`. There you will be asked a
|
||||
series of questions for your installation.
|
||||
|
||||
## Via CLI
|
||||
|
||||
If you want to perform your setup through the terminal, you can simply run:
|
||||
|
||||
```bash
|
||||
cli setup
|
||||
```
|
||||
|
||||
And follow the instructions to perform initial setup and create your first user
|
||||
account.
|
||||
|
||||
|
||||
# Usage
|
||||
|
||||
After setup is complete, you can then refer to the `/admin/configure` path to
|
||||
get the embed code that you can copy/paste onto your blog or website in order to
|
||||
start using Talk.
|
||||
|
||||
_In order for the embed to work correctly, you will need to whitelist the domain
|
||||
that is allowed to embed your site on the `/admin/configure` page, failure to do
|
||||
so will result in the comment stream not loading._
|
||||
@@ -1,15 +1,11 @@
|
||||
Copyright 2018 Mozilla Foundation
|
||||
Copyright 2017 Mozilla Foundation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||
either express or implied.
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
||||
See the License for the specific language governing permissions
|
||||
and limitations under the License.
|
||||
See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Talk Plugins
|
||||
|
||||
Our documentation for Talk has moved! Utilize [this hyperlink](https://coralproject.github.io/talk/plugins.html) via click or tap to navigate your browser to the new location!
|
||||
@@ -1,41 +1,120 @@
|
||||
# Talk · [](https://circleci.com/gh/coralproject/talk) · [](CONTRIBUTING.md#pull-requests)
|
||||
# Talk [](https://circleci.com/gh/coralproject/talk)
|
||||
|
||||
Online comments are broken. Our open-source commenting platform, Talk, rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/talk).
|
||||
Online comments are broken. Our open-source Talk tool rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here.](https://coralproject.net/products/talk.html)
|
||||
|
||||
Built with <3 by The Coral Project, a part of [Vox Media](https://www.voxmedia.com/).
|
||||
Third party licenses are available via the `/client/3rdpartylicenses.txt`
|
||||
endpoint when the server is running with built assets.
|
||||
|
||||
## Getting Started
|
||||
## Contributing to Talk
|
||||
|
||||
Check out our Quickstart and Install guides to get started with Talk in our [Technical Docs](https://docs.coralproject.net/talk/).
|
||||
See our [Contribution Guide](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md).
|
||||
|
||||
## Product Guide
|
||||
## Documentation
|
||||
|
||||
Learn more about Talk, including a deep dive into features for commenters and moderators, and FAQs in our [Talk Product Guide](https://docs.coralproject.net/talk/how-talk-works).
|
||||
### General
|
||||
|
||||
## Pre-Launch Guide
|
||||
See our [Talk Documentation & Guides](https://coralproject.github.io/talk/index.html).
|
||||
|
||||
You’ve installed Talk on your server, and you’re preparing to launch it on your site. The real community work starts now, before you go live. You have a unique opportunity pre-launch to set your community up for success. Read our [Talk Community Guide](https://coralproject.net/blog/youve-installed-talk-now-what/).
|
||||
### Plugins
|
||||
|
||||
## Advanced Usage
|
||||
See our guide to using and building [Talk Plugins](https://github.com/coralproject/talk/blob/master/PLUGINS.md).
|
||||
|
||||
For advanced configuration and usage of Talk, check out our [Configuration](https://docs.coralproject.net/talk/advanced-configuration/) and [Integration](https://docs.coralproject.net/talk/integrating/authentication/) how-tos. This covers topics in which you will need dev support to fully customize and integrate Talk, such as SSO/authentication, creating and managing assets and articles, styling Talk with custom CSS, and setting up Notifications and SMTP support.
|
||||
### Recipes
|
||||
|
||||
## Versions & Upgrading
|
||||
Recipes are plugin templates provided by the Coral Core team. Developers can use these recipes to build their own plugins. You can find all the Talk recipes here: https://github.com/coralproject/talk-recipes/
|
||||
|
||||
Check our Releases page for the latest recommended release version. [Releases](https://github.com/coralproject/talk/releases) All future even-numbered versions are considered stable LTS versions. We recommend the latest verified release for use in production environments.
|
||||
## Usage
|
||||
|
||||
## More Resources
|
||||
### Installation
|
||||
|
||||
- [Our Blog](https://coralproject.net/blog)
|
||||
- [Community Guides for Journalism](https://guides.coralproject.net/)
|
||||
- [More About Us](https://coralproject.net/)
|
||||
To set up a development environment or build from source, see [INSTALL.md](https://github.com/coralproject/talk/blob/master/INSTALL.md).
|
||||
|
||||
## End-to-End Testing
|
||||
To launch a Talk server of your own from your browser without any need to muck about in a terminal or think about engineering concepts, stay tuned. We will launch [our installer](https://github.com/coralproject/talk-install) shortly!
|
||||
|
||||
Talk uses [Nightwatch](http://nightwatchjs.org/) as our e2e testing framework. The testing infrastructure that allows us to run our tests in real browsers is provided with love by our friends at [Browserstack](https://browserstack.com).
|
||||
### Configuration
|
||||
|
||||
[](https://browserstack.com)
|
||||
The Talk application looks for the following configuration values either as environment variables:
|
||||
|
||||
- `TALK_MONGO_URL` (*required*) - the database connection string for the MongoDB database.
|
||||
- `TALK_REDIS_URL` (*required*) - the database connection string for the Redis database.
|
||||
- `TALK_ROOT_URL` (*required*) - root url of the installed application externally
|
||||
available in the format: `<scheme>://<host>` without the path.
|
||||
- `TALK_JWT_SECRET` (*required*) - a long and cryptographical secure random string which will be used to
|
||||
sign and verify tokens via a `HS256` algorithm.
|
||||
- `TALK_JWT_EXPIRY` (_optional_) - the expiry duration (`exp`) for the tokens issued for logged in sessions (Default `1 day`)
|
||||
- `TALK_JWT_ISSUER` (_optional_) - the issuer (`iss`) claim for login JWT tokens (Default `process.env.TALK_ROOT_URL`)
|
||||
- `TALK_JWT_AUDIENCE` (_optional_) - the audience (`aud`) claim for login JWT tokens (Default `talk`)
|
||||
- `TALK_SMTP_EMAIL` (*required for email*) - the address to send emails from using the
|
||||
SMTP provider.
|
||||
- `TALK_SMTP_USERNAME` (*required for email*) - username of the SMTP provider you are using.
|
||||
- `TALK_SMTP_PASSWORD` (*required for email*) - password for the SMTP provider you are using.
|
||||
- `TALK_SMTP_HOST` (*required for email*) - SMTP host url with format `smtp.domain.com`.
|
||||
- `TALK_SMTP_PORT` (*required for email*) - SMTP port.
|
||||
- `TALK_INSTALL_LOCK` (_optional for dynamic setup_) - Defaults to `FALSE`. When `TRUE`, disables the dynamic setup endpoint.
|
||||
- `TALK_RECAPTCHA_SECRET` (*required for reCAPTCHA support*) - server secret used for enabling reCAPTCHA powered logins. If not provided it will instead default to providing only a time based lockout.
|
||||
- `TALK_RECAPTCHA_PUBLIC` (*required for reCAPTCHA support*) - client secret used for enabling reCAPTCHA powered logins. If not provided it will instead default to providing only a time based lockout.
|
||||
- `TALK_PLUGINS_JSON` (_optional_) - used to specify the plugin config via the environment
|
||||
- `TALK_KEEP_ALIVE` (_optional_) - The keepalive timeout that should be used to send keep alive messages through the websocket to keep the socket alive. (Default `30s`)
|
||||
|
||||
Refer to the wiki page on [Configuration Loading](https://github.com/coralproject/talk/wiki/Configuration-Loading) for
|
||||
alternative methods of loading configuration during development.
|
||||
|
||||
### Running Migrations
|
||||
|
||||
We have a migration tool that can be run using `bin/cli migration run`. This will detect new migrations available and prompt you to backup your database before proceeding with the migration. Migrations are required with major version releases.
|
||||
|
||||
### Using Trust
|
||||
|
||||
Talk ships with core components we call "Trust". This allows Talk to automate certain actions based on previous user behavior.
|
||||
|
||||
Our first feature is the notion of Karma. Talk will automatically pre-moderate comments of users who have a negative karma score. You can [see more how karma works here](/services/karma.js).
|
||||
|
||||
## Supported Browsers
|
||||
|
||||
### Web
|
||||
|
||||
- Chrome: latest 2 versions
|
||||
- Firefox: latest 2 versions, and most recent extended support version, if any
|
||||
- Safari: latest 2 versions
|
||||
- Internet Explorer: IE Edge, 11
|
||||
|
||||
### iOS Devices
|
||||
|
||||
- iPad
|
||||
- iPad Pro
|
||||
- iPhone 7 Plus
|
||||
- iPhone 7
|
||||
- iPhone 6 Plus
|
||||
- iPhone 6
|
||||
- iPhone 5
|
||||
|
||||
### iOS Browsers
|
||||
|
||||
- Chrome for iOS: latest version
|
||||
- Firefox for iOS: latest version
|
||||
- Safari for iOS: latest version
|
||||
|
||||
### Android Devices
|
||||
|
||||
- Galaxy S5
|
||||
- Nexus 5X
|
||||
- Nexus 6P
|
||||
|
||||
### Android Browsers
|
||||
|
||||
- Chrome for Android: latest version
|
||||
- Firefox for Android: latest version
|
||||
|
||||
## License
|
||||
|
||||
Talk is released under the [Apache License, v2.0](/LICENSE).
|
||||
Copyright 2017 Mozilla Foundation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
||||
See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# Product's Terminology
|
||||
|
||||
This is a guide to have a common language to talk about "Talk".
|
||||
|
||||
## Definitions
|
||||
|
||||
* Site - a top level site, aka nytimes.com
|
||||
* Section - the section of a site, aka, Politics.
|
||||
* Subsection - the section of a site, aka, Politics.
|
||||
* Asset - An article/video/etc identified by URL.
|
||||
|
||||
* Embed - Things we put on a asset: comment box, ToS, Stream, etc…
|
||||
* Stream - All the activity on a certain asset. Container for Comments, actions, user
|
||||
* Thread - defined by a parent and everything below. All replies to a comment and their replies, etc…
|
||||
* Comment - a kind of user-generated content submitted by a comment author
|
||||
* A parent comment has replies to it
|
||||
* A child comments is a reply to another comment
|
||||
* A comment can be both a parent comment and a child of another comment
|
||||
* A top-level comment is a comment that is not a reply to any other comment
|
||||
* A nth-level comment refers to the number of replies away from the top-level comment
|
||||
|
||||
* User - an item to represent a person using Talk. It could be a moderator, reader, etc.
|
||||
* User Roles:
|
||||
* Active: some who takes action (logged in or not)
|
||||
* Passive: some who just reads, no actions performed
|
||||
* Comment Author: The user who wrote the comment
|
||||
* Staff Member: someone who works for an organization (tagged for leverage in trust)
|
||||
* Moderator: someone with the ability to access the moderation queue and perform moderation actions
|
||||
* Administrator: has the ability to change the setup of their coral space
|
||||
* Public Profile: information about users shown in public
|
||||
* Private Profile: information about users shown only to user about themselves
|
||||
* Protected Profile: information about users that only moderators and admins can see
|
||||
|
||||
* Queue - Group of items based on a query, aka - moderation queue
|
||||
* Target - The item/s on which an action is performed
|
||||
|
||||
## Actions
|
||||
|
||||
Actions are performed by users on items. Actions themselves are items. This requires two relationships: action on item, and user performs action.
|
||||
|
||||
### Flag
|
||||
* A Flagger(user) performs a Flag
|
||||
* A Flag is performed on a Comment or a username or profile content
|
||||
|
||||
|
||||
## Moderation Actions and Status
|
||||
|
||||
Comments contain a field `status`. As moderation actions are peformed, the status changes.
|
||||
|
||||
* Initial status is empty.
|
||||
* When a moderator Approves, the status is set to 'approved'.
|
||||
* When a moderator Rejects, the status is set to 'reject'.
|
||||
|
||||
### Pre and post moderation
|
||||
|
||||
Comments can be set to be premoderated or postmoderated.
|
||||
|
||||
Premoderation means that moderation has to occur _before_ a comment is shown on the site:
|
||||
|
||||
* New comments are shown in the moderator queues immediately.
|
||||
* The are not shown to users until (aka in streams) until they are approved by a moderator.
|
||||
|
||||
Postmoderation means that comments appear on the site _before_ any moderation action is taken.
|
||||
|
||||
* New comments appear in comment streams immediately.
|
||||
* New comments do not appear in moderation queues unless they are flagged by other users.
|
||||
|
||||
### Word lists
|
||||
|
||||
* Banned words - words that the site never allows in a comment
|
||||
* Suspect words - words whose usage needs to be approved by a moderator before being shown in the stream
|
||||
* Approved words - words that are usually Banned or Suspect sitewide, but approved for use in a specific article stream
|
||||
|
||||
@@ -1,108 +1,177 @@
|
||||
const express = require('express');
|
||||
const nunjucks = require('nunjucks');
|
||||
const cons = require('consolidate');
|
||||
const trace = require('./middleware/trace');
|
||||
const logging = require('./middleware/logging');
|
||||
const bodyParser = require('body-parser');
|
||||
const morgan = require('morgan');
|
||||
const path = require('path');
|
||||
const merge = require('lodash/merge');
|
||||
const helmet = require('helmet');
|
||||
const authentication = require('./middleware/authentication');
|
||||
const {passport} = require('./services/passport');
|
||||
const plugins = require('./services/plugins');
|
||||
const { HELMET_CONFIGURATION } = require('./config');
|
||||
const { MOUNT_PATH } = require('./url');
|
||||
const routes = require('./routes');
|
||||
const debug = require('debug')('talk:app');
|
||||
const { ENABLE_TRACING, APOLLO_ENGINE_KEY, PORT } = require('./config');
|
||||
const i18n = require('./services/i18n');
|
||||
const enabled = require('debug').enabled;
|
||||
const errors = require('./errors');
|
||||
const {createGraphOptions} = require('./graph');
|
||||
const apollo = require('graphql-server-express');
|
||||
const accepts = require('accepts');
|
||||
const compression = require('compression');
|
||||
const cookieParser = require('cookie-parser');
|
||||
|
||||
const app = express();
|
||||
|
||||
// Add the trace middleware first, it will create a request ID for each request
|
||||
// downstream.
|
||||
app.use(trace);
|
||||
|
||||
//==============================================================================
|
||||
// PLUGIN PRE APPLICATION MIDDLEWARE
|
||||
//==============================================================================
|
||||
|
||||
// Inject server route plugins.
|
||||
plugins.get('server', 'app').forEach(({ plugin, app: callback }) => {
|
||||
debug(`added plugin '${plugin.name}'`);
|
||||
|
||||
// Pass the app to the plugin to mount it's routes.
|
||||
callback(app);
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
// APPLICATION WIDE MIDDLEWARE
|
||||
//==============================================================================
|
||||
// Middleware declarations.
|
||||
|
||||
// Add the logging middleware only if we aren't testing.
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
app.use(logging.log);
|
||||
if (app.get('env') !== 'test') {
|
||||
app.use(morgan('dev'));
|
||||
}
|
||||
|
||||
if (ENABLE_TRACING && APOLLO_ENGINE_KEY) {
|
||||
const { Engine } = require('apollo-engine');
|
||||
//==============================================================================
|
||||
// APP MIDDLEWARE
|
||||
//==============================================================================
|
||||
|
||||
const engine = new Engine({
|
||||
engineConfig: {
|
||||
apiKey: APOLLO_ENGINE_KEY,
|
||||
},
|
||||
graphqlPort: PORT,
|
||||
endpoint: `${MOUNT_PATH}api/v1/graph/ql`,
|
||||
});
|
||||
|
||||
engine.start();
|
||||
|
||||
app.use(engine.expressMiddleware());
|
||||
}
|
||||
|
||||
// Trust the first proxy in front of us, this will enable us to trust the fact
|
||||
// that SSL was terminated correctly.
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
// Enable a suite of security good practices through helmet. We disable
|
||||
// frameguard to allow crossdomain injection of the embed.
|
||||
app.use(
|
||||
helmet(
|
||||
merge(HELMET_CONFIGURATION, {
|
||||
frameguard: false,
|
||||
})
|
||||
)
|
||||
);
|
||||
// We disable frameward on helmet to allow crossdomain injection of the embed
|
||||
app.use(helmet({
|
||||
frameguard: false
|
||||
}));
|
||||
app.use(compression());
|
||||
app.use(cookieParser());
|
||||
app.use(bodyParser.json());
|
||||
|
||||
//==============================================================================
|
||||
// STATIC FILES
|
||||
//==============================================================================
|
||||
|
||||
// If the application is in production mode, then add gzip rewriting for the
|
||||
// content.
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
app.get('*.js', (req, res, next) => {
|
||||
const accept = accepts(req);
|
||||
if (accept.encoding(['gzip']) === 'gzip') {
|
||||
|
||||
// Adjsut the headers on the request by adding a content type header
|
||||
// because express won't be able to detect the mime-type with the .gz
|
||||
// extension and we need to decalre support for the gzip encoding.
|
||||
res.set('Content-Type', 'application/javascript');
|
||||
res.set('Content-Encoding', 'gzip');
|
||||
|
||||
// Rewrite the url so that the gzip version will be served instead.
|
||||
req.url = `${req.url}.gz`;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
app.use('/client', express.static(path.join(__dirname, 'dist')));
|
||||
app.use('/public', express.static(path.join(__dirname, 'public')));
|
||||
|
||||
//==============================================================================
|
||||
// VIEW CONFIGURATION
|
||||
//==============================================================================
|
||||
|
||||
// configure the default views directory.
|
||||
const views = path.join(__dirname, 'views');
|
||||
app.set('views', views);
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
// reconfigure nunjucks.
|
||||
cons.requires.nunjucks = nunjucks.configure(views, {
|
||||
autoescape: true,
|
||||
trimBlocks: true,
|
||||
lstripBlocks: true,
|
||||
watch: process.env.NODE_ENV === 'development',
|
||||
//==============================================================================
|
||||
// PASSPORT MIDDLEWARE
|
||||
//==============================================================================
|
||||
|
||||
const passportDebug = require('debug')('talk:passport');
|
||||
|
||||
// Install the passport plugins.
|
||||
plugins.get('server', 'passport').forEach((plugin) => {
|
||||
passportDebug(`added plugin '${plugin.plugin.name}'`);
|
||||
|
||||
// Pass the passport.js instance to the plugin to allow it to inject it's
|
||||
// functionality.
|
||||
plugin.passport(passport);
|
||||
});
|
||||
|
||||
// assign the nunjucks engine to .njk files.
|
||||
app.engine('njk', cons.nunjucks);
|
||||
// Setup the PassportJS Middleware.
|
||||
app.use(passport.initialize());
|
||||
|
||||
// assign the ejs engine to .ejs and .html files.
|
||||
app.engine('ejs', cons.ejs);
|
||||
app.engine('html', cons.ejs);
|
||||
// Attach the authentication middleware, this will be responsible for decoding
|
||||
// (if present) the JWT on the request.
|
||||
app.use('/api', authentication);
|
||||
|
||||
// set .ejs as the default extension.
|
||||
app.set('view engine', 'ejs');
|
||||
//==============================================================================
|
||||
// GraphQL Router
|
||||
//==============================================================================
|
||||
|
||||
// GraphQL endpoint.
|
||||
app.use('/api/v1/graph/ql', apollo.graphqlExpress(createGraphOptions));
|
||||
|
||||
// Only include the graphiql tool if we aren't in production mode.
|
||||
if (app.get('env') !== 'production') {
|
||||
|
||||
// Interactive graphiql interface.
|
||||
app.use('/api/v1/graph/iql', (req, res) => {
|
||||
res.render('graphiql', {
|
||||
endpointURL: '/api/v1/graph/ql'
|
||||
});
|
||||
});
|
||||
|
||||
// GraphQL documention.
|
||||
app.get('/admin/docs', (req, res) => {
|
||||
res.render('admin/docs');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// ROUTES
|
||||
//==============================================================================
|
||||
|
||||
debug(`mounting routes on the ${MOUNT_PATH} path`);
|
||||
app.use('/', require('./routes'));
|
||||
|
||||
// Actually apply the routes.
|
||||
app.use(MOUNT_PATH, routes);
|
||||
//==============================================================================
|
||||
// ERROR HANDLING
|
||||
//==============================================================================
|
||||
|
||||
// Catch 404 and forward to error handler.
|
||||
app.use((req, res, next) => {
|
||||
next(errors.ErrNotFound);
|
||||
});
|
||||
|
||||
// General error handler. Respond with the message and error if we have it while
|
||||
// returning a status code that makes sense.
|
||||
app.use('/api', (err, req, res, next) => {
|
||||
if (err !== errors.ErrNotFound) {
|
||||
if (app.get('env') !== 'test' || enabled('talk:errors')) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (err instanceof errors.APIError) {
|
||||
res.status(err.status).json({
|
||||
message: err.message,
|
||||
error: err
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({});
|
||||
}
|
||||
});
|
||||
|
||||
app.use('/', (err, req, res, next) => {
|
||||
if (err !== errors.ErrNotFound) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
i18n.init(req);
|
||||
|
||||
if (err instanceof errors.APIError) {
|
||||
res.status(err.status);
|
||||
res.render('error', {
|
||||
message: err.message,
|
||||
error: app.get('env') === 'development' ? err : {}
|
||||
});
|
||||
} else {
|
||||
res.render('error', {
|
||||
message: err.message,
|
||||
error: app.get('env') === 'development' ? err : {}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
{
|
||||
"name": "The Coral Project: Talk",
|
||||
"env": {
|
||||
"TALK_JWT_SECRET": {
|
||||
"TALK_SESSION_SECRET": {
|
||||
"description": "The session secret",
|
||||
"generator": "secret"
|
||||
},
|
||||
"TALK_ROOT_URL": {
|
||||
"description": "Please copy the App Name you choose above. If you did not choose one, please do so now and copy it here. Talk on Heroku will not work without this setting.",
|
||||
"value":"https://<COPY APP NAME HERE>.herokuapp.com",
|
||||
"required": true
|
||||
},
|
||||
"TALK_FACEBOOK_APP_ID": {
|
||||
"value": "",
|
||||
"required": true
|
||||
@@ -18,12 +13,9 @@
|
||||
"value": "",
|
||||
"required": true
|
||||
},
|
||||
"MAILGUN_SMTP_PASSWORD": {
|
||||
"value": "",
|
||||
"required": true
|
||||
},
|
||||
"NODE_ENV": "production",
|
||||
"REWRITE_ENV": "TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:MAILGUN_SMTP_SERVER,TALK_SMTP_PORT:MAILGUN_SMTP_PORT,TALK_SMTP_USERNAME:MAILGUN_SMTP_LOGIN,TALK_SMTP_PASSWORD:MAILGUN_SMTP_PASSWORD",
|
||||
"TALK_SMTP_PORT": "2525",
|
||||
"REWRITE_ENV": "TALK_PORT:PORT,TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:POSTMARK_SMTP_SERVER,TALK_SMTP_USERNAME:POSTMARK_API_TOKEN,TALK_SMTP_PASSWORD:POSTMARK_API_TOKEN",
|
||||
"NPM_CONFIG_PRODUCTION": "false"
|
||||
},
|
||||
"addons": [{
|
||||
@@ -33,8 +25,8 @@
|
||||
"plan": "rediscloud:30",
|
||||
"as": "REDIS"
|
||||
}, {
|
||||
"plan": "mailgun:starter",
|
||||
"as": "MAILGUN"
|
||||
"plan": "postmark:10k",
|
||||
"as": "POSTMARK"
|
||||
}],
|
||||
"image": "heroku/nodejs",
|
||||
"success_url": "/admin/install"
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const program = require('commander');
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
// We're requiring this here so it'll setup some promise rejection hooks to log
|
||||
// out.
|
||||
require('./util');
|
||||
// const util = require('./util');
|
||||
const program = require('./commander');
|
||||
|
||||
// Setup the program.
|
||||
program
|
||||
.command('serve', 'serve the application')
|
||||
.command('db', 'run database commands')
|
||||
.command('settings', 'interact with the application settings')
|
||||
.command('assets', 'interact with assets')
|
||||
.command('setup', 'setup the application')
|
||||
@@ -22,3 +21,18 @@ program
|
||||
'provides utilities for interacting with the plugin system'
|
||||
)
|
||||
.parse(process.argv);
|
||||
|
||||
/**
|
||||
* When this provess exists, check to see if we have a running command, if we do
|
||||
* check to see if it is still running. If it is, then kill it with a SIGINT
|
||||
* signal. This is for the use case where we want to kill the process that is
|
||||
* labled with the PID written out by the parent process.
|
||||
*/
|
||||
process.once('exit', () => {
|
||||
if (
|
||||
program.runningCommand.killed === false &&
|
||||
program.runningCommand.exitCode === null
|
||||
) {
|
||||
program.runningCommand.kill('SIGINT');
|
||||
}
|
||||
});
|
||||
|
||||
+49
-182
@@ -4,52 +4,46 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const program = require('./commander');
|
||||
const parseDuration = require('ms');
|
||||
const Table = require('cli-table2');
|
||||
const Table = require('cli-table');
|
||||
const AssetModel = require('../models/asset');
|
||||
const CommentModel = require('../models/comment');
|
||||
const AssetsService = require('../services/assets');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const scraper = require('../services/scraper');
|
||||
const Context = require('../graph/context');
|
||||
const util = require('./util');
|
||||
const inquirer = require('inquirer');
|
||||
const { URL } = require('url');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
/**
|
||||
* Lists all the assets registered in the database.
|
||||
*/
|
||||
async function listAssets(opts) {
|
||||
async function listAssets() {
|
||||
try {
|
||||
let assets = await AssetModel.find({}).sort({ created_at: 1 });
|
||||
let assets = await AssetModel.find({}).sort({'created_at': 1});
|
||||
|
||||
switch (opts.format) {
|
||||
case 'json': {
|
||||
console.log(JSON.stringify(assets, null, 2));
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
let table = new Table({
|
||||
head: ['ID', 'Title', 'URL'],
|
||||
});
|
||||
let table = new Table({
|
||||
head: [
|
||||
'ID',
|
||||
'Title',
|
||||
'URL'
|
||||
]
|
||||
});
|
||||
|
||||
assets.forEach(asset => {
|
||||
table.push([
|
||||
asset.id,
|
||||
asset.title ? asset.title : '',
|
||||
asset.url ? asset.url : '',
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
assets.forEach((asset) => {
|
||||
table.push([
|
||||
asset.id,
|
||||
asset.title ? asset.title : '',
|
||||
asset.url ? asset.url : ''
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -59,45 +53,27 @@ async function listAssets(opts) {
|
||||
|
||||
async function refreshAssets(ageString) {
|
||||
try {
|
||||
const query = AssetModel.find({}, { id: 1 });
|
||||
if (ageString) {
|
||||
// An age was specified, so filter only those assets.
|
||||
const ageMs = parseDuration(ageString);
|
||||
const age = new Date(Date.now() - ageMs);
|
||||
const now = new Date().getTime();
|
||||
const ageMs = parseDuration(ageString);
|
||||
const age = new Date(now - ageMs);
|
||||
|
||||
query.merge({
|
||||
$or: [
|
||||
{
|
||||
scraped: {
|
||||
$lte: age,
|
||||
},
|
||||
},
|
||||
{
|
||||
scraped: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// Create a graph context.
|
||||
const ctx = Context.forSystem();
|
||||
|
||||
// Load the assets.
|
||||
const cursor = query.cursor();
|
||||
let assets = await AssetModel.find({
|
||||
$or: [
|
||||
{
|
||||
scraped: {
|
||||
$lte: age
|
||||
}
|
||||
},
|
||||
{
|
||||
scraped: null
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Queue all the assets for scraping.
|
||||
const promises = [];
|
||||
|
||||
let asset = await cursor.next();
|
||||
while (asset) {
|
||||
promises.push(scraper.create(ctx, asset.id));
|
||||
asset = await cursor.next();
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
console.log(`${promises.length} Assets were queued to be scraped.`);
|
||||
await Promise.all(assets.map(scraper.create));
|
||||
|
||||
console.log('Assets were queued to be scraped');
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -119,6 +95,7 @@ async function updateURL(assetID, assetURL) {
|
||||
|
||||
async function merge(srcID, dstID) {
|
||||
try {
|
||||
|
||||
// Grab the assets...
|
||||
let [srcAsset, dstAsset] = await AssetsService.findByIDs([srcID, dstID]);
|
||||
if (!srcAsset || !dstAsset) {
|
||||
@@ -126,22 +103,21 @@ async function merge(srcID, dstID) {
|
||||
}
|
||||
|
||||
// Count the affected resources...
|
||||
let srcCommentCount = await CommentModel.find({ asset_id: srcID }).count();
|
||||
let srcCommentCount = await CommentModel.find({asset_id: srcID}).count();
|
||||
|
||||
console.log(
|
||||
`Now going to update ${srcCommentCount} comments and delete the source Asset[${srcID}].`
|
||||
);
|
||||
console.log(`Now going to update ${srcCommentCount} comments and delete the source Asset[${srcID}].`);
|
||||
|
||||
let { confirm } = await inquirer.prompt([
|
||||
let {confirm} = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: 'Proceed with merge',
|
||||
default: false,
|
||||
},
|
||||
default: false
|
||||
}
|
||||
]);
|
||||
|
||||
if (confirm) {
|
||||
|
||||
// Perform the merge!
|
||||
await AssetsService.merge(srcID, dstID);
|
||||
} else {
|
||||
@@ -155,116 +131,17 @@ async function merge(srcID, dstID) {
|
||||
}
|
||||
}
|
||||
|
||||
async function rewrite(search, replace, options) {
|
||||
try {
|
||||
search = new RegExp(search);
|
||||
|
||||
const assets = await AssetModel.find({
|
||||
url: { $regex: search },
|
||||
});
|
||||
if (assets.length === 0) {
|
||||
console.log(`No assets found with the pattern: ${search}`);
|
||||
return util.shutdown(0);
|
||||
}
|
||||
|
||||
let opts = [];
|
||||
assets.forEach(({ id, url: oldURL }) => {
|
||||
// Replace the url.
|
||||
const newURL = oldURL.replace(search, replace);
|
||||
|
||||
// Try to validate that the new url is valid.
|
||||
try {
|
||||
new URL(newURL);
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Rewrite would have replaced the valid URL ${oldURL} with an invalid one ${newURL}`
|
||||
);
|
||||
}
|
||||
|
||||
opts.push({
|
||||
find: { id },
|
||||
updateOne: { $set: { url: newURL } },
|
||||
id,
|
||||
oldURL,
|
||||
newURL,
|
||||
});
|
||||
});
|
||||
|
||||
if (opts.length > 0) {
|
||||
if (options.dryRun) {
|
||||
const table = new Table({ head: ['ID', 'Old URL', 'New URL'] });
|
||||
|
||||
opts.forEach(({ id, oldURL, newURL }) => {
|
||||
table.push([id, oldURL, newURL]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
} else {
|
||||
const bulk = AssetModel.collection.initializeUnorderedBulkOp();
|
||||
opts.forEach(({ find, updateOne, oldURL, newURL }) => {
|
||||
// If the url was updated with the operation, then queue up the update op.
|
||||
if (newURL !== oldURL) {
|
||||
bulk.find(find).updateOne(updateOne);
|
||||
}
|
||||
});
|
||||
await bulk.execute();
|
||||
console.log(`${opts.length} assets had their url's updated`);
|
||||
}
|
||||
}
|
||||
|
||||
util.shutdown(0);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* debugAssetURL will scrape the given URL and print it's scraped values.
|
||||
*/
|
||||
async function debugAssetURL(url) {
|
||||
try {
|
||||
const meta = await scraper.scrape(url);
|
||||
|
||||
const table = new Table({ head: ['Property', 'Value'] });
|
||||
|
||||
for (const [property, value] of Object.entries(meta)) {
|
||||
table.push([property, value]);
|
||||
}
|
||||
|
||||
console.log(table.toString());
|
||||
|
||||
util.shutdown(0);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.command('list')
|
||||
.option(
|
||||
'--format <type>',
|
||||
'Specify the output format [table]',
|
||||
/^(table|json)$/i,
|
||||
'table'
|
||||
)
|
||||
.description('list all the assets in the database')
|
||||
.action(listAssets);
|
||||
|
||||
program
|
||||
.command('debug [url]')
|
||||
.description(
|
||||
'prints the scraped metadata that would be added to the given asset'
|
||||
)
|
||||
.action(debugAssetURL);
|
||||
|
||||
program
|
||||
.command('refresh [age]')
|
||||
.command('refresh <age>')
|
||||
.description('queues the assets that exceed the age requested')
|
||||
.action(refreshAssets);
|
||||
|
||||
@@ -275,19 +152,9 @@ program
|
||||
|
||||
program
|
||||
.command('merge <srcID> <dstID>')
|
||||
.description(
|
||||
'merges two assets together by moving comments from src to dst and deleting the src asset'
|
||||
)
|
||||
.description('merges two assets together by moving comments from src to dst and deleting the src asset')
|
||||
.action(merge);
|
||||
|
||||
program
|
||||
.command('rewrite <search> <replace>')
|
||||
.option('-d, --dry-run', 'enables dry run of the replacement')
|
||||
.description(
|
||||
"rewrites asset url's using the provided regex replacement pattern"
|
||||
)
|
||||
.action(rewrite);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const config = require('../config');
|
||||
|
||||
async function createIndexes() {
|
||||
try {
|
||||
// Ensure we enable the index creation.
|
||||
config.CREATE_MONGO_INDEXES = true;
|
||||
|
||||
// TODO: handle the plugin index creation?
|
||||
|
||||
// Let's register the shutdown hooks.
|
||||
util.onshutdown([() => require('../services/mongoose').disconnect()]);
|
||||
|
||||
// Lets create all the database indexes for the application and wait for all
|
||||
// them to finish their indexing.
|
||||
const models = [
|
||||
require('../models/action'),
|
||||
require('../models/asset'),
|
||||
require('../models/comment'),
|
||||
require('../models/setting'),
|
||||
require('../models/user'),
|
||||
require('../models/migration'),
|
||||
];
|
||||
|
||||
// Call the `.init()` method to setup all the indexes on each model.
|
||||
// `init()` returns a promise that resolves when the indexes have finished
|
||||
// building successfully. The `init()` function is idempotent, so we don't
|
||||
// have to worry about triggering an index rebuild.
|
||||
await Promise.all(models.map(Model => Model.init()));
|
||||
|
||||
console.log('Indexes created');
|
||||
util.shutdown(0);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
program
|
||||
.command('createIndexes')
|
||||
.description('creates the database indexes and waits until they are created')
|
||||
.action(createIndexes);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
if (process.argv.length <= 2) {
|
||||
program.outputHelp();
|
||||
util.shutdown();
|
||||
}
|
||||
+41
-55
@@ -4,24 +4,33 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const scraper = require('../services/scraper');
|
||||
const mailer = require('../services/mailer');
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const jobs = require('../jobs');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const kue = require('../services/kue');
|
||||
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
/**
|
||||
* Starts the job processor.
|
||||
*/
|
||||
function processJobs() {
|
||||
|
||||
// Start the scraper processor.
|
||||
scraper.process();
|
||||
|
||||
// Start the mail processor.
|
||||
mailer.process();
|
||||
|
||||
// The scraper only needs to shutdown when the scraper has actually been
|
||||
// started.
|
||||
util.onshutdown([() => kue.Task.shutdown()]);
|
||||
|
||||
// Start the jobs processor.
|
||||
jobs.process();
|
||||
util.onshutdown([
|
||||
() => kue.Task.shutdown()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,15 +39,22 @@ function processJobs() {
|
||||
* @return {Promise}
|
||||
*/
|
||||
function removeJob(job) {
|
||||
return new Promise((resolve, reject) =>
|
||||
job.remove(err => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
return new Promise((resolve, reject) => job.remove((err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve(job);
|
||||
})
|
||||
);
|
||||
return resolve(job);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the jobs passed in and returns a promise.
|
||||
* @param {Array} jobs array of jobs
|
||||
* @return {Promise}
|
||||
*/
|
||||
function removeJobs(jobs) {
|
||||
return Promise.all(jobs.map(removeJob));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,7 +63,7 @@ function removeJob(job) {
|
||||
* @param {Number} limit limit of jobs to load
|
||||
* @return {Promise}
|
||||
*/
|
||||
function rangeJobsByState(state, limit) {
|
||||
function rangeJobsByState(state = 'complete', limit) {
|
||||
return new Promise((resolve, reject) => {
|
||||
kue.Job.rangeByState(state, 0, limit, 'asc', (err, jobs) => {
|
||||
if (err) {
|
||||
@@ -59,51 +75,21 @@ function rangeJobsByState(state, limit) {
|
||||
});
|
||||
}
|
||||
|
||||
async function getJobBatch(n, includeStuck) {
|
||||
let jobs = [];
|
||||
|
||||
jobs = await rangeJobsByState('complete', n);
|
||||
|
||||
if (includeStuck) {
|
||||
jobs = jobs.concat(await rangeJobsByState('failed', n));
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the jobs that are in the queue.
|
||||
*/
|
||||
async function cleanupJobs(options) {
|
||||
// The scraper only needs to shutdown when the scraper has actually been
|
||||
// started.
|
||||
util.onshutdown([() => kue.Task.shutdown()]);
|
||||
|
||||
function cleanupJobs(options) {
|
||||
const n = 100;
|
||||
|
||||
try {
|
||||
// Connect to redis by establishing a queue.
|
||||
kue.Task.connect();
|
||||
|
||||
let jobCount = 0;
|
||||
let jobs = await getJobBatch(n, options.stuck);
|
||||
|
||||
while (jobs.length > 0) {
|
||||
// Remove all the jobs.
|
||||
await Promise.all(jobs.map(job => removeJob(job)));
|
||||
|
||||
jobCount += jobs.length;
|
||||
|
||||
// Get the next batch of jobs.
|
||||
jobs = await getJobBatch(n, options.stuck);
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
rangeJobsByState('complete', n),
|
||||
options.stuck ? rangeJobsByState('failed', n) : false
|
||||
])
|
||||
.then((joblists) => joblists.filter((jobs) => jobs).map(removeJobs))
|
||||
.then(() => {
|
||||
util.shutdown();
|
||||
console.log(`Removed ${jobCount} jobs`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
}
|
||||
console.log('Removed old jobs');
|
||||
});
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
||||
+33
-61
@@ -4,18 +4,20 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const util = require('./util');
|
||||
const _ = require('lodash');
|
||||
const program = require('commander');
|
||||
const inquirer = require('inquirer');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const MigrationService = require('../services/migration');
|
||||
|
||||
// Register shutdown hooks.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
async function createMigration(name) {
|
||||
try {
|
||||
|
||||
// Create the migration.
|
||||
await MigrationService.create(name);
|
||||
|
||||
@@ -26,60 +28,47 @@ async function createMigration(name) {
|
||||
}
|
||||
}
|
||||
|
||||
async function runMigrations(options) {
|
||||
const { yes, queryBatchSize, updateBatchSize } = options;
|
||||
try {
|
||||
if (!yes) {
|
||||
const { backedUp } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'backedUp',
|
||||
message: 'Did you perform a database backup',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
async function runMigrations() {
|
||||
|
||||
if (!backedUp) {
|
||||
throw new Error(
|
||||
'Please backup your databases prior to migrations occuring'
|
||||
);
|
||||
try {
|
||||
|
||||
let {backedUp} = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'backedUp',
|
||||
message: 'Did you perform a database backup',
|
||||
default: false
|
||||
}
|
||||
]);
|
||||
|
||||
if (!backedUp) {
|
||||
throw new Error('Please backup your databases prior to migrations occuring');
|
||||
}
|
||||
|
||||
// Get the migrations to run.
|
||||
const migrations = await MigrationService.listPending();
|
||||
let migrations = await MigrationService.listPending();
|
||||
|
||||
console.log('Now going to run the following migrations:\n');
|
||||
|
||||
for (const { filename } of migrations) {
|
||||
for (let {filename} of migrations) {
|
||||
console.log(`\tmigrations/${filename}`);
|
||||
}
|
||||
|
||||
if (!yes) {
|
||||
const { confirm } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: 'Proceed with migrations',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
if (confirm) {
|
||||
// Run the migrations.
|
||||
await MigrationService.run(migrations, {
|
||||
queryBatchSize,
|
||||
updateBatchSize,
|
||||
});
|
||||
} else {
|
||||
console.warn('Skipping migrations');
|
||||
let {confirm} = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: 'Proceed with migrations',
|
||||
default: false
|
||||
}
|
||||
} else {
|
||||
]);
|
||||
|
||||
if (confirm) {
|
||||
|
||||
// Run the migrations.
|
||||
await MigrationService.run(migrations, {
|
||||
queryBatchSize,
|
||||
updateBatchSize,
|
||||
});
|
||||
await MigrationService.run(migrations);
|
||||
} else {
|
||||
console.warn('Skipping migrations');
|
||||
}
|
||||
|
||||
util.shutdown();
|
||||
@@ -98,25 +87,8 @@ program
|
||||
.description('creates a new migration')
|
||||
.action(createMigration);
|
||||
|
||||
// Bypasses issue that defaults + coercion doesn't work well together.
|
||||
// Ref: https://github.com/tj/commander.js/issues/400#issuecomment-310860869
|
||||
const parse10 = _.ary(_.partialRight(parseInt, 10), 1);
|
||||
|
||||
program
|
||||
.command('run')
|
||||
.option(
|
||||
'-q, --query-batch-size <n>',
|
||||
'change the size of queried documents that are batched at a time',
|
||||
parse10,
|
||||
10000
|
||||
)
|
||||
.option(
|
||||
'-u, --update-batch-size <n>',
|
||||
'change the size of documents that are batched before the update is sent',
|
||||
parse10,
|
||||
20000
|
||||
)
|
||||
.option('-y, --yes', 'will answer yes to all questions')
|
||||
.description('runs all pending migrations')
|
||||
.action(runMigrations);
|
||||
|
||||
|
||||
+148
-137
@@ -7,25 +7,24 @@
|
||||
// Interface heavily inspired by the yarn package manager:
|
||||
// https://yarnpkg.com/
|
||||
|
||||
require('./util');
|
||||
const program = require('commander');
|
||||
const program = require('./commander');
|
||||
const inquirer = require('inquirer');
|
||||
|
||||
// Make things colorful!
|
||||
require('colors');
|
||||
const emoji = require('node-emoji');
|
||||
|
||||
const dir = process.cwd();
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const dir = path.resolve(__dirname, '..');
|
||||
const spawn = require('cross-spawn');
|
||||
const semver = require('semver');
|
||||
const resolve = require('resolve');
|
||||
const { plugins, iteratePlugins, isInternal } = require('../plugins');
|
||||
const {plugins, itteratePlugins, isInternal} = require('../plugins');
|
||||
|
||||
function existsInNodeModules(name) {
|
||||
try {
|
||||
resolve.sync(name, { basedir: dir });
|
||||
resolve.sync(name, {basedir: dir});
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -39,13 +38,13 @@ function versionMatch(name, version) {
|
||||
|
||||
resolve.sync(name, {
|
||||
basedir: dir,
|
||||
packageFilter: pkg => {
|
||||
packageFilter: (pkg) => {
|
||||
if (pkg && pkg.version && semver.satisfies(pkg.version, version)) {
|
||||
matched = true;
|
||||
}
|
||||
|
||||
return pkg;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
return matched;
|
||||
@@ -54,9 +53,9 @@ function versionMatch(name, version) {
|
||||
}
|
||||
}
|
||||
|
||||
const EXTERNAL = /^\w[a-z\-0-9.]+$/; // Match "react", "path", "fs", "lodash.random", etc.
|
||||
const EXTERNAL = /^\w[a-z\-0-9\.]+$/; // Match "react", "path", "fs", "lodash.random", etc.
|
||||
|
||||
function reconcilePackages({ quiet = false, upgradeRemote = false }) {
|
||||
function reconcilePackages({quiet = false, upgradeRemote = false}) {
|
||||
const fetchable = [];
|
||||
const local = [];
|
||||
const upgradable = [];
|
||||
@@ -71,14 +70,13 @@ function reconcilePackages({ quiet = false, upgradeRemote = false }) {
|
||||
}
|
||||
|
||||
for (let i in plugins) {
|
||||
let section = iteratePlugins(plugins[i]);
|
||||
let section = itteratePlugins(plugins[i]);
|
||||
|
||||
for (let j in section) {
|
||||
let { name, version } = section[j];
|
||||
let {name, version} = section[j];
|
||||
|
||||
let namespaced = name.charAt(0) === '@';
|
||||
let dep = name
|
||||
.split('/')
|
||||
let dep = name.split('/')
|
||||
.slice(0, namespaced ? 2 : 1)
|
||||
.join('/');
|
||||
|
||||
@@ -92,7 +90,7 @@ function reconcilePackages({ quiet = false, upgradeRemote = false }) {
|
||||
console.log(` l ${name}`);
|
||||
}
|
||||
|
||||
local.push({ name, version });
|
||||
local.push({name, version});
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -100,8 +98,9 @@ function reconcilePackages({ quiet = false, upgradeRemote = false }) {
|
||||
if (!quiet) {
|
||||
console.log(` m ${name}`);
|
||||
}
|
||||
fetchable.push({ name, version });
|
||||
fetchable.push({name, version});
|
||||
} else if (!versionMatch(dep, version)) {
|
||||
|
||||
// A plugin was found, yet the current version does not match the
|
||||
// current version installed. We should warn if upgradeRemote is
|
||||
// not enabled that it is currently not supported.
|
||||
@@ -115,14 +114,14 @@ function reconcilePackages({ quiet = false, upgradeRemote = false }) {
|
||||
|
||||
console.log(` oe ${name} (package upgrade may be required)`);
|
||||
|
||||
upgradable.push({ name, version });
|
||||
upgradable.push({name, version});
|
||||
} else {
|
||||
if (!quiet) {
|
||||
console.log(` e ${name}`);
|
||||
}
|
||||
|
||||
if (upgradeRemote) {
|
||||
upgradable.push({ name, version });
|
||||
upgradable.push({name, version});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,38 +131,33 @@ function reconcilePackages({ quiet = false, upgradeRemote = false }) {
|
||||
console.log();
|
||||
}
|
||||
|
||||
return { local, fetchable, upgradable };
|
||||
return {local, fetchable, upgradable};
|
||||
}
|
||||
|
||||
async function reconcileRemotePlugins({ dryRun, upgradeRemote }) {
|
||||
console.log(`\n['1/2'] ${emoji.get('mag')} Reconciling plugins...`.yellow);
|
||||
const { fetchable, upgradable } = reconcilePackages({ upgradeRemote });
|
||||
async function reconcileRemotePlugins({skipLocal, dryRun, upgradeRemote}) {
|
||||
console.log(`\n[${skipLocal ? '1/2' : '2/3'}] ${emoji.get('mag')} Reconciling plugins...`.yellow);
|
||||
const {fetchable, upgradable} = reconcilePackages({upgradeRemote});
|
||||
|
||||
console.log(`['2/2'] ${emoji.get('truck')} Fetching plugins...\n`.yellow);
|
||||
console.log(`[${skipLocal ? '2/2' : '3/3'}] ${emoji.get('truck')} Fetching plugins...\n`.yellow);
|
||||
|
||||
if (fetchable.length > 0) {
|
||||
console.log(
|
||||
`$ yarn add --ignore-scripts --ignore-workspace-root-check ${fetchable
|
||||
.map(({ name, version }) => `${name}@${version}`.cyan)
|
||||
.join(' ')}`
|
||||
);
|
||||
|
||||
console.log(`$ yarn add --ignore-scripts ${fetchable.map(({name, version}) => `${name}@${version}`.cyan)}`);
|
||||
|
||||
if (!dryRun) {
|
||||
|
||||
let args = [
|
||||
'add',
|
||||
'--ignore-scripts',
|
||||
'--ignore-workspace-root-check',
|
||||
...fetchable.map(({ name, version }) => `${name}@${version}`),
|
||||
...fetchable.map(({name, version}) => `${name}@${version}`)
|
||||
];
|
||||
|
||||
let output = spawn.sync('yarn', args, {
|
||||
stdio: ['ignore', 'pipe', 'inherit'],
|
||||
stdio: ['ignore', 'pipe', 'inherit']
|
||||
});
|
||||
|
||||
if (output.status) {
|
||||
throw new Error(
|
||||
'Could not install external plugins, errors occurred during install'
|
||||
);
|
||||
throw new Error('Could not install external plugins, errors occured during install');
|
||||
}
|
||||
|
||||
console.log(output.stdout.toString());
|
||||
@@ -171,47 +165,86 @@ async function reconcileRemotePlugins({ dryRun, upgradeRemote }) {
|
||||
}
|
||||
|
||||
if (upgradable.length > 0) {
|
||||
console.log(
|
||||
`$ yarn upgrade ${upgradable.map(
|
||||
({ name, version }) => `${name}@${version}`.cyan
|
||||
)}`
|
||||
);
|
||||
console.log(`$ yarn upgrade ${upgradable.map(({name, version}) => `${name}@${version}`.cyan)}`);
|
||||
|
||||
if (!dryRun) {
|
||||
|
||||
let args = [
|
||||
'upgrade',
|
||||
...upgradable.map(({ name, version }) => `${name}@${version}`),
|
||||
...upgradable.map(({name, version}) => `${name}@${version}`)
|
||||
];
|
||||
|
||||
let output = spawn.sync('yarn', args, {
|
||||
stdio: ['ignore', 'pipe', 'inherit'],
|
||||
stdio: ['ignore', 'pipe', 'inherit']
|
||||
});
|
||||
|
||||
if (output.status) {
|
||||
throw new Error(
|
||||
'Could not install external plugins, errors occurred during install'
|
||||
);
|
||||
throw new Error('Could not install external plugins, errors occured during install');
|
||||
}
|
||||
|
||||
console.log(output.stdout.toString());
|
||||
}
|
||||
}
|
||||
|
||||
return { upgradable, fetchable };
|
||||
return {upgradable, fetchable};
|
||||
}
|
||||
|
||||
async function reconcileLocalPlugins({skipRemote, dryRun}) {
|
||||
console.log(`\n[${skipRemote ? '1/1' : '1/3'}] ${emoji.get('pick')} Installing local plugin dependencies...\n`.yellow);
|
||||
const {local} = reconcilePackages({quiet: true});
|
||||
|
||||
for (let i in local) {
|
||||
let {name} = local[i];
|
||||
|
||||
if (!fs.existsSync(path.join(dir, 'plugins', name, 'package.json'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let wd = path.join(dir, 'plugins', name);
|
||||
|
||||
console.log(`$ cd ${wd.cyan} && yarn`);
|
||||
|
||||
if (!dryRun) {
|
||||
let args = [];
|
||||
|
||||
let output = spawn.sync('yarn', args, {
|
||||
stdio: ['ignore', 'pipe', 'inherit'],
|
||||
cwd: wd
|
||||
});
|
||||
|
||||
if (output.status) {
|
||||
throw new Error('Could not install local plugin dependencies, errors occured during install');
|
||||
}
|
||||
|
||||
console.log(output.stdout.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This traverses the local plugins and installs any dependencies listed there,
|
||||
// this only is really needed for plugins that are installed via docker because
|
||||
// core plugins will have their dependencies already included in core.
|
||||
async function reconcilePluginDeps({ dryRun, upgradeRemote }) {
|
||||
try {
|
||||
let startTime = new Date();
|
||||
async function reconcilePluginDeps({skipLocal, skipRemote, dryRun, upgradeRemote}) {
|
||||
let startTime = new Date();
|
||||
|
||||
// Locate any external plugins and install them.
|
||||
const results = await reconcileRemotePlugins({
|
||||
dryRun,
|
||||
upgradeRemote,
|
||||
});
|
||||
// We don't need to do anything if we skip everything....
|
||||
if (skipLocal && skipRemote) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Traverse local plugins and install dependencies if enabled.
|
||||
if (!skipLocal) {
|
||||
await reconcileLocalPlugins({skipRemote, dryRun});
|
||||
}
|
||||
|
||||
// Locate any external plugins and install them.
|
||||
if (!skipRemote) {
|
||||
let results = [];
|
||||
try {
|
||||
results = await reconcileRemotePlugins({skipLocal, skipRemote, dryRun, upgradeRemote});
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
let status;
|
||||
if (dryRun) {
|
||||
@@ -228,31 +261,25 @@ async function reconcilePluginDeps({ dryRun, upgradeRemote }) {
|
||||
} else if (results.fetchable.length === 0) {
|
||||
message = `Upgraded ${results.upgradable.length} new plugins.`;
|
||||
} else {
|
||||
message = `Fetched ${results.fetchable.length} new plugins, upgraded ${
|
||||
results.upgradable.length
|
||||
} plugins.`;
|
||||
message = `Fetched ${results.fetchable.length} new plugins, upgraded ${results.upgradable.length} plugins.`;
|
||||
}
|
||||
|
||||
console.log(`\n${status} ${message}`);
|
||||
|
||||
let endTime = new Date();
|
||||
|
||||
let totalTime = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(
|
||||
2
|
||||
);
|
||||
console.log(`✨ Done in ${totalTime}s.`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let endTime = new Date();
|
||||
|
||||
let totalTime = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(2);
|
||||
console.log(`✨ Done in ${totalTime}s.`);
|
||||
}
|
||||
|
||||
async function createSeedPlugin() {
|
||||
const pluginsDir = path.resolve(__dirname, '..', 'plugins');
|
||||
const pluginsDir = path.join(__dirname, 'plugins');
|
||||
|
||||
function pluginNameExists(pluginName) {
|
||||
const pluginNames = fs.readdirSync(pluginsDir);
|
||||
return !!pluginNames.filter(pn => pn === pluginName).length;
|
||||
return !!pluginNames
|
||||
.filter((pn) => pn === pluginName).length;
|
||||
}
|
||||
|
||||
let answers = await inquirer.prompt([
|
||||
@@ -260,7 +287,8 @@ async function createSeedPlugin() {
|
||||
type: 'input',
|
||||
name: 'pluginName',
|
||||
message: 'Plugin Name:',
|
||||
validate: input => {
|
||||
validate: (input) => {
|
||||
|
||||
if (pluginNameExists(input)) {
|
||||
return 'Please, choose another name. That name already exists';
|
||||
}
|
||||
@@ -270,108 +298,92 @@ async function createSeedPlugin() {
|
||||
}
|
||||
|
||||
return 'Plugin Name is required.';
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'server',
|
||||
message: 'Is this plugin extending the server capabilities?',
|
||||
message: 'Is this plugin extending the server capabilities?'
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'client',
|
||||
message: 'Is this plugin extending the client capabilities?',
|
||||
message: 'Is this plugin extending the client capabilities?'
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'addPluginsJson',
|
||||
message: 'Should we add it to the plugins.json?',
|
||||
},
|
||||
message: 'Should we add it to the plugins.json?'
|
||||
}
|
||||
]);
|
||||
|
||||
//==============================================================================
|
||||
// Creating plugin seed
|
||||
//==============================================================================
|
||||
|
||||
const seedPlugin = path.join(__dirname, 'templates/plugin');
|
||||
const seedPlugin = path.join(__dirname, 'bin/templates/plugin');
|
||||
const newPluginPath = path.join(pluginsDir, answers.pluginName);
|
||||
|
||||
if (fs.existsSync(seedPlugin)) {
|
||||
|
||||
if (answers.server && answers.client) {
|
||||
|
||||
// This is a server-side and client-side plugin!, let's copy the template
|
||||
fs.copySync(seedPlugin, newPluginPath);
|
||||
} else {
|
||||
fs.copySync(seedPlugin, newPluginPath, {
|
||||
filter: p => {
|
||||
// Allowing plugin folder and files with no subfolders
|
||||
const rootRx = /plugin$|plugin\/[^/]*(\.).{2,3}/gim;
|
||||
if (
|
||||
rootRx.test(p) &&
|
||||
(fs.lstatSync(p).isDirectory() || fs.lstatSync(p).isFile())
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
|
||||
// If it's a client-side plugin, copying client folder
|
||||
if (answers.client) {
|
||||
return /client/.test(p);
|
||||
}
|
||||
fs.copySync(seedPlugin, newPluginPath, {filter: (p) => {
|
||||
|
||||
// If it's a server-side plugin, copying server folder
|
||||
if (answers.server) {
|
||||
return /server/.test(p);
|
||||
}
|
||||
},
|
||||
});
|
||||
// Allowing plugin folder and files with no subfolders
|
||||
const rootRx = /plugin$|plugin\/[^/]*(\.).{2,3}/igm;
|
||||
if (rootRx.test(p) && (fs.lstatSync(p).isDirectory() || fs.lstatSync(p).isFile())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If it's a client-side plugin, copying client folder
|
||||
if (answers.client) {
|
||||
return /client/.test(p);
|
||||
}
|
||||
|
||||
// If it's a server-side plugin, copying server folder
|
||||
if (answers.server) {
|
||||
return /server/.test(p);
|
||||
}
|
||||
|
||||
}});
|
||||
}
|
||||
|
||||
// Let's add this to the plugins.json
|
||||
if (answers.addPluginsJson) {
|
||||
const pluginsJson = path.resolve(__dirname, '..', 'plugins.json');
|
||||
const pluginsJson = path.join(dir, 'plugins.json');
|
||||
|
||||
let j;
|
||||
try {
|
||||
j = await fs.readJson(pluginsJson);
|
||||
} catch (err) {
|
||||
// Fallback to plugins.default.json if the plugins.json file does not
|
||||
// exist.
|
||||
const defaultPluginsJson = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'plugins.default.json'
|
||||
);
|
||||
fs.readJson(pluginsJson)
|
||||
.then((j) => {
|
||||
|
||||
try {
|
||||
j = await fs.readJson(defaultPluginsJson);
|
||||
} catch (err) {
|
||||
// Fallback to an empty one if that also, doesn't exist.
|
||||
j = { client: [], server: [] };
|
||||
}
|
||||
}
|
||||
// This is a client-side plugin, let's push this.
|
||||
if (answers.client) {
|
||||
j.client.push(answers.pluginName);
|
||||
|
||||
// This is a client-side plugin, let's push this.
|
||||
if (answers.client) {
|
||||
j.client.push(answers.pluginName);
|
||||
const output = JSON.stringify(j, null, 2);
|
||||
fs.writeFileSync(pluginsJson, output);
|
||||
}
|
||||
|
||||
const output = JSON.stringify(j, null, 2);
|
||||
fs.writeFileSync(pluginsJson, output);
|
||||
}
|
||||
// This is a server-side plugin, let's push this.
|
||||
if (answers.server) {
|
||||
j.server.push(answers.pluginName);
|
||||
|
||||
// This is a server-side plugin, let's push this.
|
||||
if (answers.server) {
|
||||
j.server.push(answers.pluginName);
|
||||
|
||||
const output = JSON.stringify(j, null, 2);
|
||||
fs.writeFileSync(pluginsJson, output);
|
||||
}
|
||||
const output = JSON.stringify(j, null, 2);
|
||||
fs.writeFileSync(pluginsJson, output);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✨ Yay! Plugin created! Find your plugin: ${
|
||||
answers.pluginName
|
||||
} in the ./plugins folder`
|
||||
);
|
||||
console.log(`✨ Yay! Plugin created! Find your plugin: ${answers.pluginName} in the ./plugins folder`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
@@ -390,12 +402,11 @@ program
|
||||
|
||||
program
|
||||
.command('reconcile')
|
||||
.description('reconciles dependencies by downloading external plugins')
|
||||
.description('reconciles local plugin dependencies and downloads external plugins')
|
||||
.option('-u, --upgrade-remote', 'upgrades remote dependencies')
|
||||
.option(
|
||||
'-d, --dry-run',
|
||||
'does not actually change anything on the filesystem acts only as a simulation'
|
||||
)
|
||||
.option('-d, --dry-run', 'does not actually change anything on the filesystem acts only as a simulation')
|
||||
.option('--skip-local', 'skips the local dependancy reconciliation')
|
||||
.option('--skip-remote', 'skips the remote plugin reconciliation')
|
||||
.action(reconcilePluginDeps);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
+167
-27
@@ -1,10 +1,153 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const throng = require('throng');
|
||||
const { CONCURRENCY } = require('../config');
|
||||
const { logger } = require('../services/logging');
|
||||
const program = require('./commander');
|
||||
const app = require('../app');
|
||||
const debug = require('debug')('talk:cli:serve');
|
||||
const errors = require('../errors');
|
||||
const {createServer} = require('http');
|
||||
const scraper = require('../services/scraper');
|
||||
const mailer = require('../services/mailer');
|
||||
const MigrationService = require('../services/migration');
|
||||
const SetupService = require('../services/setup');
|
||||
const kue = require('../services/kue');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const {createSubscriptionManager} = require('../graph/subscriptions');
|
||||
const {
|
||||
PORT
|
||||
} = require('../config');
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
|
||||
const port = normalizePort(PORT);
|
||||
app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
const server = createServer(app);
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*/
|
||||
function onError(error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
let bind = typeof port === 'string'
|
||||
? `Pipe ${port}`
|
||||
: `Port ${port}`;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(`${bind} requires elevated privileges`);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(`${bind} is already in use`);
|
||||
break;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*/
|
||||
|
||||
function normalizePort(val) {
|
||||
let port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
|
||||
function onListening() {
|
||||
let addr = server.address();
|
||||
let bind = typeof addr === 'string'
|
||||
? `pipe ${addr}`
|
||||
: `port ${addr.port}`;
|
||||
console.log(`API Server Listening on ${bind}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the app.
|
||||
*/
|
||||
async function startApp(program) {
|
||||
|
||||
try {
|
||||
|
||||
// Check to see if the application is installed. If the application
|
||||
// has been installed, then it will throw errors.ErrSettingsNotInit, this
|
||||
// just means we don't have to check that the migrations have run.
|
||||
await SetupService.isAvailable();
|
||||
|
||||
debug('setup is currently available, migrations not being checked');
|
||||
|
||||
} catch (e) {
|
||||
|
||||
// Check the error.
|
||||
switch (e) {
|
||||
case errors.ErrInstallLock, errors.ErrSettingsInit:
|
||||
|
||||
debug('setup is not currently available, migrations now being checked');
|
||||
|
||||
// The error was expected, just continue.
|
||||
break;
|
||||
default:
|
||||
|
||||
// The error was not expected, throw the error!
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Now try and check the migration status.
|
||||
try {
|
||||
|
||||
// Verify that the minimum migration version is met.
|
||||
await MigrationService.verify();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
debug('migrations do not have to be run');
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
server.listen(port, () => {
|
||||
|
||||
// Mount the websocket server if requested.
|
||||
if (program.websockets) {
|
||||
console.log(`Websocket Server Listening on ${port}`);
|
||||
|
||||
// Mount the subscriptions server on the application server.
|
||||
createSubscriptionManager(server);
|
||||
}
|
||||
});
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
@@ -12,30 +155,27 @@ const program = require('commander');
|
||||
|
||||
program
|
||||
.option('-j, --jobs', 'enable job processing on this thread')
|
||||
.option(
|
||||
'--disabled-jobs <jobs>',
|
||||
'disable jobs specified if the -j option is passed, specified as a comma separated list',
|
||||
val => val.split(','),
|
||||
[]
|
||||
)
|
||||
.option(
|
||||
'-w, --websockets',
|
||||
'enable the websocket (subscriptions) handler on this thread'
|
||||
)
|
||||
.option('-w, --websockets', 'enable the websocket (subscriptions) handler on this thread')
|
||||
.parse(process.argv);
|
||||
|
||||
throng({
|
||||
workers: CONCURRENCY,
|
||||
start: i => {
|
||||
logger.info({ workerID: i }, 'started worker');
|
||||
// Start the application serving.
|
||||
startApp(program);
|
||||
|
||||
// Load in the serve.
|
||||
const serve = require('../serve');
|
||||
// Enable job processing on the thread if enabled.
|
||||
if (program.jobs) {
|
||||
|
||||
// Start serving.
|
||||
serve(program).catch(err => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
},
|
||||
});
|
||||
// Start the scraper processor.
|
||||
scraper.process();
|
||||
|
||||
// Start the mail processor.
|
||||
mailer.process();
|
||||
}
|
||||
|
||||
// Define a safe shutdown function to call in the event we need to shutdown
|
||||
// because the node hooks are below which will interrupt the shutdown process.
|
||||
// Shutdown the mongoose connection, the app server, and the scraper.
|
||||
util.onshutdown([
|
||||
() => program.jobs ? kue.Task.shutdown() : null,
|
||||
() => mongoose.disconnect(),
|
||||
() => server.close()
|
||||
]);
|
||||
|
||||
+12
-15
@@ -1,11 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const program = require('./commander');
|
||||
const inquirer = require('inquirer');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const Settings = require('../services/settings');
|
||||
const cache = require('../services/cache');
|
||||
const SettingsService = require('../services/settings');
|
||||
const util = require('./util');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
@@ -15,22 +14,20 @@ util.onshutdown([() => mongoose.disconnect()]);
|
||||
*/
|
||||
async function changeOrgName() {
|
||||
try {
|
||||
await cache.init();
|
||||
let settings = await SettingsService.retrieve();
|
||||
|
||||
// Get the original settings.
|
||||
const settings = await Settings.select('organizationName');
|
||||
|
||||
const { organizationName } = await inquirer.prompt([
|
||||
let {organizationName} = await inquirer.prompt([
|
||||
{
|
||||
name: 'organizationName',
|
||||
message: 'Organization Name',
|
||||
default: settings.organizationName,
|
||||
},
|
||||
default: settings.organizationName
|
||||
}
|
||||
]);
|
||||
|
||||
if (settings.organizationName !== organizationName) {
|
||||
// Set the organization name if there was a mutation to it.
|
||||
await Settings.update({ organizationName });
|
||||
settings.organizationName = organizationName;
|
||||
|
||||
await SettingsService.update(settings);
|
||||
|
||||
console.log('Settings were updated.');
|
||||
} else {
|
||||
@@ -39,9 +36,9 @@ async function changeOrgName() {
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
} finally {
|
||||
util.shutdown();
|
||||
}
|
||||
|
||||
util.shutdown();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
||||
+67
-102
@@ -4,8 +4,7 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const program = require('./commander');
|
||||
const inquirer = require('inquirer');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const SettingModel = require('../models/setting');
|
||||
@@ -13,12 +12,13 @@ const MODERATION_OPTIONS = require('../models/enum/moderation_options');
|
||||
const SettingsService = require('../services/settings');
|
||||
const SetupService = require('../services/setup');
|
||||
const UsersService = require('../services/users');
|
||||
const MigrationService = require('../services/migration');
|
||||
const { ErrSettingsInit, ErrSettingsNotInit } = require('../errors');
|
||||
const Context = require('../graph/context');
|
||||
const util = require('./util');
|
||||
const errors = require('../errors');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
@@ -34,32 +34,29 @@ program
|
||||
//==============================================================================
|
||||
|
||||
const performSetup = async () => {
|
||||
|
||||
// Get the current settings, we are expecing an error here.
|
||||
try {
|
||||
|
||||
// Try to get the settings.
|
||||
await SettingsService.retrieve();
|
||||
|
||||
// We should NOT have gotten a settings object, this means that the
|
||||
// application is already setup. Error out here.
|
||||
throw new ErrSettingsInit();
|
||||
} catch (err) {
|
||||
throw errors.ErrSettingsInit;
|
||||
|
||||
} catch (e) {
|
||||
|
||||
// If the error is `not init`, then we're good, otherwise, it's something
|
||||
// else.
|
||||
if (!err instanceof ErrSettingsNotInit) {
|
||||
throw err;
|
||||
return;
|
||||
if (e !== errors.ErrSettingsNotInit) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (program.defaults) {
|
||||
await SettingsService.init();
|
||||
|
||||
// Get the migrations to run.
|
||||
let migrations = await MigrationService.listPending();
|
||||
|
||||
// Perform all migrations.
|
||||
await MigrationService.run(migrations);
|
||||
|
||||
console.log('Settings created.');
|
||||
console.log('\nTalk is now installed!');
|
||||
|
||||
@@ -69,9 +66,7 @@ const performSetup = async () => {
|
||||
// Create the base settings model.
|
||||
let settings = new SettingModel();
|
||||
|
||||
console.log(
|
||||
"\nWe'll ask you some questions in order to setup your installation of Talk.\n"
|
||||
);
|
||||
console.log('\nWe\'ll ask you some questions in order to setup your installation of Talk.\n');
|
||||
|
||||
let answers = await inquirer.prompt([
|
||||
{
|
||||
@@ -79,144 +74,114 @@ const performSetup = async () => {
|
||||
name: 'organizationName',
|
||||
message: 'Organization Name',
|
||||
default: settings.organizationName,
|
||||
validate: input => {
|
||||
validate: (input) => {
|
||||
if (input && input.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return 'Organization Name is required.';
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
choices: MODERATION_OPTIONS,
|
||||
name: 'moderation',
|
||||
default: settings.moderation,
|
||||
message: 'Select a moderation mode',
|
||||
message: 'Select a moderation mode'
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'requireEmailConfirmation',
|
||||
default: settings.requireEmailConfirmation,
|
||||
message: 'Should emails always be confirmed',
|
||||
},
|
||||
message: 'Should emails always be confirmed'
|
||||
}
|
||||
]);
|
||||
|
||||
// Update the settings that were changed.
|
||||
Object.keys(answers).forEach(key => {
|
||||
Object.keys(answers).forEach((key) => {
|
||||
if (answers[key] !== undefined) {
|
||||
settings[key] = answers[key];
|
||||
}
|
||||
});
|
||||
|
||||
answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'inputWhitelistedDomains',
|
||||
default: true,
|
||||
message: 'Would you like to specify a whitelisted domain',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'whitelistedDomain',
|
||||
message: 'Whitelisted Domain',
|
||||
when: ({ inputWhitelistedDomains }) => inputWhitelistedDomains,
|
||||
validate: input => {
|
||||
if (input && input.length > 0) {
|
||||
return true;
|
||||
}
|
||||
console.log('\nWe\'ll ask you some questions about your first admin user.\n');
|
||||
|
||||
return 'Whitelisted Domain cannot be empty.';
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
if (answers.inputWhitelistedDomains) {
|
||||
settings.domains.whitelist = [answers.whitelistedDomain];
|
||||
}
|
||||
|
||||
console.log("\nWe'll ask you some questions about your first admin user.\n");
|
||||
|
||||
let { username, email } = await inquirer.prompt([
|
||||
let user = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'username',
|
||||
message: 'Username',
|
||||
filter: username => {
|
||||
return UsersService.isValidUsername(username, false).catch(err => {
|
||||
throw err.message;
|
||||
});
|
||||
},
|
||||
filter: (username) => {
|
||||
return UsersService
|
||||
.isValidUsername(username, false)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
message: 'Email',
|
||||
format: 'email',
|
||||
validate: value => {
|
||||
validate: (value) => {
|
||||
if (value && value.length >= 3) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return 'Email is required';
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
type: 'password',
|
||||
filter: (password) => {
|
||||
return UsersService
|
||||
.isValidPassword(password)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'confirmPassword',
|
||||
message: 'Confirm Password',
|
||||
type: 'password',
|
||||
filter: (confirmPassword) => {
|
||||
return UsersService
|
||||
.isValidPassword(confirmPassword)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
let password = '';
|
||||
while (!password) {
|
||||
answers = await inquirer.prompt([
|
||||
{
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
type: 'password',
|
||||
filter: password => {
|
||||
try {
|
||||
UsersService.isValidPassword(password);
|
||||
} catch (err) {
|
||||
throw err.message;
|
||||
}
|
||||
|
||||
return password;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'confirmPassword',
|
||||
message: 'Confirm Password',
|
||||
type: 'password',
|
||||
},
|
||||
]);
|
||||
|
||||
if (answers.password !== answers.confirmPassword) {
|
||||
console.error('Passwords do not match');
|
||||
} else {
|
||||
password = answers.password;
|
||||
}
|
||||
if (user.password !== user.confirmPassword) {
|
||||
return Promise.reject(new Error('Passwords do not match'));
|
||||
}
|
||||
|
||||
const ctx = Context.forSystem();
|
||||
let { user: newUser } = await SetupService.setup(ctx, {
|
||||
let {user: newUser} = await SetupService.setup({
|
||||
settings: settings.toObject(),
|
||||
user: {
|
||||
email,
|
||||
username,
|
||||
password,
|
||||
},
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
password: user.password
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
console.log('Settings created.');
|
||||
console.log(`User ${newUser.id} created.`);
|
||||
console.log('\nTalk is now installed!');
|
||||
console.log(
|
||||
'\nWe recommend adding TALK_INSTALL_LOCK=TRUE to your environment to turn off the dynamic setup.'
|
||||
);
|
||||
console.log('\nWe recommend adding TALK_INSTALL_LOCK=TRUE to your environment to turn off the dynamic setup.');
|
||||
};
|
||||
|
||||
// Start the setup process.
|
||||
// Start tthe setup process.
|
||||
performSetup()
|
||||
.then(() => {
|
||||
util.shutdown();
|
||||
})
|
||||
.catch(e => {
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
});
|
||||
|
||||
+20
-11
@@ -4,25 +4,35 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const program = require('./commander');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const TokensService = require('../services/tokens');
|
||||
const Table = require('cli-table2');
|
||||
const util = require('./util');
|
||||
const Table = require('cli-table');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
async function listTokens(userID) {
|
||||
try {
|
||||
let tokens = await TokensService.list(userID);
|
||||
|
||||
let table = new Table({
|
||||
head: ['ID', 'Name', 'Status'],
|
||||
head: [
|
||||
'ID',
|
||||
'Name',
|
||||
'Status'
|
||||
]
|
||||
});
|
||||
|
||||
tokens.forEach(token => {
|
||||
table.push([token.id, token.name, token.active ? 'Active' : 'Revoked']);
|
||||
tokens.forEach((token) => {
|
||||
table.push([
|
||||
token.id,
|
||||
token.name,
|
||||
token.active ? 'Active' : 'Revoked'
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
@@ -36,6 +46,7 @@ async function listTokens(userID) {
|
||||
|
||||
async function revokeToken(tokenID) {
|
||||
try {
|
||||
|
||||
await TokensService.revoke(null, tokenID);
|
||||
|
||||
console.log(`Revoked Token[${tokenID}]`);
|
||||
@@ -49,10 +60,8 @@ async function revokeToken(tokenID) {
|
||||
|
||||
async function createToken(userID, tokenName) {
|
||||
try {
|
||||
let {
|
||||
pat: { id },
|
||||
jwt,
|
||||
} = await TokensService.create(userID, tokenName);
|
||||
|
||||
let {pat: {id}, jwt} = await TokensService.create(userID, tokenName);
|
||||
|
||||
console.log(`Created Token[${id}] for User[${userID}] = ${jwt}`);
|
||||
|
||||
|
||||
+399
-263
@@ -4,180 +4,285 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const program = require('./commander');
|
||||
const inquirer = require('inquirer');
|
||||
const { stripIndent } = require('common-tags');
|
||||
const Table = require('cli-table2');
|
||||
|
||||
// Make things colorful!
|
||||
require('colors');
|
||||
|
||||
// Register the autocomplete plugin.
|
||||
inquirer.registerPrompt(
|
||||
'autocomplete',
|
||||
require('inquirer-autocomplete-prompt')
|
||||
);
|
||||
|
||||
const Context = require('../graph/context');
|
||||
const UsersService = require('../services/users');
|
||||
const UserModel = require('../models/user');
|
||||
const USER_ROLES = require('../models/enum/user_roles');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const util = require('./util');
|
||||
const Table = require('cli-table');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
|
||||
/**
|
||||
* Deletes a user and cleans up their associated verifications.
|
||||
*/
|
||||
async function deleteUser(userID) {
|
||||
try {
|
||||
// Find the user we're removing.
|
||||
const user = await UserModel.findOne({ id: userID });
|
||||
if (!user) {
|
||||
throw new Error(`user with id ${userID} not found`);
|
||||
}
|
||||
|
||||
printUserAsTable(user);
|
||||
|
||||
console.warn(stripIndent`
|
||||
|
||||
This will delete the above user.
|
||||
|
||||
This might take a long time if there is a lot of data, please confirm that
|
||||
you want to continue.
|
||||
`);
|
||||
const { confirm } = await inquirer.prompt({
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: 'Continue',
|
||||
default: false,
|
||||
});
|
||||
if (!confirm) {
|
||||
return util.shutdown();
|
||||
}
|
||||
|
||||
const ctx = Context.forSystem();
|
||||
|
||||
const { data, errors } = await ctx.graphql(
|
||||
`
|
||||
mutation DeleteUser($user_id: ID!) {
|
||||
delUser(id: $user_id) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ user_id: user.id }
|
||||
);
|
||||
if (errors) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
if (data.errors) {
|
||||
throw data.errors;
|
||||
}
|
||||
|
||||
console.log('User was deleted.');
|
||||
|
||||
util.shutdown();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
const validateRequired = (msg = 'Field is required', len = 1) => (input) => {
|
||||
if (input && input.length >= len) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function printUserAsTable(user) {
|
||||
let table = new Table({});
|
||||
return msg;
|
||||
};
|
||||
|
||||
table.push(
|
||||
{ ID: user.id.gray },
|
||||
{ Username: user.username },
|
||||
{ Emails: user.emails },
|
||||
{ Tags: user.tags ? user.tags.map(({ tag: { name } }) => name) : '' },
|
||||
{ Role: user.role },
|
||||
{ Verified: user.hasVerifiedEmail },
|
||||
{ Username: user.status.username.status },
|
||||
{ Banned: user.banned },
|
||||
// Regeister the shutdown criteria.
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
function getUserCreateAnswers(options) {
|
||||
if (options.flag_mode) {
|
||||
|
||||
let user = {
|
||||
email: options.email,
|
||||
password: options.password,
|
||||
confirmPassword: options.password,
|
||||
username: options.name,
|
||||
roles: []
|
||||
};
|
||||
|
||||
if (options.role && USER_ROLES.indexOf(options.role) > -1) {
|
||||
user.roles = [options.role];
|
||||
}
|
||||
|
||||
return Promise.resolve(user);
|
||||
}
|
||||
|
||||
return inquirer.prompt([
|
||||
{
|
||||
Suspension: user.suspended
|
||||
? `Until ${user.status.suspension.until}`
|
||||
: false,
|
||||
name: 'email',
|
||||
message: 'Email',
|
||||
format: 'email',
|
||||
validate: validateRequired('Email is required')
|
||||
},
|
||||
{ 'Always premod comments': user.alwaysPremod }
|
||||
);
|
||||
|
||||
console.log(table.toString());
|
||||
{
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
type: 'password',
|
||||
filter: (password) => {
|
||||
return UsersService
|
||||
.isValidPassword(password)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'confirmPassword',
|
||||
message: 'Confirm Password',
|
||||
type: 'password',
|
||||
filter: (confirmPassword) => {
|
||||
return UsersService
|
||||
.isValidPassword(confirmPassword)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'username',
|
||||
message: 'Username',
|
||||
filter: (username) => {
|
||||
return UsersService
|
||||
.isValidUsername(username)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'roles',
|
||||
message: 'User Role',
|
||||
type: 'checkbox',
|
||||
choices: USER_ROLES
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for users based on their username and email address.
|
||||
* Prompts for input and registers a user based on those.
|
||||
*/
|
||||
async function searchUsers() {
|
||||
const ctx = Context.forSystem();
|
||||
const searchQuery = `
|
||||
query SearchUsers($value: String) {
|
||||
users(query: {value: $value}) {
|
||||
nodes {
|
||||
id
|
||||
username
|
||||
role
|
||||
profiles {
|
||||
id
|
||||
provider
|
||||
}
|
||||
}
|
||||
function createUser(options) {
|
||||
getUserCreateAnswers(options)
|
||||
.then((answers) => {
|
||||
if (answers.password !== answers.confirmPassword) {
|
||||
return Promise.reject(new Error('Passwords do not match'));
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
const answers = await inquirer.prompt({
|
||||
type: 'autocomplete',
|
||||
name: 'userID',
|
||||
message: 'Search for a user',
|
||||
source: async (answers, value) => {
|
||||
if (value === null) {
|
||||
value = '';
|
||||
}
|
||||
return answers;
|
||||
})
|
||||
.then((answers) => {
|
||||
return UsersService
|
||||
.createLocalUser(answers.email.trim(), answers.password.trim(), answers.username.trim())
|
||||
.then((user) => {
|
||||
console.log(`Created user ${user.id}.`);
|
||||
|
||||
const { data, errors } = await ctx.graphql(searchQuery, {
|
||||
value,
|
||||
if (answers.roles.length > 0) {
|
||||
return Promise.all(answers.roles.map((role) => {
|
||||
return UsersService
|
||||
.addRoleToUser(user.id, role)
|
||||
.then(() => {
|
||||
console.log(`Added the role ${role} to User ${user.id}.`);
|
||||
});
|
||||
}));
|
||||
}
|
||||
});
|
||||
if (errors && errors.length > 0) {
|
||||
throw errors[0];
|
||||
}
|
||||
|
||||
if (data.users === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.users.nodes.map(user => {
|
||||
const emails = user.profiles
|
||||
.filter(({ provider }) => provider === 'local')
|
||||
.map(({ id }) => id)
|
||||
.join(', ');
|
||||
return {
|
||||
name: `${user.username} (${emails}) ${user.id.gray} - ${
|
||||
user.role.gray
|
||||
}`,
|
||||
value: user.id,
|
||||
};
|
||||
});
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown();
|
||||
});
|
||||
}
|
||||
|
||||
const { userID } = answers;
|
||||
const user = await UserModel.findOne({ id: userID });
|
||||
/**
|
||||
* Deletes a user.
|
||||
*/
|
||||
function deleteUser(userID) {
|
||||
UserModel
|
||||
.findOneAndRemove({
|
||||
id: userID
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Deleted user');
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown();
|
||||
});
|
||||
}
|
||||
|
||||
printUserAsTable(user);
|
||||
util.shutdown(0);
|
||||
} catch (err) {
|
||||
/**
|
||||
* Changes the password for a user.
|
||||
*/
|
||||
function passwd(userID) {
|
||||
inquirer.prompt([
|
||||
{
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
type: 'password',
|
||||
validate: validateRequired('Password is required')
|
||||
},
|
||||
{
|
||||
name: 'confirmPassword',
|
||||
message: 'Confirm Password',
|
||||
type: 'password',
|
||||
validate: validateRequired('Confirm Password is required')
|
||||
}
|
||||
])
|
||||
.then((answers) => {
|
||||
if (answers.password !== answers.confirmPassword) {
|
||||
return Promise.reject(new Error('Password mismatch'));
|
||||
}
|
||||
|
||||
return UsersService.changePassword(userID, answers.password);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Password changed.');
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the user from the options array.
|
||||
*/
|
||||
function updateUser(userID, options) {
|
||||
const updates = [];
|
||||
|
||||
if (options.email && typeof options.email === 'string' && options.email.length > 0) {
|
||||
let q = UserModel.update({
|
||||
'id': userID,
|
||||
'profiles.provider': 'local'
|
||||
}, {
|
||||
$set: {
|
||||
'profiles.$.id': options.email
|
||||
}
|
||||
});
|
||||
|
||||
updates.push(q);
|
||||
}
|
||||
|
||||
if (options.name && typeof options.name === 'string' && options.name.length > 0) {
|
||||
let q = UserModel.update({
|
||||
'id': userID
|
||||
}, {
|
||||
$set: {
|
||||
username: options.name
|
||||
}
|
||||
});
|
||||
|
||||
updates.push(q);
|
||||
}
|
||||
|
||||
Promise
|
||||
.all(updates.map((q) => q.exec()))
|
||||
.then(() => {
|
||||
console.log(`User ${userID} updated.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all the users registered in the database.
|
||||
*/
|
||||
function listUsers() {
|
||||
UsersService
|
||||
.all()
|
||||
.then((users) => {
|
||||
let table = new Table({
|
||||
head: [
|
||||
'ID',
|
||||
'Username',
|
||||
'Profiles',
|
||||
'Roles',
|
||||
'Status',
|
||||
'State'
|
||||
]
|
||||
});
|
||||
|
||||
users.forEach((user) => {
|
||||
table.push([
|
||||
user.id,
|
||||
user.username,
|
||||
user.profiles.map((p) => p.provider).join(', '),
|
||||
user.roles.join(', '),
|
||||
user.status,
|
||||
user.disabled ? 'Disabled' : 'Enabled'
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges two users using the specified ID's.
|
||||
* @param {String} dstUserID id of the user to which is the target of the merge
|
||||
* @param {String} srcUserID id of the user to which is the source of the merge
|
||||
*/
|
||||
function mergeUsers(dstUserID, srcUserID) {
|
||||
UsersService
|
||||
.mergeUsers(dstUserID, srcUserID)
|
||||
.then(() => {
|
||||
console.log(`User ${srcUserID} was merged into user ${dstUserID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,128 +290,117 @@ async function searchUsers() {
|
||||
* @param {String} userUD id of the user to add the role to
|
||||
* @param {String} role the role to add
|
||||
*/
|
||||
async function setUserRole(userID) {
|
||||
try {
|
||||
const { role } = await inquirer.prompt([
|
||||
{
|
||||
name: 'role',
|
||||
message: 'User Role',
|
||||
type: 'list',
|
||||
choices: USER_ROLES,
|
||||
},
|
||||
]);
|
||||
function addRole(userID, role) {
|
||||
|
||||
await UsersService.setRole(userID, role);
|
||||
|
||||
console.log(`Set User ${userID} to the ${role} role.`);
|
||||
util.shutdown();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (USER_ROLES.indexOf(role) === -1) {
|
||||
console.error(`Role '${role}' is not supported. Supported roles are ${USER_ROLES.join(', ')}.`);
|
||||
util.shutdown(1);
|
||||
return;
|
||||
}
|
||||
|
||||
UsersService
|
||||
.addRoleToUser(userID, role)
|
||||
.then(() => {
|
||||
console.log(`Added the ${role} role to User ${userID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies an email address for a user.
|
||||
*
|
||||
* @param userID the user's id
|
||||
* @param email the user's email address to be verified, otherwise verifies the
|
||||
* first email if there is one, if there are multiple, you get a
|
||||
* prompt.
|
||||
* Removes a role from a user
|
||||
* @param {String} userUD id of the user to remove the role from
|
||||
* @param {String} role the role to remove
|
||||
*/
|
||||
async function verifyUserEmail(userID, email) {
|
||||
try {
|
||||
// Get the user.
|
||||
const user = await UserModel.findOne({ id: userID });
|
||||
if (!user) {
|
||||
throw new Error(`user with ID ${userID} cannot be found`);
|
||||
}
|
||||
function removeRole(userID, role) {
|
||||
|
||||
// Get all the user's email addresses.
|
||||
const emails = user.emails;
|
||||
if (emails.length === 0) {
|
||||
throw new Error('user did not have any email addresses');
|
||||
}
|
||||
|
||||
if (!email && emails.length === 1) {
|
||||
// The email wasn't passed, and there is only one option.
|
||||
email = emails[0];
|
||||
} else if (!emails.includes(email)) {
|
||||
// The email passed doesn't belong to this user.
|
||||
throw new Error(`user does not have the email ${email}`);
|
||||
} else if (emails.length > 1) {
|
||||
// The email wasn't passed, and there is more than one choice.
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
name: 'email',
|
||||
message: 'Select Email to Verify',
|
||||
type: 'list',
|
||||
choices: emails,
|
||||
},
|
||||
]);
|
||||
|
||||
email = answers.email;
|
||||
}
|
||||
|
||||
// Verify the email.
|
||||
await UsersService.confirmEmail(userID, email);
|
||||
console.log(`User ${userID} had their email ${email} verified.`);
|
||||
util.shutdown();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (USER_ROLES.indexOf(role) === -1) {
|
||||
console.error(`Role '${role}' is not supported. Supported roles are ${USER_ROLES.join(', ')}.`);
|
||||
util.shutdown(1);
|
||||
return;
|
||||
}
|
||||
|
||||
UsersService
|
||||
.removeRoleFromUser(userID, role)
|
||||
.then(() => {
|
||||
console.log(`Removed the ${role} role from User ${userID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* createUser will prompt the user for the user information when creating a
|
||||
* local user.
|
||||
* Ban a user
|
||||
* @param {String} userID id of the user to ban
|
||||
*/
|
||||
async function createUser() {
|
||||
try {
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
name: 'email',
|
||||
message: 'Email',
|
||||
},
|
||||
{
|
||||
name: 'username',
|
||||
message: 'Username',
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
type: 'password',
|
||||
},
|
||||
{
|
||||
name: 'role',
|
||||
message: 'Role',
|
||||
type: 'list',
|
||||
choices: USER_ROLES,
|
||||
},
|
||||
]);
|
||||
function ban(userID) {
|
||||
UsersService
|
||||
.setStatus(userID, 'BANNED')
|
||||
.then(() => {
|
||||
console.log(`Banned the User ${userID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
const { email, username, password, role } = answers;
|
||||
/**
|
||||
* Unban a user
|
||||
* @param {String} userUD id of the user to remove the role from
|
||||
*/
|
||||
function unban(userID) {
|
||||
UsersService
|
||||
.setStatus(userID, 'ACTIVE')
|
||||
.then(() => {
|
||||
console.log(`Unban the User ${userID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
const ctx = Context.forSystem();
|
||||
/**
|
||||
* Disable a given user.
|
||||
* @param {String} userID the ID of a user to disable
|
||||
*/
|
||||
function disableUser(userID) {
|
||||
UsersService
|
||||
.disableUser(userID)
|
||||
.then(() => {
|
||||
console.log(`User ${userID} was disabled.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
// Create the user.
|
||||
const user = await UsersService.createLocalUser(
|
||||
ctx,
|
||||
email,
|
||||
password,
|
||||
username
|
||||
);
|
||||
|
||||
// Set the role.
|
||||
await UsersService.setRole(user.id, role);
|
||||
|
||||
console.log(`Created User[${user.id}]`);
|
||||
util.shutdown(0);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
}
|
||||
/**
|
||||
* Enabled a given user.
|
||||
* @param {String} userID the ID of a user to enable
|
||||
*/
|
||||
function enableUser(userID) {
|
||||
UsersService
|
||||
.enableUser(userID)
|
||||
.then(() => {
|
||||
console.log(`User ${userID} was enabled.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
@@ -315,7 +409,12 @@ async function createUser() {
|
||||
|
||||
program
|
||||
.command('create')
|
||||
.description('creates a local user')
|
||||
.option('--email [email]', 'Email to use')
|
||||
.option('--password [password]', 'Password to use')
|
||||
.option('--name [name]', 'Name to use')
|
||||
.option('--role [role]', 'Role to add')
|
||||
.option('-f, --flag_mode', 'Source from flags instead of prompting')
|
||||
.description('create a new user')
|
||||
.action(createUser);
|
||||
|
||||
program
|
||||
@@ -323,20 +422,57 @@ program
|
||||
.description('delete a user')
|
||||
.action(deleteUser);
|
||||
|
||||
program
|
||||
.command('passwd <userID>')
|
||||
.description('change a password for a user')
|
||||
.action(passwd);
|
||||
|
||||
program
|
||||
.command('update <userID>')
|
||||
.option('--email [email]', 'Email to use')
|
||||
.option('--name [name]', 'Name to use')
|
||||
.description('update a user')
|
||||
.action(updateUser);
|
||||
|
||||
program
|
||||
.command('list')
|
||||
.description('searches for a user based on their stored username and email')
|
||||
.action(searchUsers);
|
||||
.description('list all the users in the database')
|
||||
.action(listUsers);
|
||||
|
||||
program
|
||||
.command('set-role <userID>')
|
||||
.description('sets the role on a user')
|
||||
.action(setUserRole);
|
||||
.command('merge <dstUserID> <srcUserID>')
|
||||
.description('merge srcUser into the dstUser')
|
||||
.action(mergeUsers);
|
||||
|
||||
program
|
||||
.command('verify <userID> <email>')
|
||||
.description("verifies the given user's email address")
|
||||
.action(verifyUserEmail);
|
||||
.command('addrole <userID> <role>')
|
||||
.description('adds a role to a given user')
|
||||
.action(addRole);
|
||||
|
||||
program
|
||||
.command('removerole <userID> <role>')
|
||||
.description('removes a role from a given user')
|
||||
.action(removeRole);
|
||||
|
||||
program
|
||||
.command('ban <userID>')
|
||||
.description('ban a given user')
|
||||
.action(ban);
|
||||
|
||||
program
|
||||
.command('uban <userID>')
|
||||
.description('unban a given user')
|
||||
.action(unban);
|
||||
|
||||
program
|
||||
.command('disable <userID>')
|
||||
.description('disable a given user from logging in')
|
||||
.action(disableUser);
|
||||
|
||||
program
|
||||
.command('enable <userID>')
|
||||
.description('enable a given user from logging in')
|
||||
.action(enableUser);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
const pkg = require('../package.json');
|
||||
const dotenv = require('dotenv');
|
||||
const fs = require('fs');
|
||||
const program = require('commander');
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
const parseArgs = require('minimist')(process.argv.slice(2), {
|
||||
alias: {
|
||||
'c': 'config'
|
||||
},
|
||||
string: [
|
||||
'config',
|
||||
'pid'
|
||||
],
|
||||
default: {
|
||||
'config': null,
|
||||
'pid': null
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* If the config flag is present, then we have to load the configuration from
|
||||
* the file specified. We will then load those values into the environment.
|
||||
*/
|
||||
if (parseArgs.config) {
|
||||
let envConfig = dotenv.parse(fs.readFileSync(parseArgs.config, {encoding: 'utf8'}));
|
||||
|
||||
Object.keys(envConfig).forEach((k) => {
|
||||
process.env[k] = envConfig[k];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* If the pid flag is present, then we have to create a pid file at the location
|
||||
* specified.
|
||||
*/
|
||||
if (parseArgs.pid) {
|
||||
const util = require('./util');
|
||||
|
||||
console.log('Wrote PID');
|
||||
|
||||
util.pid(parseArgs.pid);
|
||||
}
|
||||
|
||||
module.exports = program
|
||||
.version(pkg.version)
|
||||
.option('-c, --config [path]', 'Specify the configuration file to load')
|
||||
.option('--pid [path]', 'Specify a path to output the current PID to');
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"presets": [
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"add-module-exports",
|
||||
"transform-class-properties",
|
||||
"transform-decorators-legacy",
|
||||
"transform-object-assign",
|
||||
"transform-object-rest-spread",
|
||||
"transform-async-to-generator",
|
||||
"transform-react-jsx"
|
||||
]
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
import React from 'react';
|
||||
import { CoralLogo } from 'plugin-api/beta/client/components/ui';
|
||||
import {CoralLogo} from 'plugin-api/beta/client/components/ui';
|
||||
import styles from './MyPluginComponent.css';
|
||||
|
||||
class MyPluginComponent extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.myPluginContainer}>
|
||||
<CoralLogo className={styles.logo} />
|
||||
<CoralLogo className={styles.logo}/>
|
||||
<div className={styles.description}>
|
||||
<h3>Plugin created by Talk CLI</h3>
|
||||
|
||||
<small>
|
||||
To read more about plugins check{' '}
|
||||
<a href="https://docs.coralproject.net/talk/plugins-client">
|
||||
<a href="https://coralproject.github.io/talk/plugins-client.html">
|
||||
our docs and guides!
|
||||
</a>
|
||||
</small>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
/**
|
||||
This is a client index example file and it could look like this:
|
||||
|
||||
@@ -13,13 +14,13 @@
|
||||
};
|
||||
```
|
||||
|
||||
To read more info on how to build client plugins. Please, go to: https://docs.coralproject.net/talk/plugins-client
|
||||
To read more info on how to build client plugins. Please, go to: https://coralproject.github.io/talk/plugins-client.html
|
||||
*/
|
||||
|
||||
import MyPluginComponent from './components/MyPluginComponent';
|
||||
|
||||
export default {
|
||||
slots: {
|
||||
stream: [MyPluginComponent],
|
||||
},
|
||||
stream: [MyPluginComponent]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
#
|
||||
# ```
|
||||
# en:
|
||||
# talk-plugin-respect:
|
||||
# coral-plugin-respect:
|
||||
# respect: Respect
|
||||
# respected: Respected
|
||||
# es:
|
||||
# talk-plugin-respect:
|
||||
# coral-plugin-respect:
|
||||
# respect: Respetar
|
||||
# respected: Respetado
|
||||
# ```
|
||||
|
||||
+49
-23
@@ -1,11 +1,7 @@
|
||||
// Setup the environment.
|
||||
require('../services/env');
|
||||
|
||||
const debug = require('debug')('talk:util');
|
||||
const { uniq } = require('lodash');
|
||||
const fs = require('fs');
|
||||
|
||||
// Setup the utilities.
|
||||
const util = {};
|
||||
const util = module.exports = {};
|
||||
|
||||
/**
|
||||
* Stores an array of functions that should be executed in the event that the
|
||||
@@ -16,21 +12,23 @@ util.toshutdown = [];
|
||||
|
||||
/**
|
||||
* Calls all the shutdown functions and then ends the process.
|
||||
* @param {Number} [defaultCode=0] default return code upon successful shutdown.
|
||||
* @param {Number} [defaultCode=0] default return code upon sucesfull shutdown.
|
||||
*/
|
||||
util.shutdown = (defaultCode = 0, signal = null) => {
|
||||
|
||||
if (signal) {
|
||||
debug(`Reached ${signal} signal`);
|
||||
}
|
||||
|
||||
debug(`${util.toshutdown.length} jobs now being called`);
|
||||
|
||||
Promise.all(util.toshutdown.map(func => (func ? func(signal) : null)))
|
||||
Promise
|
||||
.all(util.toshutdown.map((func) => func ? func(signal) : null).filter((func) => func))
|
||||
.then(() => {
|
||||
debug('Shutdown complete, now exiting');
|
||||
process.exit(defaultCode);
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
|
||||
process.exit(1);
|
||||
@@ -43,26 +41,54 @@ util.shutdown = (defaultCode = 0, signal = null) => {
|
||||
* @param {Array} jobs Array of promise capable shutdown functions that are
|
||||
* executed.
|
||||
*/
|
||||
util.onshutdown = jobs => {
|
||||
util.onshutdown = (jobs) => {
|
||||
|
||||
debug(`${jobs.length} jobs registered to be called during shutdown`);
|
||||
|
||||
// Add the new jobs to shutdown to the object reference.
|
||||
util.toshutdown = uniq(util.toshutdown.concat(jobs));
|
||||
util.toshutdown = util.toshutdown.concat(jobs);
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a PID file to be maintained for the lifespan of the process.
|
||||
* @param {String} path path to the PID file to create
|
||||
*/
|
||||
util.pid = (path) => {
|
||||
if (!/\//.test(path)) {
|
||||
if (!/\.pid/.test(path)) {
|
||||
path += '.pid';
|
||||
}
|
||||
path = `/tmp/${path}`;
|
||||
}
|
||||
|
||||
const pid = `${process.pid.toString()}\n`;
|
||||
|
||||
fs.writeFile(path, pid, (err) => {
|
||||
if (err) {
|
||||
console.error(`Can't write PID file: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Add the cleanup for the fs onto the shutdown.
|
||||
util.onshutdown([
|
||||
() => new Promise((resolve, reject) => {
|
||||
|
||||
// Remove the pid file.
|
||||
fs.unlink(path, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve();
|
||||
});
|
||||
})
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
// Attach to the SIGTERM + SIGINT handles to ensure a clean shutdown in the
|
||||
// event that we have an external event. SIGUSR2 is called when the app is asked
|
||||
// to be 'killed', same procedure here.
|
||||
process.once('SIGTERM', () => util.shutdown(0, 'SIGTERM'));
|
||||
process.once('SIGINT', () => util.shutdown(0, 'SIGINT'));
|
||||
process.on('SIGTERM', () => util.shutdown(0, 'SIGTERM'));
|
||||
process.on('SIGINT', () => util.shutdown(0, 'SIGINT'));
|
||||
process.once('SIGUSR2', () => util.shutdown(0, 'SIGUSR2'));
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on('unhandledRejection', err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
module.exports = util;
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
machine:
|
||||
node:
|
||||
version: 7.10.1
|
||||
services:
|
||||
- docker
|
||||
- redis
|
||||
environment:
|
||||
PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin"
|
||||
NODE_ENV: "test"
|
||||
|
||||
dependencies:
|
||||
override:
|
||||
# TODO: use the following to add in support for MongoDB 3.4.
|
||||
# # Upgrade the database version to 3.4.
|
||||
# - sudo apt-get purge mongodb-org*
|
||||
# - sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 0C49F3730359A14518585931BC711F9BA15703C6
|
||||
# - echo "deb [ arch=amd64 ] http://repo.mongodb.org/apt/ubuntu precise/mongodb-org/3.4 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.4.list
|
||||
# - sudo apt-get update
|
||||
# - sudo apt-get install -y mongodb-org
|
||||
# - sudo service mongod restart
|
||||
|
||||
# Install node dependencies.
|
||||
- yarn --version
|
||||
- yarn
|
||||
cache_directories:
|
||||
- ~/.cache/yarn
|
||||
post:
|
||||
# Build the static assets.
|
||||
- yarn build
|
||||
# Lint the project here, before tests are ran.
|
||||
- yarn lint
|
||||
|
||||
database:
|
||||
post:
|
||||
# Initialize the settings in the database, this will create indicies for the
|
||||
# database.
|
||||
- ./bin/cli setup --defaults
|
||||
- sleep 2
|
||||
|
||||
test:
|
||||
override:
|
||||
# Run the tests using the junit reporter.
|
||||
- MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml MOCHA_REPORTER=mocha-junit-reporter yarn test
|
||||
# Run the e2e test suite.
|
||||
# - E2E_REPORT_PATH=$CIRCLE_TEST_REPORTS/e2e yarn e2e
|
||||
|
||||
deployment:
|
||||
release:
|
||||
tag: /v[0-9]+(\.[0-9]+)*/
|
||||
commands:
|
||||
- bash ./scripts/docker.sh deploy
|
||||
|
||||
latest:
|
||||
branch: master
|
||||
owner: coralproject
|
||||
commands:
|
||||
- bash ./scripts/docker.sh deploy
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"presets": [
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"add-module-exports",
|
||||
"transform-class-properties",
|
||||
"transform-decorators-legacy",
|
||||
"transform-object-assign",
|
||||
"transform-object-rest-spread",
|
||||
"transform-async-to-generator",
|
||||
"transform-react-jsx"
|
||||
]
|
||||
}
|
||||
+21
-1
@@ -1,3 +1,23 @@
|
||||
{
|
||||
"extends": "@coralproject/eslint-config-talk/client"
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +1,61 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Router, Route, IndexRedirect, IndexRoute } from 'react-router';
|
||||
|
||||
import Install from 'routes/Install';
|
||||
import Stories from 'routes/Stories';
|
||||
import Community from 'routes/Community';
|
||||
import {Router, Route, IndexRedirect, browserHistory, Redirect} from 'react-router';
|
||||
|
||||
import Configure from 'routes/Configure';
|
||||
import StreamSettings from './routes/Configure/containers/StreamSettings';
|
||||
import ModerationSettings from './routes/Configure/containers/ModerationSettings';
|
||||
import TechSettings from './routes/Configure/containers/TechSettings';
|
||||
import OrganizationSettings from './routes/Configure/containers/OrganizationSettings';
|
||||
|
||||
import { ModerationLayout, Moderation } from 'routes/Moderation';
|
||||
import Dashboard from 'routes/Dashboard';
|
||||
import Install from 'routes/Install';
|
||||
import Stories from 'routes/Stories';
|
||||
import {CommunityLayout, Community} from 'routes/Community';
|
||||
import {ModerationLayout, Moderation} from 'routes/Moderation';
|
||||
|
||||
import Layout from 'containers/Layout';
|
||||
|
||||
const routes = (
|
||||
<div>
|
||||
<Route exact path="/admin/install" component={Install} />
|
||||
<Route path="/admin" component={Layout}>
|
||||
<IndexRedirect to="/admin/moderate" />
|
||||
|
||||
<Route path="configure" component={Configure}>
|
||||
<Route path="stream" component={StreamSettings} />
|
||||
<Route path="moderation" component={ModerationSettings} />
|
||||
<Route path="tech" component={TechSettings} />
|
||||
<Route path="organization" component={OrganizationSettings} />
|
||||
<IndexRedirect to="stream" />
|
||||
</Route>
|
||||
|
||||
<Route path="stories" component={Stories} />
|
||||
<Route exact path="/admin/install" component={Install}/>
|
||||
<Route path='/admin' component={Layout}>
|
||||
<IndexRedirect to='/admin/moderate/all' />
|
||||
<Route path='configure' component={Configure} />
|
||||
<Route path='stories' component={Stories} />
|
||||
<Route path='dashboard' component={Dashboard} />
|
||||
|
||||
{/* Community Routes */}
|
||||
|
||||
<Route path="community">
|
||||
<Route path="flagged" components={Community}>
|
||||
<Route path=":id" components={Community} />
|
||||
<Route path='community' component={CommunityLayout}>
|
||||
<Route path='flagged' components={Community}>
|
||||
<Route path=':id' components={Community} />
|
||||
</Route>
|
||||
<Route path="people" components={Community}>
|
||||
<Route path=":id" components={Community} />
|
||||
<Route path='people' components={Community}>
|
||||
<Route path=':id' components={Community} />
|
||||
</Route>
|
||||
<IndexRedirect to="flagged" />
|
||||
<IndexRedirect to='flagged' />
|
||||
</Route>
|
||||
|
||||
{/* Moderation Routes */}
|
||||
|
||||
<Route path="moderate" component={ModerationLayout}>
|
||||
<IndexRoute components={Moderation} />
|
||||
|
||||
<Route path=":tabOrId" components={Moderation} />
|
||||
|
||||
<Route path=":tab" components={Moderation}>
|
||||
<Route path=":id" components={Moderation} />
|
||||
<Route path='moderate' component={ModerationLayout}>
|
||||
<Route path='all' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
<Route path='accepted' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
<Route path='premod' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
<Route path='rejected' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
<Route path='flagged' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
<Redirect from=':id' to='all/:id' />
|
||||
<IndexRedirect to='all' />
|
||||
</Route>
|
||||
</Route>
|
||||
</div>
|
||||
);
|
||||
|
||||
class AppRouter extends React.Component {
|
||||
static contextTypes = {
|
||||
history: PropTypes.object,
|
||||
};
|
||||
|
||||
render() {
|
||||
return <Router history={this.context.history} routes={routes} />;
|
||||
}
|
||||
}
|
||||
const AppRouter = () => <Router history={browserHistory} routes={routes} />;
|
||||
|
||||
export default AppRouter;
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import {
|
||||
SHOW_ALWAYS_PREMOD_USER_DIALOG,
|
||||
HIDE_ALWAYS_PREMOD_USER_DIALOG,
|
||||
} from '../constants/alwaysPremodUserDialog.js';
|
||||
|
||||
export const showAlwaysPremodUserDialog = ({ userId, username }) => ({
|
||||
type: SHOW_ALWAYS_PREMOD_USER_DIALOG,
|
||||
userId,
|
||||
username,
|
||||
});
|
||||
|
||||
export const hideAlwaysPremodUserDialog = () => ({
|
||||
type: HIDE_ALWAYS_PREMOD_USER_DIALOG,
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
FETCH_ASSETS_REQUEST,
|
||||
FETCH_ASSETS_SUCCESS,
|
||||
FETCH_ASSETS_FAILURE,
|
||||
UPDATE_ASSET_STATE_REQUEST,
|
||||
UPDATE_ASSET_STATE_SUCCESS,
|
||||
UPDATE_ASSET_STATE_FAILURE,
|
||||
UPDATE_ASSETS
|
||||
} from '../constants/assets';
|
||||
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
/**
|
||||
* Action disptacher related to assets
|
||||
*/
|
||||
|
||||
// Fetch a page of assets
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch) => {
|
||||
dispatch({type: FETCH_ASSETS_REQUEST});
|
||||
return coralApi(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`)
|
||||
.then(({result, count}) =>
|
||||
dispatch({type: FETCH_ASSETS_SUCCESS,
|
||||
assets: result,
|
||||
count
|
||||
}))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: FETCH_ASSETS_FAILURE, error: errorMessage});
|
||||
});
|
||||
};
|
||||
|
||||
// Update an asset state
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
export const updateAssetState = (id, closedAt) => (dispatch) => {
|
||||
dispatch({type: UPDATE_ASSET_STATE_REQUEST});
|
||||
return coralApi(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}})
|
||||
.then(() => dispatch({type: UPDATE_ASSET_STATE_SUCCESS}))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: UPDATE_ASSET_STATE_FAILURE, error: errorMessage});
|
||||
});
|
||||
};
|
||||
|
||||
export const updateAssets = (assets) => (dispatch) => {
|
||||
dispatch({type: UPDATE_ASSETS, assets});
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
import bowser from 'bowser';
|
||||
import * as actions from '../constants/auth';
|
||||
import coralApi from 'coral-framework/helpers/request';
|
||||
import * as Storage from 'coral-framework/helpers/storage';
|
||||
import {handleAuthToken} from 'coral-framework/actions/auth';
|
||||
import {resetWebsocket} from 'coral-framework/services/client';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
//==============================================================================
|
||||
// SIGN IN
|
||||
//==============================================================================
|
||||
|
||||
export const handleLogin = (email, password, recaptchaResponse) => (dispatch) => {
|
||||
dispatch({type: actions.LOGIN_REQUEST});
|
||||
|
||||
const params = {
|
||||
method: 'POST',
|
||||
body: {
|
||||
email,
|
||||
password
|
||||
}
|
||||
};
|
||||
|
||||
if (recaptchaResponse) {
|
||||
params.headers = {
|
||||
'X-Recaptcha-Response': recaptchaResponse
|
||||
};
|
||||
}
|
||||
|
||||
return coralApi('/auth/local', params)
|
||||
.then(({user, token}) => {
|
||||
|
||||
if (!user) {
|
||||
if (!bowser.safari && !bowser.ios) {
|
||||
Storage.removeItem('token');
|
||||
}
|
||||
return dispatch(checkLoginFailure('not logged in'));
|
||||
}
|
||||
|
||||
dispatch(handleAuthToken(token));
|
||||
resetWebsocket();
|
||||
dispatch(checkLoginSuccess(user));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
|
||||
if (error.translation_key === 'NOT_AUTHORIZED') {
|
||||
|
||||
// invalid credentials
|
||||
dispatch({
|
||||
type: actions.LOGIN_FAILURE,
|
||||
message: t('error.email_password')
|
||||
});
|
||||
}
|
||||
else if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
|
||||
dispatch({
|
||||
type: actions.LOGIN_MAXIMUM_EXCEEDED,
|
||||
message: t(`error.${error.translation_key}`),
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: actions.LOGIN_FAILURE,
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// FORGOT PASSWORD
|
||||
//==============================================================================
|
||||
|
||||
const forgotPasswordRequest = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_REQUEST
|
||||
});
|
||||
|
||||
const forgotPasswordSuccess = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS
|
||||
});
|
||||
|
||||
const forgotPasswordFailure = (error) => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const requestPasswordReset = (email) => (dispatch) => {
|
||||
dispatch(forgotPasswordRequest(email));
|
||||
const redirectUri = location.href;
|
||||
|
||||
return coralApi('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}})
|
||||
.then(() => dispatch(forgotPasswordSuccess()))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(forgotPasswordFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// CHECK LOGIN
|
||||
//==============================================================================
|
||||
|
||||
const checkLoginRequest = () => ({
|
||||
type: actions.CHECK_LOGIN_REQUEST
|
||||
});
|
||||
|
||||
const checkLoginSuccess = (user, isAdmin) => ({
|
||||
type: actions.CHECK_LOGIN_SUCCESS,
|
||||
user,
|
||||
isAdmin
|
||||
});
|
||||
|
||||
const checkLoginFailure = (error) => ({
|
||||
type: actions.CHECK_LOGIN_FAILURE,
|
||||
error
|
||||
});
|
||||
|
||||
export const checkLogin = () => (dispatch) => {
|
||||
dispatch(checkLoginRequest());
|
||||
return coralApi('/auth')
|
||||
.then(({user}) => {
|
||||
if (!user) {
|
||||
if (!bowser.safari && !bowser.ios) {
|
||||
Storage.removeItem('token');
|
||||
}
|
||||
return dispatch(checkLoginFailure('not logged in'));
|
||||
}
|
||||
|
||||
resetWebsocket();
|
||||
dispatch(checkLoginSuccess(user));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(checkLoginFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
@@ -1,19 +1,7 @@
|
||||
import {
|
||||
SHOW_BAN_USER_DIALOG,
|
||||
HIDE_BAN_USER_DIALOG,
|
||||
} from '../constants/banUserDialog';
|
||||
import {SHOW_BAN_USER_DIALOG, HIDE_BAN_USER_DIALOG} from '../constants/banUserDialog';
|
||||
|
||||
export const showBanUserDialog = ({
|
||||
userId,
|
||||
username,
|
||||
commentId,
|
||||
commentStatus,
|
||||
}) => ({
|
||||
type: SHOW_BAN_USER_DIALOG,
|
||||
userId,
|
||||
username,
|
||||
commentId,
|
||||
commentStatus,
|
||||
});
|
||||
export const showBanUserDialog = ({userId, username, commentId, commentStatus}) =>
|
||||
({type: SHOW_BAN_USER_DIALOG, userId, username, commentId, commentStatus});
|
||||
|
||||
export const hideBanUserDialog = () => ({type: HIDE_BAN_USER_DIALOG});
|
||||
|
||||
export const hideBanUserDialog = () => ({ type: HIDE_BAN_USER_DIALOG });
|
||||
|
||||
@@ -1,88 +1,74 @@
|
||||
import queryString from 'querystringify';
|
||||
import qs from 'qs';
|
||||
|
||||
import {
|
||||
FETCH_USERS_REQUEST,
|
||||
FETCH_USERS_SUCCESS,
|
||||
FETCH_USERS_FAILURE,
|
||||
FETCH_COMMENTERS_REQUEST,
|
||||
FETCH_COMMENTERS_SUCCESS,
|
||||
FETCH_COMMENTERS_FAILURE,
|
||||
SORT_UPDATE,
|
||||
SET_PAGE,
|
||||
SET_SEARCH_VALUE,
|
||||
COMMENTERS_NEW_PAGE,
|
||||
SET_ROLE,
|
||||
SET_COMMENTER_STATUS,
|
||||
SHOW_BANUSER_DIALOG,
|
||||
HIDE_BANUSER_DIALOG,
|
||||
SHOW_ALWAYS_PREMOD_USER_DIALOG,
|
||||
HIDE_ALWAYS_PREMOD_USER_DIALOG,
|
||||
SHOW_REJECT_USERNAME_DIALOG,
|
||||
HIDE_REJECT_USERNAME_DIALOG,
|
||||
SET_INDICATOR_TRACK,
|
||||
HIDE_REJECT_USERNAME_DIALOG
|
||||
} from '../constants/community';
|
||||
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const fetchUsers = (query = {}) => (dispatch, _, { rest }) => {
|
||||
dispatch(requestFetchUsers());
|
||||
rest(`/users?${queryString.stringify(query)}`)
|
||||
.then(({ result, page, count, limit, totalPages }) => {
|
||||
export const fetchAccounts = (query = {}) => (dispatch) => {
|
||||
|
||||
dispatch(requestFetchAccounts());
|
||||
coralApi(`/users?${qs.stringify(query)}`)
|
||||
.then(({result, page, count, limit, totalPages}) =>{
|
||||
dispatch({
|
||||
type: FETCH_USERS_SUCCESS,
|
||||
users: result,
|
||||
type: FETCH_COMMENTERS_SUCCESS,
|
||||
accounts: result,
|
||||
page,
|
||||
count,
|
||||
limit,
|
||||
totalPages,
|
||||
totalPages
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch({ type: FETCH_USERS_FAILURE, error: errorMessage });
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: FETCH_COMMENTERS_FAILURE, error: errorMessage});
|
||||
});
|
||||
};
|
||||
|
||||
const requestFetchUsers = () => ({
|
||||
type: FETCH_USERS_REQUEST,
|
||||
const requestFetchAccounts = () => ({
|
||||
type: FETCH_COMMENTERS_REQUEST
|
||||
});
|
||||
|
||||
export const updateSorting = sort => ({
|
||||
export const updateSorting = (sort) => ({
|
||||
type: SORT_UPDATE,
|
||||
sort,
|
||||
sort
|
||||
});
|
||||
|
||||
export const setPage = page => ({
|
||||
type: SET_PAGE,
|
||||
page,
|
||||
export const newPage = () => ({
|
||||
type: COMMENTERS_NEW_PAGE
|
||||
});
|
||||
|
||||
export const setSearchValue = value => ({
|
||||
type: SET_SEARCH_VALUE,
|
||||
value,
|
||||
});
|
||||
export const setRole = (id, role) => (dispatch) => {
|
||||
return coralApi(`/users/${id}/role`, {method: 'POST', body: {role}})
|
||||
.then(() => {
|
||||
return dispatch({type: SET_ROLE, id, role});
|
||||
});
|
||||
};
|
||||
|
||||
export const setCommenterStatus = (id, status) => (dispatch) => {
|
||||
return coralApi(`/users/${id}/status`, {method: 'POST', body: {status}})
|
||||
.then(() => {
|
||||
return dispatch({type: SET_COMMENTER_STATUS, id, status});
|
||||
});
|
||||
};
|
||||
|
||||
// Ban User Dialog
|
||||
export const showBanUserDialog = user => ({ type: SHOW_BANUSER_DIALOG, user });
|
||||
export const hideBanUserDialog = () => ({ type: HIDE_BANUSER_DIALOG });
|
||||
|
||||
// Always premod User Dialog
|
||||
export const showAlwaysPremodUserDialog = user => ({
|
||||
type: SHOW_ALWAYS_PREMOD_USER_DIALOG,
|
||||
user,
|
||||
});
|
||||
export const hideAlwaysPremodUserDialog = () => ({
|
||||
type: HIDE_ALWAYS_PREMOD_USER_DIALOG,
|
||||
});
|
||||
export const showBanUserDialog = (user) => ({type: SHOW_BANUSER_DIALOG, user});
|
||||
export const hideBanUserDialog = () => ({type: HIDE_BANUSER_DIALOG});
|
||||
|
||||
// Reject Username Dialog
|
||||
export const showRejectUsernameDialog = user => ({
|
||||
type: SHOW_REJECT_USERNAME_DIALOG,
|
||||
user,
|
||||
});
|
||||
export const hideRejectUsernameDialog = () => ({
|
||||
type: HIDE_REJECT_USERNAME_DIALOG,
|
||||
});
|
||||
|
||||
// Enable or disable the activity indicator subscriptions.
|
||||
export const setIndicatorTrack = track => ({
|
||||
type: SET_INDICATOR_TRACK,
|
||||
track,
|
||||
});
|
||||
export const showRejectUsernameDialog = (user) => ({type: SHOW_REJECT_USERNAME_DIALOG, user});
|
||||
export const hideRejectUsernameDialog = () => ({type: HIDE_REJECT_USERNAME_DIALOG});
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export const CONFIG_UPDATED = 'CONFIG_UPDATED';
|
||||
|
||||
export const fetchConfig = () => (dispatch) => {
|
||||
let json = document.getElementById('data');
|
||||
let data = JSON.parse(json.textContent);
|
||||
dispatch({type: CONFIG_UPDATED, data});
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import * as actions from 'constants/configure';
|
||||
|
||||
export const updatePending = ({ updater, errorUpdater }) => {
|
||||
return { type: actions.UPDATE_PENDING, updater, errorUpdater };
|
||||
};
|
||||
|
||||
export const clearPending = () => {
|
||||
return { type: actions.CLEAR_PENDING };
|
||||
};
|
||||
|
||||
export const showSaveDialog = () => {
|
||||
return { type: actions.SHOW_SAVE_DIALOG };
|
||||
};
|
||||
|
||||
export const hideSaveDialog = () => {
|
||||
return { type: actions.HIDE_SAVE_DIALOG };
|
||||
};
|
||||
@@ -1,19 +1,20 @@
|
||||
import coralApi from 'coral-framework/helpers/request';
|
||||
import * as actions from '../constants/install';
|
||||
import validate from 'coral-framework/helpers/validate';
|
||||
import errorMsj from 'coral-framework/helpers/error';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const nextStep = () => ({ type: actions.NEXT_STEP });
|
||||
export const previousStep = () => ({ type: actions.PREVIOUS_STEP });
|
||||
export const goToStep = step => ({ type: actions.GO_TO_STEP, step });
|
||||
export const nextStep = () => ({type: actions.NEXT_STEP});
|
||||
export const previousStep = () => ({type: actions.PREVIOUS_STEP});
|
||||
export const goToStep = (step) => ({type: actions.GO_TO_STEP, step});
|
||||
|
||||
const installRequest = () => ({ type: actions.INSTALL_REQUEST });
|
||||
const installSuccess = () => ({ type: actions.INSTALL_SUCCESS });
|
||||
const installFailure = error => ({ type: actions.INSTALL_FAILURE, error });
|
||||
const installRequest = () => ({type: actions.INSTALL_REQUEST});
|
||||
const installSuccess = () => ({type: actions.INSTALL_SUCCESS});
|
||||
const installFailure = (error) => ({type: actions.INSTALL_FAILURE, error});
|
||||
|
||||
const addError = (name, error) => ({ type: actions.ADD_ERROR, name, error });
|
||||
const hasError = error => ({ type: actions.HAS_ERROR, error });
|
||||
const clearErrors = () => ({ type: actions.CLEAR_ERRORS });
|
||||
const addError = (name, error) => ({type: actions.ADD_ERROR, name, error});
|
||||
const hasError = (error) => ({type: actions.HAS_ERROR, error});
|
||||
const clearErrors = () => ({type: actions.CLEAR_ERRORS});
|
||||
|
||||
const validation = (formData, dispatch, next) => {
|
||||
if (!(formData != null)) {
|
||||
@@ -21,14 +22,17 @@ const validation = (formData, dispatch, next) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const validKeys = Object.keys(formData).filter(name => name !== 'domains');
|
||||
const validKeys = Object.keys(formData)
|
||||
.filter((name) => name !== 'domains');
|
||||
|
||||
// Required Validation
|
||||
const empty = validKeys.filter(name => {
|
||||
const empty = validKeys
|
||||
.filter((name) => {
|
||||
const cond = !formData[name].length;
|
||||
|
||||
if (cond) {
|
||||
// Adding Error
|
||||
|
||||
// Adding Error
|
||||
dispatch(addError(name, 'This field is required.'));
|
||||
} else {
|
||||
dispatch(addError(name, ''));
|
||||
@@ -43,17 +47,19 @@ const validation = (formData, dispatch, next) => {
|
||||
}
|
||||
|
||||
// RegExp Validation
|
||||
const validation = validKeys.filter(name => {
|
||||
const cond = !validate[name](formData[name]);
|
||||
if (cond) {
|
||||
// Adding Error
|
||||
dispatch(addError(name, errorMsj[name]));
|
||||
} else {
|
||||
dispatch(addError(name, ''));
|
||||
}
|
||||
const validation = validKeys
|
||||
.filter((name) => {
|
||||
const cond = !validate[name](formData[name]);
|
||||
if (cond) {
|
||||
|
||||
return cond;
|
||||
});
|
||||
// Adding Error
|
||||
dispatch(addError(name, errorMsj[name]));
|
||||
} else {
|
||||
dispatch(addError(name, ''));
|
||||
}
|
||||
|
||||
return cond;
|
||||
});
|
||||
|
||||
if (validation.length) {
|
||||
dispatch(hasError());
|
||||
@@ -62,21 +68,20 @@ const validation = (formData, dispatch, next) => {
|
||||
|
||||
// Confirm Validation
|
||||
const prefixLength = 'confirm'.length;
|
||||
const confirm = validKeys.filter(name => {
|
||||
if (!name.startsWith('confirm')) {
|
||||
return false;
|
||||
}
|
||||
const confirm = validKeys
|
||||
.filter((name) => {
|
||||
if (!name.startsWith('confirm')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check that 'confirmX' equals 'X'.
|
||||
const other =
|
||||
name.substr(prefixLength, 1).toLowerCase() +
|
||||
name.substr(prefixLength + 1);
|
||||
const cond = formData[other] !== formData[name];
|
||||
if (cond) {
|
||||
dispatch(addError(name, errorMsj[name]));
|
||||
}
|
||||
return cond;
|
||||
});
|
||||
// Check that 'confirmX' equals 'X'.
|
||||
const other = name.substr(prefixLength, 1).toLowerCase() + name.substr(prefixLength + 1);
|
||||
const cond = formData[other] !== formData[name];
|
||||
if (cond) {
|
||||
dispatch(addError(name, errorMsj[name]));
|
||||
}
|
||||
return cond;
|
||||
});
|
||||
|
||||
if (confirm.length) {
|
||||
dispatch(hasError());
|
||||
@@ -88,75 +93,54 @@ const validation = (formData, dispatch, next) => {
|
||||
};
|
||||
|
||||
export const submitSettings = () => (dispatch, getState) => {
|
||||
const settingsFormData = getState().install.data.settings;
|
||||
const settingsFormData = getState().install.toJS().data.settings;
|
||||
validation(settingsFormData, dispatch, function() {
|
||||
dispatch(nextStep());
|
||||
});
|
||||
};
|
||||
|
||||
export const submitUser = () => (dispatch, getState) => {
|
||||
const userFormData = getState().install.data.user;
|
||||
const userFormData = getState().install.toJS().data.user;
|
||||
validation(userFormData, dispatch, function() {
|
||||
dispatch(nextStep());
|
||||
});
|
||||
};
|
||||
|
||||
export const finishInstall = () => (dispatch, getState, { rest }) => {
|
||||
const data = getState().install.data;
|
||||
export const finishInstall = () => (dispatch, getState) => {
|
||||
const data = getState().install.toJS().data;
|
||||
dispatch(installRequest());
|
||||
return rest('/setup', { method: 'POST', body: data })
|
||||
return coralApi('/setup', {method: 'POST', body: data})
|
||||
.then(() => {
|
||||
dispatch(installSuccess());
|
||||
dispatch(nextStep());
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(installFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
export const updateSettingsFormData = (name, value) => ({
|
||||
type: actions.UPDATE_FORMDATA_SETTINGS,
|
||||
name,
|
||||
value,
|
||||
});
|
||||
export const updateUserFormData = (name, value) => ({
|
||||
type: actions.UPDATE_FORMDATA_USER,
|
||||
name,
|
||||
value,
|
||||
});
|
||||
export const updatePermittedDomains = value => ({
|
||||
type: actions.UPDATE_PERMITTED_DOMAINS_SETTINGS,
|
||||
value,
|
||||
});
|
||||
export const updateSettingsFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_SETTINGS, name, value});
|
||||
export const updateUserFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_USER, name, value});
|
||||
export const updatePermittedDomains = (value) => ({type: actions.UPDATE_PERMITTED_DOMAINS_SETTINGS, value});
|
||||
|
||||
const checkInstallRequest = () => ({ type: actions.CHECK_INSTALL_REQUEST });
|
||||
const checkInstallSuccess = installed => ({
|
||||
type: actions.CHECK_INSTALL_SUCCESS,
|
||||
installed,
|
||||
});
|
||||
const checkInstallFailure = error => ({
|
||||
type: actions.CHECK_INSTALL_FAILURE,
|
||||
error,
|
||||
});
|
||||
const checkInstallRequest = () => ({type: actions.CHECK_INSTALL_REQUEST});
|
||||
const checkInstallSuccess = (installed) => ({type: actions.CHECK_INSTALL_SUCCESS, installed});
|
||||
const checkInstallFailure = (error) => ({type: actions.CHECK_INSTALL_FAILURE, error});
|
||||
|
||||
export const checkInstall = next => async (dispatch, _, { rest }) => {
|
||||
export const checkInstall = (next) => (dispatch) => {
|
||||
dispatch(checkInstallRequest());
|
||||
|
||||
try {
|
||||
const { installed } = await rest('/setup');
|
||||
dispatch(checkInstallSuccess(installed));
|
||||
if (installed) {
|
||||
next();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch(checkInstallFailure(errorMessage));
|
||||
}
|
||||
coralApi('/setup')
|
||||
.then(({installed}) => {
|
||||
dispatch(checkInstallSuccess(installed));
|
||||
if (installed) {
|
||||
next();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(checkInstallFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,39 +1,56 @@
|
||||
import * as actions from 'constants/moderation';
|
||||
|
||||
export const toggleModal = open => ({ type: actions.TOGGLE_MODAL, open });
|
||||
export const singleView = () => ({ type: actions.SINGLE_VIEW });
|
||||
export const toggleModal = (open) => ({type: actions.TOGGLE_MODAL, open});
|
||||
export const singleView = () => ({type: actions.SINGLE_VIEW});
|
||||
|
||||
// hide shortcuts note
|
||||
export const hideShortcutsNote = () => (dispatch, _, { localStorage }) => {
|
||||
localStorage.setItem('coral:shortcutsNote', 'hide');
|
||||
dispatch({ type: actions.HIDE_SHORTCUTS_NOTE });
|
||||
export const hideShortcutsNote = () => {
|
||||
try {
|
||||
window.localStorage.setItem('coral:shortcutsNote', 'hide');
|
||||
} catch (e) {
|
||||
|
||||
// above will fail in Safari private mode
|
||||
}
|
||||
|
||||
return {type: actions.HIDE_SHORTCUTS_NOTE};
|
||||
};
|
||||
|
||||
export const setSortOrder = order => ({
|
||||
export const viewUserDetail = (userId) => ({type: actions.VIEW_USER_DETAIL, userId});
|
||||
export const hideUserDetail = () => ({type: actions.HIDE_USER_DETAIL});
|
||||
|
||||
export const setSortOrder = (order) => ({
|
||||
type: actions.SET_SORT_ORDER,
|
||||
order,
|
||||
order
|
||||
});
|
||||
|
||||
export const toggleStorySearch = active => ({
|
||||
type: active ? actions.SHOW_STORY_SEARCH : actions.HIDE_STORY_SEARCH,
|
||||
export const changeUserDetailStatuses = (tab) => {
|
||||
let statuses;
|
||||
if (tab === 'all') {
|
||||
statuses = ['NONE', 'ACCEPTED', 'REJECTED', 'PREMOD'];
|
||||
} else if (tab === 'rejected') {
|
||||
statuses = ['REJECTED'];
|
||||
}
|
||||
return {type: actions.CHANGE_USER_DETAIL_STATUSES, tab, statuses};
|
||||
};
|
||||
|
||||
export const clearUserDetailSelections = () => ({type: actions.CLEAR_USER_DETAIL_SELECTIONS});
|
||||
|
||||
export const toggleSelectCommentInUserDetail = (id, active) => {
|
||||
return {
|
||||
type: active ? actions.SELECT_USER_DETAIL_COMMENT : actions.UNSELECT_USER_DETAIL_COMMENT,
|
||||
id
|
||||
};
|
||||
};
|
||||
|
||||
export const toggleStorySearch = (active) => ({
|
||||
type: active ? actions.SHOW_STORY_SEARCH : actions.HIDE_STORY_SEARCH
|
||||
});
|
||||
|
||||
export const storySearchChange = value => ({
|
||||
export const storySearchChange = (value) => ({
|
||||
type: actions.STORY_SEARCH_CHANGE_VALUE,
|
||||
value,
|
||||
value
|
||||
});
|
||||
|
||||
export const clearState = () => ({
|
||||
type: actions.CLEAR_STATE,
|
||||
});
|
||||
|
||||
export const selectCommentId = id => ({
|
||||
type: actions.SELECT_COMMENT,
|
||||
id,
|
||||
});
|
||||
|
||||
// Enable or disable the activity indicator subscriptions.
|
||||
export const setIndicatorTrack = track => ({
|
||||
type: actions.SET_INDICATOR_TRACK,
|
||||
track,
|
||||
type: actions.MODERATION_CLEAR_STATE
|
||||
});
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import {
|
||||
SHOW_REJECT_USERNAME_DIALOG,
|
||||
HIDE_REJECT_USERNAME_DIALOG,
|
||||
} from '../constants/rejectUsernameDialog';
|
||||
|
||||
export const showRejectUsernameDialog = ({ userId, username }) => ({
|
||||
type: SHOW_REJECT_USERNAME_DIALOG,
|
||||
userId,
|
||||
username,
|
||||
});
|
||||
|
||||
export const hideRejectUsernameDialog = () => ({
|
||||
type: HIDE_REJECT_USERNAME_DIALOG,
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const SETTINGS_LOADING = 'SETTINGS_LOADING';
|
||||
export const SETTINGS_RECEIVED = 'SETTINGS_RECEIVED';
|
||||
export const SETTINGS_FETCH_ERROR = 'SETTINGS_FETCH_ERROR';
|
||||
|
||||
export const SETTINGS_UPDATED = 'SETTINGS_UPDATED';
|
||||
|
||||
export const SAVE_SETTINGS_LOADING = 'SAVE_SETTINGS_LOADING';
|
||||
export const SAVE_SETTINGS_SUCCESS = 'SAVE_SETTINGS_SUCCESS';
|
||||
export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED';
|
||||
|
||||
export const WORDLIST_UPDATED = 'WORDLIST_UPDATED';
|
||||
export const DOMAINLIST_UPDATED = 'DOMAINLIST_UPDATED';
|
||||
|
||||
export const fetchSettings = () => (dispatch) => {
|
||||
dispatch({type: SETTINGS_LOADING});
|
||||
coralApi('/settings')
|
||||
.then((settings) => {
|
||||
dispatch({type: SETTINGS_RECEIVED, settings});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: SETTINGS_FETCH_ERROR, error: errorMessage});
|
||||
});
|
||||
};
|
||||
|
||||
// for updating top-level settings
|
||||
export const updateSettings = (settings) => {
|
||||
return {type: SETTINGS_UPDATED, settings};
|
||||
};
|
||||
|
||||
// this is a nested property, so it needs a special action.
|
||||
export const updateWordlist = (listName, list) => {
|
||||
return {type: WORDLIST_UPDATED, listName, list};
|
||||
};
|
||||
|
||||
export const updateDomainlist = (listName, list) => {
|
||||
return {type: DOMAINLIST_UPDATED, listName, list};
|
||||
};
|
||||
|
||||
export const saveSettingsToServer = () => (dispatch, getState) => {
|
||||
let settings = getState().settings.toJS();
|
||||
if (settings.charCount) {
|
||||
settings.charCount = parseInt(settings.charCount);
|
||||
}
|
||||
dispatch({type: SAVE_SETTINGS_LOADING});
|
||||
coralApi('/settings', {method: 'PUT', body: settings})
|
||||
.then(() => {
|
||||
dispatch({type: SAVE_SETTINGS_SUCCESS, settings});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: SAVE_SETTINGS_FAILED, error: errorMessage});
|
||||
});
|
||||
};
|
||||
@@ -1,78 +0,0 @@
|
||||
import queryString from 'querystringify';
|
||||
|
||||
import {
|
||||
FETCH_ASSETS_REQUEST,
|
||||
FETCH_ASSETS_SUCCESS,
|
||||
FETCH_ASSETS_FAILURE,
|
||||
SET_PAGE,
|
||||
SET_SEARCH_VALUE,
|
||||
SET_CRITERIA,
|
||||
UPDATE_ASSET_STATE_REQUEST,
|
||||
UPDATE_ASSET_STATE_SUCCESS,
|
||||
UPDATE_ASSET_STATE_FAILURE,
|
||||
UPDATE_ASSETS,
|
||||
} from '../constants/stories';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
/**
|
||||
* Action disptacher related to assets
|
||||
*/
|
||||
|
||||
// Fetch a page of assets
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
export const fetchAssets = (query = {}) => (dispatch, _, { rest }) => {
|
||||
dispatch({ type: FETCH_ASSETS_REQUEST });
|
||||
return rest(`/assets?${queryString.stringify(query)}`)
|
||||
.then(({ result, page, count, limit, totalPages }) =>
|
||||
dispatch({
|
||||
type: FETCH_ASSETS_SUCCESS,
|
||||
assets: result,
|
||||
page,
|
||||
count,
|
||||
limit,
|
||||
totalPages,
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch({ type: FETCH_ASSETS_FAILURE, error: errorMessage });
|
||||
});
|
||||
};
|
||||
|
||||
// Update an asset state
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
export const updateAssetState = (id, closedAt) => (dispatch, _, { rest }) => {
|
||||
dispatch({ type: UPDATE_ASSET_STATE_REQUEST, id, closedAt });
|
||||
return rest(`/assets/${id}/status`, { method: 'PUT', body: { closedAt } })
|
||||
.then(() => dispatch({ type: UPDATE_ASSET_STATE_SUCCESS }))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch({ type: UPDATE_ASSET_STATE_FAILURE, error: errorMessage });
|
||||
});
|
||||
};
|
||||
|
||||
export const updateAssets = assets => dispatch => {
|
||||
dispatch({ type: UPDATE_ASSETS, assets });
|
||||
};
|
||||
|
||||
export const setPage = page => ({
|
||||
type: SET_PAGE,
|
||||
page,
|
||||
});
|
||||
|
||||
export const setSearchValue = value => ({
|
||||
type: SET_SEARCH_VALUE,
|
||||
value,
|
||||
});
|
||||
|
||||
export const setCriteria = criteria => ({
|
||||
type: SET_CRITERIA,
|
||||
criteria,
|
||||
});
|
||||
@@ -1,19 +1,7 @@
|
||||
import {
|
||||
SHOW_SUSPEND_USER_DIALOG,
|
||||
HIDE_SUSPEND_USER_DIALOG,
|
||||
} from '../constants/suspendUserDialog.js';
|
||||
import {SHOW_SUSPEND_USER_DIALOG, HIDE_SUSPEND_USER_DIALOG} from '../constants/suspendUserDialog.js';
|
||||
|
||||
export const showSuspendUserDialog = ({
|
||||
userId,
|
||||
username,
|
||||
commentId,
|
||||
commentStatus,
|
||||
}) => ({
|
||||
type: SHOW_SUSPEND_USER_DIALOG,
|
||||
userId,
|
||||
username,
|
||||
commentId,
|
||||
commentStatus,
|
||||
});
|
||||
export const showSuspendUserDialog = ({userId, username, commentId, commentStatus}) =>
|
||||
({type: SHOW_SUSPEND_USER_DIALOG, userId, username, commentId, commentStatus});
|
||||
|
||||
export const hideSuspendUserDialog = () => ({type: HIDE_SUSPEND_USER_DIALOG});
|
||||
|
||||
export const hideSuspendUserDialog = () => ({ type: HIDE_SUSPEND_USER_DIALOG });
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import * as actions from 'constants/userDetail';
|
||||
|
||||
export const viewUserDetail = userId => ({
|
||||
type: actions.VIEW_USER_DETAIL,
|
||||
userId,
|
||||
});
|
||||
export const hideUserDetail = () => ({ type: actions.HIDE_USER_DETAIL });
|
||||
|
||||
export const changeTab = tab => {
|
||||
let statuses = null;
|
||||
if (tab === 'rejected') {
|
||||
statuses = ['REJECTED'];
|
||||
}
|
||||
return { type: actions.CHANGE_TAB_USER_DETAIL, tab, statuses };
|
||||
};
|
||||
|
||||
export const clearUserDetailSelections = () => ({
|
||||
type: actions.CLEAR_USER_DETAIL_SELECTIONS,
|
||||
});
|
||||
|
||||
export const toggleSelectCommentInUserDetail = (id, active) => {
|
||||
return {
|
||||
type: active
|
||||
? actions.SELECT_USER_DETAIL_COMMENT
|
||||
: actions.UNSELECT_USER_DETAIL_COMMENT,
|
||||
id,
|
||||
};
|
||||
};
|
||||
|
||||
export const toggleSelectAllCommentInUserDetail = (ids, active) => {
|
||||
return {
|
||||
type: active
|
||||
? actions.SELECT_ALL_USER_DETAIL_COMMENT
|
||||
: actions.CLEAR_USER_DETAIL_SELECTIONS,
|
||||
ids,
|
||||
};
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import * as userTypes from '../constants/users';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
@@ -6,56 +7,38 @@ import t from 'coral-framework/services/i18n';
|
||||
*/
|
||||
// change status of a user
|
||||
export const userStatusUpdate = (status, userId, commentId) => {
|
||||
return (dispatch, _, { rest }) => {
|
||||
dispatch({ type: userTypes.UPDATE_STATUS_REQUEST });
|
||||
return rest(`/users/${userId}/status`, {
|
||||
method: 'POST',
|
||||
body: { status: status, comment_id: commentId },
|
||||
})
|
||||
.then(res => dispatch({ type: userTypes.UPDATE_STATUS_SUCCESS, res }))
|
||||
.catch(error => {
|
||||
return (dispatch) => {
|
||||
dispatch({type: userTypes.UPDATE_STATUS_REQUEST});
|
||||
return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
|
||||
.then((res) => dispatch({type: userTypes.UPDATE_STATUS_SUCCESS, res}))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch({
|
||||
type: userTypes.UPDATE_STATUS_FAILURE,
|
||||
error: errorMessage,
|
||||
});
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: userTypes.UPDATE_STATUS_FAILURE, error: errorMessage});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// change status of a user
|
||||
export const sendNotificationEmail = (userId, subject, body) => {
|
||||
return (dispatch, _, { rest }) => {
|
||||
return rest(`/users/${userId}/email`, {
|
||||
method: 'POST',
|
||||
body: { subject, body },
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch({ type: userTypes.USER_EMAIL_FAILURE, error: errorMessage });
|
||||
});
|
||||
return (dispatch) => {
|
||||
return coralApi(`/users/${userId}/email`, {method: 'POST', body: {subject, body}})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: userTypes.USER_EMAIL_FAILURE, error: errorMessage});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// let a user edit their username
|
||||
export const enableUsernameEdit = userId => {
|
||||
return (dispatch, _, { rest }) => {
|
||||
return rest(`/users/${userId}/username-enable`, { method: 'POST' }).catch(
|
||||
error => {
|
||||
export const enableUsernameEdit = (userId) => {
|
||||
return (dispatch) => {
|
||||
return coralApi(`/users/${userId}/username-enable`, {method: 'POST'})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch({
|
||||
type: userTypes.USERNAME_ENABLE_FAILURE,
|
||||
error: errorMessage,
|
||||
});
|
||||
}
|
||||
);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: userTypes.USERNAME_ENABLE_FAILURE, error: errorMessage});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './ModerationList.css';
|
||||
import {Button} from 'coral-ui';
|
||||
import {menuActionsMap} from '../routes/Moderation/helpers/moderationQueueActionsMap';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const ActionButton = ({type = '', active, ...props}) => {
|
||||
const typeName = type.toLowerCase();
|
||||
let text = menuActionsMap[type].text;
|
||||
|
||||
if (text === 'approve' && active) {
|
||||
text = 'approved';
|
||||
} else if (text === 'reject' && active) {
|
||||
text = 'rejected';
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={`${typeName} ${styles.actionButton} ${props.minimal ? styles.minimal : ''} ${active ? styles[`${typeName}__active`] : ''}`}
|
||||
cStyle={typeName}
|
||||
icon={menuActionsMap[type].icon}
|
||||
onClick={type === 'APPROVE' ? props.acceptComment : props.rejectComment}
|
||||
>{props.minimal ? '' : t(`modqueue.${text}`)}</Button>
|
||||
);
|
||||
};
|
||||
|
||||
ActionButton.propTypes = {
|
||||
active: PropTypes.bool
|
||||
};
|
||||
|
||||
export default ActionButton;
|
||||
@@ -8,6 +8,9 @@
|
||||
color: black;
|
||||
> :global(.mdl-menu__container) {
|
||||
margin-left: 10px;
|
||||
> :global(.mdl-menu__outline) {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +18,12 @@
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
background-color: #616161;
|
||||
border-color: #616161;
|
||||
}
|
||||
|
||||
.arrowIcon {
|
||||
margin-left: 6px;
|
||||
margin-right: 0;
|
||||
vertical-align: middle;
|
||||
vertical-align: middle;
|
||||
margin-right: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -31,10 +33,8 @@
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
color: #2a2a2a;
|
||||
background-color: white;
|
||||
font-size: 0.95em;
|
||||
|
||||
background-color: #2a2a2a;
|
||||
color: white;
|
||||
&:first-child {
|
||||
margin-bottom: 1px;
|
||||
border-radius: 2px 2px 0px 0px;
|
||||
@@ -43,8 +43,7 @@
|
||||
border-radius: 0px 0px 2px 2px;
|
||||
}
|
||||
&:hover, &:active, &:focus {
|
||||
background-color: #e2e2e2;
|
||||
border-color: #616161;
|
||||
background-color: #767676;
|
||||
}
|
||||
&[disabled], &[disabled]:hover, &[disabled]:focus, &[disabled]:active {
|
||||
background-color: #262626;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Icon } from 'coral-ui';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
import { Menu } from 'react-mdl';
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Button, Icon} from 'coral-ui';
|
||||
import {Menu} from 'react-mdl';
|
||||
import cn from 'classnames';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import styles from './ActionsMenu.css';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
@@ -14,70 +12,51 @@ let count = 0;
|
||||
class ActionsMenu extends React.Component {
|
||||
id = `actions-dropdown-${count++}`;
|
||||
menu = null;
|
||||
state = { open: false };
|
||||
state = {open: false};
|
||||
timeout = null;
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
|
||||
handleRef = ref => {
|
||||
handleRef = (ref) => {
|
||||
this.menu = ref ? findDOMNode(ref).parentNode : null;
|
||||
};
|
||||
}
|
||||
|
||||
syncOpenState = () => {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = setTimeout(() => {
|
||||
this.setState({ open: this.menu.className.indexOf('is-visible') >= 0 });
|
||||
this.setState({open: this.menu.className.indexOf('is-visible') >= 0});
|
||||
}, 150);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { className = '', buttonClassNames = '', label = '' } = this.props;
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.syncOpenState}>
|
||||
<div
|
||||
className={cn(styles.root, className)}
|
||||
onBlur={this.syncOpenState}
|
||||
<div className={styles.root} onBlur={this.syncOpenState} >
|
||||
<Button
|
||||
cStyle='actions'
|
||||
className={cn(styles.button, {[styles.buttonOpen]: this.state.open})}
|
||||
disabled={false}
|
||||
id={this.id}
|
||||
onClick={this.syncOpenState}
|
||||
onKeyUp={this.syncOpenState}
|
||||
>
|
||||
<Button
|
||||
cStyle="actions"
|
||||
className={cn(
|
||||
styles.button,
|
||||
{ [styles.buttonOpen]: this.state.open },
|
||||
buttonClassNames
|
||||
)}
|
||||
disabled={false}
|
||||
id={this.id}
|
||||
onClick={this.syncOpenState}
|
||||
icon={this.props.icon}
|
||||
raised
|
||||
>
|
||||
{label ? label : t('modqueue.actions')}
|
||||
<Icon
|
||||
name={
|
||||
this.state.open ? 'keyboard_arrow_up' : 'keyboard_arrow_down'
|
||||
}
|
||||
className={styles.arrowIcon}
|
||||
/>
|
||||
</Button>
|
||||
<Menu target={this.id} className={styles.menu} ref={this.handleRef}>
|
||||
{this.props.children}
|
||||
</Menu>
|
||||
</div>
|
||||
</ClickOutside>
|
||||
icon={this.props.icon}
|
||||
raised>
|
||||
{t('modqueue.actions')}
|
||||
<Icon
|
||||
name={this.state.open ? 'keyboard_arrow_up' : 'keyboard_arrow_down'}
|
||||
className={styles.arrowIcon}
|
||||
/>
|
||||
</Button>
|
||||
<Menu target={this.id} className={styles.menu} ref={this.handleRef}>
|
||||
{this.props.children}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ActionsMenu.propTypes = {
|
||||
icon: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
label: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
|
||||
buttonClassNames: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ActionsMenu;
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { MenuItem } from 'react-mdl';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MenuItem} from 'react-mdl';
|
||||
import styles from './ActionsMenu.css';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
|
||||
const ActionsMenuItem = props => (
|
||||
<MenuItem
|
||||
className={cn(styles.menuItem, props.className, 'action-menu-item')}
|
||||
{...props}
|
||||
id={camelCase(props.children)}
|
||||
/>
|
||||
);
|
||||
|
||||
ActionsMenuItem.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
|
||||
};
|
||||
const ActionsMenuItem = (props) =>
|
||||
<MenuItem className={cn(styles.menuItem, props.className)} {...props} />;
|
||||
|
||||
export default ActionsMenuItem;
|
||||
|
||||
@@ -1,128 +1,98 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Layout from 'coral-admin/src/components/Layout';
|
||||
import React, {PropTypes} from 'react';
|
||||
import Layout from 'coral-admin/src/components/ui/Layout';
|
||||
import styles from './NotFound.css';
|
||||
import { Button, TextField, Alert, Success } from 'coral-ui';
|
||||
import {Button, TextField, Alert, Success} from 'coral-ui';
|
||||
import Recaptcha from 'react-recaptcha';
|
||||
import cn from 'classnames';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class AdminLogin extends React.Component {
|
||||
constructor(props) {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = { email: '', password: '', requestPassword: false };
|
||||
this.state = {email: '', password: '', requestPassword: false};
|
||||
}
|
||||
|
||||
handleSignIn = e => {
|
||||
handleSignIn = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.handleLogin(this.state.email, this.state.password);
|
||||
};
|
||||
}
|
||||
|
||||
onRecaptchaLoad = () => {
|
||||
|
||||
// do something?
|
||||
};
|
||||
}
|
||||
|
||||
onRecaptchaVerify = recaptchaResponse => {
|
||||
this.props.handleLogin(
|
||||
this.state.email,
|
||||
this.state.password,
|
||||
recaptchaResponse
|
||||
);
|
||||
};
|
||||
onRecaptchaVerify = (recaptchaResponse) => {
|
||||
this.props.handleLogin(this.state.email, this.state.password, recaptchaResponse);
|
||||
}
|
||||
|
||||
handleRequestPassword = e => {
|
||||
handleRequestPassword = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.requestPasswordReset(this.state.email);
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { errorMessage, loginMaxExceeded, recaptchaPublic } = this.props;
|
||||
render () {
|
||||
const {errorMessage, loginMaxExceeded, recaptchaPublic} = this.props;
|
||||
const signInForm = (
|
||||
<form className="talk-admin-login-sign-in" onSubmit={this.handleSignIn}>
|
||||
<form onSubmit={this.handleSignIn}>
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
id="email"
|
||||
label={t('login.email_address')}
|
||||
label='Email Address'
|
||||
value={this.state.email}
|
||||
onChange={e => this.setState({ email: e.target.value })}
|
||||
/>
|
||||
onChange={(e) => this.setState({email: e.target.value})} />
|
||||
<TextField
|
||||
id="password"
|
||||
label={t('login.password')}
|
||||
label='Password'
|
||||
value={this.state.password}
|
||||
onChange={e => this.setState({ password: e.target.value })}
|
||||
type="password"
|
||||
/>
|
||||
<div style={{ height: 10 }} />
|
||||
onChange={(e) => this.setState({password: e.target.value})}
|
||||
type='password' />
|
||||
<div style={{height: 10}}></div>
|
||||
<Button
|
||||
className="talk-admin-login-sign-in-button"
|
||||
type="submit"
|
||||
cStyle="black"
|
||||
type='submit'
|
||||
cStyle='black'
|
||||
full
|
||||
onClick={this.handleSignIn}
|
||||
>
|
||||
{t('login.sign_in_button')}
|
||||
</Button>
|
||||
onClick={this.handleSignIn}>Sign In</Button>
|
||||
<p className={styles.forgotPasswordCTA}>
|
||||
{t('login.forgot_password')}{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.forgotPasswordLink}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
this.setState({ requestPassword: true });
|
||||
}}
|
||||
>
|
||||
{t('login.request_passowrd')}
|
||||
</a>
|
||||
Forgot your password? <a href="#" className={styles.forgotPasswordLink} onClick={(e) => {
|
||||
e.preventDefault();
|
||||
this.setState({requestPassword: true});
|
||||
}}>Request a new one.</a>
|
||||
</p>
|
||||
{loginMaxExceeded && (
|
||||
{
|
||||
loginMaxExceeded &&
|
||||
<Recaptcha
|
||||
sitekey={recaptchaPublic}
|
||||
render="explicit"
|
||||
theme="dark"
|
||||
render='explicit'
|
||||
theme='dark'
|
||||
onloadCallback={this.onRecaptchaLoad}
|
||||
verifyCallback={this.onRecaptchaVerify}
|
||||
/>
|
||||
)}
|
||||
verifyCallback={this.onRecaptchaVerify} />
|
||||
}
|
||||
</form>
|
||||
);
|
||||
const requestPasswordForm = this.props.passwordRequestSuccess ? (
|
||||
<p
|
||||
className={styles.passwordRequestSuccess}
|
||||
onClick={() => {
|
||||
location.href = location.href;
|
||||
}}
|
||||
>
|
||||
{this.props.passwordRequestSuccess}{' '}
|
||||
<a className={styles.signInLink} href="#">
|
||||
{t('login.sign_in')}
|
||||
</a>
|
||||
<Success />
|
||||
</p>
|
||||
) : (
|
||||
<form onSubmit={this.handleRequestPassword}>
|
||||
const requestPasswordForm = (
|
||||
this.props.passwordRequestSuccess
|
||||
? <p className={styles.passwordRequestSuccess} onClick={() => {
|
||||
location.href = location.href;
|
||||
}}>
|
||||
{this.props.passwordRequestSuccess} <a className={styles.signInLink} href="#">Sign in</a>
|
||||
<Success />
|
||||
</p>
|
||||
: <form onSubmit={this.handleRequestPassword}>
|
||||
<TextField
|
||||
label={t('login.email_address')}
|
||||
label='Email Address'
|
||||
value={this.state.email}
|
||||
onChange={e => this.setState({ email: e.target.value })}
|
||||
/>
|
||||
onChange={(e) => this.setState({email: e.target.value})} />
|
||||
<Button
|
||||
type="submit"
|
||||
cStyle="black"
|
||||
type='submit'
|
||||
cStyle='black'
|
||||
full
|
||||
onClick={this.handleRequestPassword}
|
||||
>
|
||||
{t('login.reset_password')}
|
||||
</Button>
|
||||
onClick={this.handleRequestPassword}>Reset Password</Button>
|
||||
</form>
|
||||
);
|
||||
return (
|
||||
<Layout fixedDrawer restricted={true}>
|
||||
<div className={cn(styles.loginLayout, 'talk-admin-login')}>
|
||||
<h1 className={styles.loginHeader}>{t('login.team_sign_in')}</h1>
|
||||
<p className={styles.loginCTA}>{t('login.sign_in_message')}</p>
|
||||
{this.state.requestPassword ? requestPasswordForm : signInForm}
|
||||
<div className={styles.loginLayout}>
|
||||
<h1 className={styles.loginHeader}>Team sign in</h1>
|
||||
<p className={styles.loginCTA}>Sign in to interact with your community.</p>
|
||||
{ this.state.requestPassword ? requestPasswordForm : signInForm }
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
@@ -134,9 +104,7 @@ AdminLogin.propTypes = {
|
||||
handleLogin: PropTypes.func.isRequired,
|
||||
passwordRequestSuccess: PropTypes.string,
|
||||
loginError: PropTypes.string,
|
||||
recaptchaPublic: PropTypes.string,
|
||||
requestPasswordReset: PropTypes.func,
|
||||
errorMessage: PropTypes.string,
|
||||
recaptchaPublic: PropTypes.string
|
||||
};
|
||||
|
||||
export default AdminLogin;
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
.dialog {
|
||||
border: none;
|
||||
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
|
||||
width: 400px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.header {
|
||||
color: black;
|
||||
font-size: 1.5em;
|
||||
font-weight: 500;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.subheader {
|
||||
color: black;
|
||||
font-size: 1.3em;
|
||||
font-weight: 500;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 6px;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Dialog } from 'coral-ui';
|
||||
import styles from './AlwaysPremodUserDialog.css';
|
||||
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class AlwaysPremodUserDialog extends React.Component {
|
||||
handlePerform = () => {
|
||||
this.props.onPerform();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { open, onCancel, username, info } = this.props;
|
||||
return (
|
||||
<Dialog
|
||||
className={cn(styles.dialog, 'talk-always-premod-user-dialog')}
|
||||
id="alwaysPremodUserDialog"
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
title={t('alwayspremoddialog.always_premod_user')}
|
||||
>
|
||||
<span className={styles.close} onClick={onCancel}>
|
||||
×
|
||||
</span>
|
||||
<section>
|
||||
<h2 className={styles.header}>
|
||||
{t('alwayspremoddialog.always_premod_user')}
|
||||
</h2>
|
||||
<h3 className={styles.subheader}>
|
||||
{t('alwayspremoddialog.are_you_sure', username)}
|
||||
</h3>
|
||||
<p className={styles.description}>{info}</p>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
className={cn('talk-always-premod-user-dialog-button-cancel')}
|
||||
cStyle="white"
|
||||
onClick={onCancel}
|
||||
raised
|
||||
>
|
||||
{t('alwayspremoddialog.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={cn('talk-always-premod-user-dialog-button-confirm')}
|
||||
cStyle="black"
|
||||
onClick={this.handlePerform}
|
||||
raised
|
||||
>
|
||||
{t('alwayspremoddialog.yes_always_premod_user')}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AlwaysPremodUserDialog.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
onPerform: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
username: PropTypes.string,
|
||||
info: PropTypes.string,
|
||||
};
|
||||
|
||||
export default AlwaysPremodUserDialog;
|
||||
@@ -1,11 +0,0 @@
|
||||
:global {
|
||||
html, body, #root, #root > div {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: #FAFAFA;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import React from 'react';
|
||||
import ToastContainer from './ToastContainer';
|
||||
import './App.css';
|
||||
import 'material-design-lite';
|
||||
|
||||
import AppRouter from '../AppRouter';
|
||||
|
||||
export default class App extends React.Component {
|
||||
render() {
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<ToastContainer />
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
.root {
|
||||
display: block;
|
||||
color: #519954;
|
||||
border: solid 2px rgba(81, 153, 84, 0.75);
|
||||
background: white;
|
||||
padding: 10px 12px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 3px;
|
||||
text-transform: capitalize;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
|
||||
width: 100%;
|
||||
margin: 0 0 .5em;
|
||||
|
||||
&:not(:disabled):hover {
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
background-color: #519954;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
background-color: #519954;
|
||||
|
||||
&:hover {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.minimal {
|
||||
width: 45px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import cn from 'classnames';
|
||||
import styles from './ApproveButton.css';
|
||||
import { Icon } from 'coral-ui';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const ApproveButton = ({ active, minimal, onClick, className, disabled }) => {
|
||||
const text = active ? t('modqueue.approved') : t('modqueue.approve');
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
styles.root,
|
||||
{ [styles.minimal]: minimal, [styles.active]: active },
|
||||
className,
|
||||
'talk-admin-approve-button'
|
||||
)}
|
||||
onClick={onClick}
|
||||
disabled={disabled || active}
|
||||
>
|
||||
<Icon name={'done'} className={styles.icon} />
|
||||
{!minimal && text}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
ApproveButton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
active: PropTypes.bool,
|
||||
minimal: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ApproveButton;
|
||||
@@ -1,153 +1,153 @@
|
||||
.dialog {
|
||||
border: none;
|
||||
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
|
||||
width: 400px;
|
||||
width: 500px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 184px;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.header {
|
||||
color: black;
|
||||
font-size: 1.5em;
|
||||
font-weight: 500;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
h2 {
|
||||
color: black;
|
||||
font-size: 1.76em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.subheader {
|
||||
color: black;
|
||||
font-size: 1.3em;
|
||||
font-weight: 500;
|
||||
margin: 0 0 8px 0;
|
||||
h3 {
|
||||
color: black;
|
||||
font-size: 1.4em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.textField {
|
||||
margin-top: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.textField label {
|
||||
font-size: 1.08em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
font-size: 1.08em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.textField input {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border: none;
|
||||
outline: none;
|
||||
border: 1px solid rgba(0,0,0,.12);
|
||||
padding: 10px 6px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
margin: 5px auto;
|
||||
width: 100%;
|
||||
display: block;
|
||||
border: none;
|
||||
outline: none;
|
||||
border: 1px solid rgba(0,0,0,.12);
|
||||
padding: 10px 6px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
margin: 5px auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin: 20px auto 10px;
|
||||
text-align: center;
|
||||
margin: 20px auto 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer span {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0 5px;
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.socialConnections {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.signInButton {
|
||||
margin-top: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.close {
|
||||
font-size: 20px;
|
||||
line-height: 14px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #363636;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
line-height: 14px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #363636;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: #6b6b6b;
|
||||
color: #6b6b6b;
|
||||
}
|
||||
|
||||
input.error{
|
||||
border: solid 2px #f44336;
|
||||
border: solid 2px #f44336;
|
||||
}
|
||||
|
||||
.errorMsg, .hint {
|
||||
color: grey;
|
||||
font-weight: 600;
|
||||
padding: 3px 0 16px;
|
||||
color: grey;
|
||||
font-weight: 600;
|
||||
padding: 3px 0 16px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 2px;
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.alert--success {
|
||||
border: solid 1px #1ec00e;
|
||||
background: #cbf1b8;
|
||||
color: #006900;
|
||||
border: solid 1px #1ec00e;
|
||||
background: #cbf1b8;
|
||||
color: #006900;
|
||||
}
|
||||
|
||||
.alert--error {
|
||||
background: #FFEBEE;
|
||||
color: #B71C1C;
|
||||
background: #FFEBEE;
|
||||
color: #B71C1C;
|
||||
}
|
||||
|
||||
.userBox a {
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0px;
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.attention {
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background: #B71C1C;
|
||||
color: #FFEBEE;
|
||||
font-weight: bolder;
|
||||
padding: 4px;
|
||||
vertical-align: middle;
|
||||
border-radius: 20px;
|
||||
box-sizing: border-box;
|
||||
font-size: 9px;
|
||||
line-height: 7px;
|
||||
text-align: center;
|
||||
margin-right: 5px;
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background: #B71C1C;
|
||||
color: #FFEBEE;
|
||||
font-weight: bolder;
|
||||
padding: 4px;
|
||||
vertical-align: middle;
|
||||
border-radius: 20px;
|
||||
box-sizing: border-box;
|
||||
font-size: 9px;
|
||||
line-height: 7px;
|
||||
text-align: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.action {
|
||||
margin-top: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.passwordRequestSuccess {
|
||||
border: 1px solid green;
|
||||
background-color: lightgreen;
|
||||
padding: 10px;
|
||||
border: 1px solid green;
|
||||
background-color: lightgreen;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.passwordRequestFailure {
|
||||
border: 1px solid orange;
|
||||
background-color: 1px solid coral;
|
||||
padding: 10px;
|
||||
border: 1px solid orange;
|
||||
background-color: 1px solid coral;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
@@ -160,20 +160,6 @@ input.error{
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 6px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.legend {
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.messageInput {
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
margin: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -1,138 +1,37 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Dialog } from 'coral-ui';
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import styles from './BanUserDialog.css';
|
||||
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const initialState = { step: 0, message: '' };
|
||||
|
||||
class BanUserDialog extends React.Component {
|
||||
state = initialState;
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
if (this.props.open && !next.open) {
|
||||
this.setState(initialState);
|
||||
}
|
||||
}
|
||||
|
||||
handleMessageChange = e => {
|
||||
const {
|
||||
target: { value: message },
|
||||
} = e;
|
||||
this.setState({ message });
|
||||
};
|
||||
|
||||
goToStep1 = () => {
|
||||
this.setState({
|
||||
step: 1,
|
||||
message: t('bandialog.email_message_ban', this.props.username),
|
||||
});
|
||||
};
|
||||
|
||||
handlePerform = () => {
|
||||
this.props.onPerform({
|
||||
message: this.state.message,
|
||||
});
|
||||
};
|
||||
|
||||
renderStep0() {
|
||||
const { onCancel, username, info } = this.props;
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2 className={styles.header}>{t('bandialog.ban_user')}</h2>
|
||||
<h3 className={styles.subheader}>
|
||||
{t('bandialog.are_you_sure', username)}
|
||||
</h3>
|
||||
<p className={styles.description}>{info}</p>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
className={cn('talk-ban-user-dialog-button-cancel')}
|
||||
cStyle="white"
|
||||
onClick={onCancel}
|
||||
raised
|
||||
>
|
||||
{t('bandialog.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={cn('talk-ban-user-dialog-button-confirm')}
|
||||
cStyle="black"
|
||||
onClick={this.goToStep1}
|
||||
raised
|
||||
>
|
||||
{t('bandialog.yes_ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
renderStep1() {
|
||||
const { onCancel } = this.props;
|
||||
const { message } = this.state;
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2 className={styles.header}>{t('bandialog.notify_ban_headline')}</h2>
|
||||
<p className={styles.description}>
|
||||
{t('bandialog.notify_ban_description')}
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend className={styles.legend}>
|
||||
{t('bandialog.write_a_message')}
|
||||
</legend>
|
||||
<textarea
|
||||
rows={5}
|
||||
className={styles.messageInput}
|
||||
value={message}
|
||||
onChange={this.handleMessageChange}
|
||||
/>
|
||||
</fieldset>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
className={cn('talk-ban-user-dialog-button-cancel')}
|
||||
cStyle="white"
|
||||
onClick={onCancel}
|
||||
raised
|
||||
>
|
||||
{t('bandialog.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={cn('talk-ban-user-dialog-button-confirm')}
|
||||
cStyle="black"
|
||||
onClick={this.handlePerform}
|
||||
raised
|
||||
>
|
||||
{t('bandialog.send')}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { step } = this.state;
|
||||
const { open, onCancel } = this.props;
|
||||
return (
|
||||
<Dialog
|
||||
className={cn(styles.dialog, 'talk-ban-user-dialog')}
|
||||
id="banUserDialog"
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
title={t('bandialog.ban_user')}
|
||||
>
|
||||
<span className={styles.close} onClick={onCancel}>
|
||||
×
|
||||
</span>
|
||||
{step === 0 && this.renderStep0()}
|
||||
{step === 1 && this.renderStep1()}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
const BanUserDialog = ({open, onCancel, onPerform, username, info}) => (
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
id="banUserDialog"
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
title={t('bandialog.ban_user')}>
|
||||
<span className={styles.close} onClick={onCancel}>×</span>
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h2>{t('bandialog.ban_user')}</h2>
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h3>{t('bandialog.are_you_sure', username)}</h3>
|
||||
<i>{info}</i>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<Button cStyle="cancel" className={styles.cancel} onClick={onCancel} raised>
|
||||
{t('bandialog.cancel')}
|
||||
</Button>
|
||||
<Button cStyle="black" className={styles.ban} onClick={onPerform} raised>
|
||||
{t('bandialog.yes_ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
BanUserDialog.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Button } from 'coral-ui';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { withCopyToClipboard } from 'coral-framework/hocs';
|
||||
|
||||
class ButtonCopyToClipboard extends React.Component {
|
||||
render() {
|
||||
return <Button {...this.props}>{t('common.copy')}</Button>;
|
||||
}
|
||||
}
|
||||
|
||||
export default withCopyToClipboard(ButtonCopyToClipboard);
|
||||
@@ -1,28 +0,0 @@
|
||||
.root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bodyLeave {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
background-color: white;
|
||||
opacity: 1.0;
|
||||
transition: background 400ms, opacity 800ms 1600ms;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bodyLeaveActive {
|
||||
opacity: 0;
|
||||
background-color: rgba(255,255,0, 0.2);
|
||||
}
|
||||
|
||||
.bodyEnter {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bodyEnterActive {
|
||||
opacity: 1.0;
|
||||
transition: opacity 800ms 2400ms;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import { murmur3 } from 'murmurhash-js';
|
||||
import { CSSTransitionGroup } from 'react-transition-group';
|
||||
import styles from './CommentAnimatedEdit.css';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const CommentAnimatedEdit = ({ children, body }) => {
|
||||
return (
|
||||
<CSSTransitionGroup
|
||||
component={'div'}
|
||||
className={styles.root}
|
||||
transitionName={{
|
||||
enter: styles.bodyEnter,
|
||||
enterActive: styles.bodyEnterActive,
|
||||
leave: styles.bodyLeave,
|
||||
leaveActive: styles.bodyLeaveActive,
|
||||
}}
|
||||
transitionEnter={true}
|
||||
transitionLeave={true}
|
||||
transitionEnterTimeout={3600}
|
||||
transitionLeaveTimeout={2800}
|
||||
>
|
||||
{React.cloneElement(React.Children.only(children), {
|
||||
key: murmur3(body),
|
||||
})}
|
||||
</CSSTransitionGroup>
|
||||
);
|
||||
};
|
||||
|
||||
CommentAnimatedEdit.propTypes = {
|
||||
children: PropTypes.node,
|
||||
body: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CommentAnimatedEdit;
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
.textareaContainer {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
|
||||
import React from 'react';
|
||||
import styles from './CommentBox.css';
|
||||
import {Button} from 'react-mdl';
|
||||
|
||||
// Renders a comment box for creating a new comment
|
||||
export default class CommentBox extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {name: '', body: ''};
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
}
|
||||
|
||||
onSubmit () {
|
||||
const {name, body} = this.state;
|
||||
this.props.onSubmit({name, body});
|
||||
this.setState({body: '', name: ''});
|
||||
}
|
||||
|
||||
render (props, {name, body}) {
|
||||
return (
|
||||
<div>
|
||||
<div class={`${styles.textareaContainer} mdl-textfield mdl-js-textfield`}>
|
||||
<input type='text' value={name} onInput={this.linkState('name')} class='mdl-textfield__input' id='name' />
|
||||
<label class='mdl-textfield__label' for='name'>Your name</label>
|
||||
</div>
|
||||
<div class={`${styles.textareaContainer} mdl-textfield mdl-js-textfield`}>
|
||||
<textarea value={body} onInput={this.linkState('body')} class='mdl-textfield__input' type='text' rows='5' id='comment' />
|
||||
<label class='mdl-textfield__label' for='comment'>Write your comment</label>
|
||||
</div>
|
||||
<Button onClick={this.onSubmit} raised>Post</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
.tombstone {
|
||||
background-color: #f0f0f0;
|
||||
padding: 1em;
|
||||
color: #1a212f;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './CommentDeletedTombstone.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const CommentDeletedTombstone = () => (
|
||||
<div className={styles.tombstone}>{t('framework.comment_is_deleted')}</div>
|
||||
);
|
||||
|
||||
export default CommentDeletedTombstone;
|
||||
@@ -1,18 +0,0 @@
|
||||
.root {
|
||||
min-height: 25px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.moreDetail {
|
||||
position: absolute;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: black;
|
||||
right: 16px;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './CommentDetails.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty';
|
||||
|
||||
class CommentDetails extends Component {
|
||||
state = {
|
||||
showDetail: false,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
showDetail: false,
|
||||
};
|
||||
}
|
||||
|
||||
toggleDetail = () => {
|
||||
this.setState(state => ({
|
||||
showDetail: !state.showDetail,
|
||||
}));
|
||||
this.props.clearHeightCache && this.props.clearHeightCache();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { root, comment, clearHeightCache } = this.props;
|
||||
const { showDetail } = this.state;
|
||||
|
||||
const slotPassthrough = {
|
||||
clearHeightCache,
|
||||
root,
|
||||
comment,
|
||||
more: showDetail,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<IfSlotIsNotEmpty
|
||||
slot={['adminCommentMoreDetails', 'adminCommentMoreFlagDetails']}
|
||||
passthrough={slotPassthrough}
|
||||
>
|
||||
<a onClick={this.toggleDetail} className={styles.moreDetail}>
|
||||
{showDetail ? t('modqueue.less_detail') : t('modqueue.more_detail')}
|
||||
</a>
|
||||
</IfSlotIsNotEmpty>
|
||||
<Slot fill="adminCommentDetailArea" passthrough={slotPassthrough} />
|
||||
{showDetail && (
|
||||
<Slot fill="adminCommentMoreDetails" passthrough={slotPassthrough} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CommentDetails.propTypes = {
|
||||
root: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object.isRequired,
|
||||
clearHeightCache: PropTypes.func,
|
||||
};
|
||||
|
||||
export default CommentDetails;
|
||||
@@ -1,27 +0,0 @@
|
||||
.root {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.coreLabels {
|
||||
> *:not(:last-child) {
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.slot {
|
||||
&:not(:empty) {
|
||||
padding-left: 3px;
|
||||
}
|
||||
> *:not(:last-child) {
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.replyLabel {
|
||||
background-color: #3D73D5;
|
||||
}
|
||||
|
||||
.premodLabel {
|
||||
background-color: #063B9A;
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Label from 'coral-ui/components/Label';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import { t } from 'coral-framework/services/i18n';
|
||||
import FlagLabel from 'coral-ui/components/FlagLabel';
|
||||
import cn from 'classnames';
|
||||
import styles from './CommentLabels.css';
|
||||
import { ADMIN, MODERATOR, STAFF } from 'coral-framework/constants/roles';
|
||||
|
||||
const staffRoles = [ADMIN, MODERATOR, STAFF];
|
||||
|
||||
function isUserFlagged(actions) {
|
||||
return actions.some(
|
||||
action => action.__typename === 'FlagAction' && action.user
|
||||
);
|
||||
}
|
||||
|
||||
function getUserFlaggedType(actions) {
|
||||
return actions.some(
|
||||
action =>
|
||||
action.__typename === 'FlagAction' &&
|
||||
action.user &&
|
||||
staffRoles.includes(action.user.role)
|
||||
)
|
||||
? 'Staff'
|
||||
: 'User';
|
||||
}
|
||||
|
||||
function hasSuspectedWords(actions) {
|
||||
return actions.some(
|
||||
action =>
|
||||
action.__typename === 'FlagAction' && action.reason === 'SUSPECT_WORD'
|
||||
);
|
||||
}
|
||||
|
||||
function hasHistoryFlag(actions) {
|
||||
return actions.some(
|
||||
action => action.__typename === 'FlagAction' && action.reason === 'TRUST'
|
||||
);
|
||||
}
|
||||
|
||||
const CommentLabels = ({
|
||||
comment,
|
||||
comment: { className, status, actions, hasParent },
|
||||
}) => {
|
||||
const slotPassthrough = {
|
||||
comment,
|
||||
};
|
||||
return (
|
||||
<div className={cn(className, styles.root)}>
|
||||
<div className={styles.coreLabels}>
|
||||
{hasParent && (
|
||||
<Label iconName="reply" className={styles.replyLabel}>
|
||||
reply
|
||||
</Label>
|
||||
)}
|
||||
{status === 'PREMOD' && (
|
||||
<Label iconName="query_builder" className={styles.premodLabel}>
|
||||
Pre-Mod
|
||||
</Label>
|
||||
)}
|
||||
{isUserFlagged(actions) && (
|
||||
<FlagLabel iconName="person">{getUserFlaggedType(actions)}</FlagLabel>
|
||||
)}
|
||||
{hasSuspectedWords(actions) && (
|
||||
<FlagLabel iconName="sms_failed">
|
||||
{t('flags.reasons.comment.suspect_word')}
|
||||
</FlagLabel>
|
||||
)}
|
||||
{hasHistoryFlag(actions) && (
|
||||
<FlagLabel iconName="sentiment_very_dissatisfied">
|
||||
{t('flags.reasons.comment.trust')}
|
||||
</FlagLabel>
|
||||
)}
|
||||
</div>
|
||||
<Slot
|
||||
className={styles.slot}
|
||||
fill="adminCommentLabels"
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CommentLabels.propTypes = {
|
||||
comment: PropTypes.shape({
|
||||
className: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
actions: PropTypes.array,
|
||||
hasParent: PropTypes.bool,
|
||||
}),
|
||||
};
|
||||
|
||||
export default CommentLabels;
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './CountBadge.css';
|
||||
import { humanizeNumber } from 'coral-framework/helpers/numbers';
|
||||
|
||||
const CountBadge = ({ count }) => {
|
||||
let number = count;
|
||||
|
||||
// shorten large counts to abbreviations
|
||||
number = humanizeNumber(number);
|
||||
|
||||
return <span className={styles.count}>{number}</span>;
|
||||
};
|
||||
|
||||
CountBadge.propTypes = {
|
||||
count: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export default CountBadge;
|
||||
@@ -1,13 +0,0 @@
|
||||
@custom-media --table-viewport (max-width: 1024px);
|
||||
|
||||
:global {
|
||||
.mdl-layout__drawer-button {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
@media (--table-viewport) {
|
||||
.mdl-layout__drawer-button {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Navigation, Drawer } from 'react-mdl';
|
||||
import { IndexLink, Link } from 'react-router';
|
||||
import styles from './Drawer.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import cn from 'classnames';
|
||||
|
||||
const CoralDrawer = ({ handleLogout, currentUser }) =>
|
||||
currentUser && can(currentUser, 'ACCESS_ADMIN') ? (
|
||||
<Drawer className={cn('talk-admin-drawer-nav', styles.drawer)}>
|
||||
<div>
|
||||
<Navigation className={styles.nav}>
|
||||
{can(currentUser, 'MODERATE_COMMENTS') && (
|
||||
<IndexLink
|
||||
className={cn('talk-admin-nav-moderate', styles.navLink)}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.moderate')}
|
||||
</IndexLink>
|
||||
)}
|
||||
<Link
|
||||
className={cn('talk-admin-nav-stories', styles.navLink)}
|
||||
to="/admin/stories"
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.stories')}
|
||||
</Link>
|
||||
<Link
|
||||
className={cn('talk-admin-nav-community', styles.navLink)}
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.community')}
|
||||
</Link>
|
||||
{can(currentUser, 'UPDATE_CONFIG') && (
|
||||
<Link
|
||||
className={cn('talk-admin-nav-configure', styles.navLink)}
|
||||
to="/admin/configure"
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.configure')}
|
||||
</Link>
|
||||
)}
|
||||
<a onClick={handleLogout}>{t('configure.sign_out')}</a>
|
||||
<span>{`v${process.env.VERSION}`}</span>
|
||||
</Navigation>
|
||||
</div>
|
||||
</Drawer>
|
||||
) : null;
|
||||
|
||||
CoralDrawer.propTypes = {
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
restricted: PropTypes.bool, // hide app elements from a logged out user
|
||||
currentUser: PropTypes.object,
|
||||
};
|
||||
|
||||
export default CoralDrawer;
|
||||
@@ -1,15 +1,14 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Card } from 'coral-ui';
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Card} from 'coral-ui';
|
||||
|
||||
const EmptyCard = props => (
|
||||
<Card style={{ textAlign: 'center', maxWidth: 400, margin: '0 auto' }}>
|
||||
const EmptyCard = (props) => (
|
||||
<Card style={{textAlign: 'center', maxWidth: 400, margin: '0 auto'}}>
|
||||
{props.children}
|
||||
</Card>
|
||||
);
|
||||
|
||||
EmptyCard.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
children: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
export default EmptyCard;
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
.external {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.separator h5 {
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.slot > * {
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './External.css';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty';
|
||||
|
||||
const External = ({ slot }) => (
|
||||
<IfSlotIsNotEmpty slot={slot}>
|
||||
<div>
|
||||
<div className={styles.external}>
|
||||
<Slot fill={slot} className={styles.slot} />
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h5>Or</h5>
|
||||
</div>
|
||||
</div>
|
||||
</IfSlotIsNotEmpty>
|
||||
);
|
||||
|
||||
External.propTypes = {
|
||||
slot: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default External;
|
||||
@@ -1,8 +0,0 @@
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.copy {
|
||||
padding: 20px 0;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './Forbidden.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const Forbidden = () => (
|
||||
<div className={styles.container}>
|
||||
<p className={styles.copy}>{t('error.PAGE_NOT_AVAILABLE_ROLE')}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Forbidden;
|
||||
@@ -1,20 +0,0 @@
|
||||
|
||||
.header, .cta, .success {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.success {
|
||||
cursor: pointer;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
|
||||
.signInLink {
|
||||
color: blue;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.signInLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './ForgotPassword.css';
|
||||
import { Button, TextField, Alert, Success } from 'coral-ui';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class ForgotPassword extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
handleEmailChange = e => this.props.onEmailChange(e.target.value);
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
handleSignInLink = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSignInLink();
|
||||
};
|
||||
|
||||
renderSuccess() {
|
||||
return (
|
||||
<div className={styles.success} onClick={this.handleSignInLink}>
|
||||
{t('password_reset.mail_sent')}{' '}
|
||||
<a
|
||||
className={styles.signInLink}
|
||||
href="#"
|
||||
onClick={this.handleSignInLink}
|
||||
>
|
||||
{t('login.sign_in')}
|
||||
</a>
|
||||
<Success />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderForm() {
|
||||
const { email, errorMessage } = this.props;
|
||||
return (
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
label={t('login.email_address')}
|
||||
value={email}
|
||||
onChange={this.handleEmailChange}
|
||||
/>
|
||||
<Button type="submit" cStyle="black" full>
|
||||
{t('login.reset_password_send_button')}
|
||||
</Button>
|
||||
<p className={styles.cta}>
|
||||
{t('login.go_back')}{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.signInLink}
|
||||
onClick={this.handleSignInLink}
|
||||
>
|
||||
{t('login.sign_in')}
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.success ? this.renderSuccess() : this.renderForm();
|
||||
}
|
||||
}
|
||||
|
||||
ForgotPassword.propTypes = {
|
||||
success: PropTypes.bool.isRequired,
|
||||
email: PropTypes.string.isRequired,
|
||||
onEmailChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
onSignInLink: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ForgotPassword;
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Layout } from 'react-mdl';
|
||||
import {Layout} from 'react-mdl';
|
||||
import styles from './FullLoading.css';
|
||||
import { CoralLogo } from 'coral-ui';
|
||||
import {CoralLogo} from 'coral-ui';
|
||||
|
||||
export const FullLoading = () => (
|
||||
<Layout fixedDrawer>
|
||||
<div className={styles.layout}>
|
||||
<div className={styles.layout} >
|
||||
<h1>Loading</h1>
|
||||
<CoralLogo />
|
||||
</div>
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Navigation, Header, IconButton, MenuItem, Menu } from 'react-mdl';
|
||||
import { Link, IndexLink } from 'react-router';
|
||||
import styles from './Header.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { Logo } from './Logo';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import CommunityIndicator from '../routes/Community/containers/Indicator';
|
||||
|
||||
const CoralHeader = ({
|
||||
handleLogout,
|
||||
showShortcuts = () => {},
|
||||
currentUser,
|
||||
root,
|
||||
data,
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.headerWrapper}>
|
||||
<Header className={styles.header}>
|
||||
<Logo className={styles.logo} />
|
||||
<div>
|
||||
{currentUser && can(currentUser, 'ACCESS_ADMIN') ? (
|
||||
<Navigation className={styles.nav}>
|
||||
{can(currentUser, 'MODERATE_COMMENTS') && (
|
||||
<IndexLink
|
||||
id="moderateNav"
|
||||
className={cn('talk-admin-nav-moderate', styles.navLink)}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.moderate')}
|
||||
</IndexLink>
|
||||
)}
|
||||
<Link
|
||||
id="storiesNav"
|
||||
className={cn('talk-admin-nav-stories', styles.navLink)}
|
||||
to="/admin/stories"
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.stories')}
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
id="communityNav"
|
||||
className={cn('talk-admin-nav-community', styles.navLink)}
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.community')}
|
||||
<CommunityIndicator root={root} data={data} />
|
||||
</Link>
|
||||
|
||||
{can(currentUser, 'UPDATE_CONFIG') && (
|
||||
<Link
|
||||
id="configureNav"
|
||||
className={cn('talk-admin-nav-configure', styles.navLink)}
|
||||
to="/admin/configure"
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.configure')}
|
||||
</Link>
|
||||
)}
|
||||
</Navigation>
|
||||
) : null}
|
||||
<div className={styles.rightPanel}>
|
||||
<ul>
|
||||
<li className={cn(styles.settings, 'talk-admin-header-settings')}>
|
||||
<div>
|
||||
<IconButton
|
||||
name="settings"
|
||||
id="menu-settings"
|
||||
className="talk-admin-header-settings-button"
|
||||
/>
|
||||
<Menu target="menu-settings" align="right">
|
||||
<MenuItem onClick={() => showShortcuts(true)}>
|
||||
{t('configure.shortcuts')}
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a
|
||||
href="https://docs.coralproject.net/talk/how-talk-works/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('configure.product_guide_link')}
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a
|
||||
href="https://github.com/coralproject/talk/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('configure.view_last_version')}
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a
|
||||
href="https://support.coralproject.net"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('configure.report_bug_or_feedback')}
|
||||
</a>
|
||||
</MenuItem>
|
||||
{currentUser && (
|
||||
<MenuItem
|
||||
onClick={handleLogout}
|
||||
className="talk-admin-header-sign-out"
|
||||
>
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
<li>{`v${process.env.VERSION}`}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Header>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CoralHeader.propTypes = {
|
||||
currentUser: PropTypes.object,
|
||||
showShortcuts: PropTypes.func,
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default CoralHeader;
|
||||
@@ -1,12 +0,0 @@
|
||||
import React from 'react';
|
||||
import matchLinks from 'coral-framework/utils/matchLinks';
|
||||
|
||||
export default ({ text, children }) => {
|
||||
const hasLinks = !!matchLinks(text);
|
||||
|
||||
if (!hasLinks) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return React.Children.only(children);
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
.indicator {
|
||||
display: inline-block;
|
||||
background-color: #E46D59;
|
||||
border-radius: 10px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-left: 7px;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './Indicator.css';
|
||||
|
||||
const Indicator = () => <span className={styles.indicator} />;
|
||||
|
||||
export default Indicator;
|
||||
@@ -1,103 +0,0 @@
|
||||
.karmaTooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 2px 4px 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
color: #0D5B8F;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color:rgba(0,0,0,0);
|
||||
|
||||
> i {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
.icon:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu {
|
||||
background-color: white;
|
||||
border: solid 1px #999;
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
position: absolute;
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
|
||||
z-index: 10;
|
||||
top: 32px;
|
||||
left: -100px;
|
||||
width: 150px;
|
||||
text-align: left;
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.menu::before{
|
||||
content: '';
|
||||
border: 10px solid transparent;
|
||||
border-top-color: #999;
|
||||
position: absolute;
|
||||
left: 96px;
|
||||
top: -20px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.menu::after{
|
||||
content: '';
|
||||
border: 10px solid transparent;
|
||||
border-top-color: white;
|
||||
position: absolute;
|
||||
left: 96px;
|
||||
top: -19px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.menu ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 4px 5px;
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
font-size: .9em;
|
||||
line-height: normal;
|
||||
letter-spacing: .4px;
|
||||
min-width: 25px;
|
||||
display: block;
|
||||
|
||||
/* &.reliable { background-color: #03AB61; } */
|
||||
/* &.neutral { background-color: #616161; } */
|
||||
&.unreliable { background-color: #F44336; }
|
||||
}
|
||||
|
||||
.descriptionList {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.strongItem {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.descriptionItem {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #2B7EB5;
|
||||
text-decoration: underline;
|
||||
display: block;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user