mirror of
https://github.com/wassname/talk.git
synced 2026-07-05 22:55:13 +08:00
Merge branch 'master' into bugs-fix
This commit is contained in:
+2
-1
@@ -53,7 +53,8 @@
|
||||
"no-lonely-if": [2],
|
||||
"curly": [2],
|
||||
"no-unused-vars": ["error", {
|
||||
"argsIgnorePattern": "next"
|
||||
"argsIgnorePattern": "^_|next",
|
||||
"varsIgnorePattern": "^_"
|
||||
}],
|
||||
"no-multiple-empty-lines": ["error", {
|
||||
"max": 1
|
||||
|
||||
@@ -12,6 +12,7 @@ program
|
||||
.command('assets', 'interact with assets')
|
||||
.command('setup', 'setup the application')
|
||||
.command('jobs', 'work with the job queues')
|
||||
.command('token', 'work with the access tokens')
|
||||
.command('users', 'work with the application auth')
|
||||
.command('migration', 'provides utilities for migrating the database')
|
||||
.command('plugins', 'provides utilities for interacting with the plugin system')
|
||||
|
||||
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const TokensService = require('../services/tokens');
|
||||
const util = require('./util');
|
||||
const Table = require('cli-table');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
async function listTokens(userID) {
|
||||
try {
|
||||
let tokens = await TokensService.list(userID);
|
||||
|
||||
let table = new Table({
|
||||
head: [
|
||||
'ID',
|
||||
'Name',
|
||||
'Status'
|
||||
]
|
||||
});
|
||||
|
||||
tokens.forEach((token) => {
|
||||
table.push([
|
||||
token.id,
|
||||
token.name,
|
||||
token.active ? 'Active' : 'Revoked'
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function revokeToken(tokenID) {
|
||||
try {
|
||||
|
||||
await TokensService.revoke(null, tokenID);
|
||||
|
||||
console.log(`Revoked Token[${tokenID}]`);
|
||||
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function createToken(userID, tokenName) {
|
||||
try {
|
||||
|
||||
let {pat: {id}, jwt} = await TokensService.create(userID, tokenName);
|
||||
|
||||
console.log(`Created Token[${id}] for User[${userID}] = ${jwt}`);
|
||||
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.command('list <userID>')
|
||||
.description('list tokens for a user')
|
||||
.action(listTokens);
|
||||
|
||||
program
|
||||
.command('revoke <tokenID>')
|
||||
.description('revokes a token with a given id')
|
||||
.action(revokeToken);
|
||||
|
||||
program
|
||||
.command('create <userID> <tokenName>')
|
||||
.description('create a token for a user with a given name')
|
||||
.action(createToken);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp();
|
||||
util.shutdown();
|
||||
}
|
||||
@@ -50,7 +50,7 @@ const routes = (
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
<Route path=':id' components={Moderation} />
|
||||
<IndexRedirect to='premod' />
|
||||
<IndexRedirect to='all' />
|
||||
</Route>
|
||||
</Route>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '../constants/assets';
|
||||
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
/**
|
||||
* Action disptacher related to assets
|
||||
@@ -19,12 +20,16 @@ import coralApi from '../../../coral-framework/helpers/request';
|
||||
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) => dispatch({type: FETCH_ASSETS_FAILURE, error}));
|
||||
.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
|
||||
@@ -32,9 +37,12 @@ export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filte
|
||||
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) => dispatch({type: UPDATE_ASSET_STATE_FAILURE, error}));
|
||||
.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) => {
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
@@ -41,13 +42,27 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch) =>
|
||||
dispatch(checkLoginSuccess(user));
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
|
||||
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: error.translation_key
|
||||
message: t(`error.${error.translation_key}`),
|
||||
});
|
||||
} else {
|
||||
dispatch({type: actions.LOGIN_FAILURE, message: error.translation_key});
|
||||
dispatch({
|
||||
type: actions.LOGIN_FAILURE,
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -56,25 +71,30 @@ export const handleLogin = (email, password, recaptchaResponse) => (dispatch) =>
|
||||
// FORGOT PASSWORD
|
||||
//==============================================================================
|
||||
|
||||
const forgotPassowordRequest = () => ({
|
||||
const forgotPasswordRequest = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_REQUEST
|
||||
});
|
||||
|
||||
const forgotPassowordSuccess = () => ({
|
||||
const forgotPasswordSuccess = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS
|
||||
});
|
||||
|
||||
const forgotPassowordFailure = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_FAILURE
|
||||
const forgotPasswordFailure = (error) => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const requestPasswordReset = (email) => (dispatch) => {
|
||||
dispatch(forgotPassowordRequest(email));
|
||||
dispatch(forgotPasswordRequest(email));
|
||||
const redirectUri = location.href;
|
||||
|
||||
return coralApi('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}})
|
||||
.then(() => dispatch(forgotPassowordSuccess()))
|
||||
.catch((error) => dispatch(forgotPassowordFailure(error)));
|
||||
.then(() => dispatch(forgotPasswordSuccess()))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(forgotPasswordFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
@@ -112,6 +132,7 @@ export const checkLogin = () => (dispatch) => {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
dispatch(checkLoginFailure(`${error.translation_key}`));
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(checkLoginFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from '../constants/community';
|
||||
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const fetchAccounts = (query = {}) => (dispatch) => {
|
||||
|
||||
@@ -30,7 +31,11 @@ export const fetchAccounts = (query = {}) => (dispatch) => {
|
||||
totalPages
|
||||
});
|
||||
})
|
||||
.catch((error) => dispatch({type: FETCH_COMMENTERS_FAILURE, error}));
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: FETCH_COMMENTERS_FAILURE, error: errorMessage});
|
||||
});
|
||||
};
|
||||
|
||||
const requestFetchAccounts = () => ({
|
||||
@@ -47,7 +52,6 @@ export const newPage = () => ({
|
||||
});
|
||||
|
||||
export const setRole = (id, role) => (dispatch) => {
|
||||
|
||||
return coralApi(`/users/${id}/role`, {method: 'POST', body: {role}})
|
||||
.then(() => {
|
||||
return dispatch({type: SET_ROLE, id, role});
|
||||
|
||||
@@ -2,6 +2,7 @@ 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});
|
||||
@@ -17,7 +18,8 @@ const clearErrors = () => ({type: actions.CLEAR_ERRORS});
|
||||
|
||||
const validation = (formData, dispatch, next) => {
|
||||
if (!(formData != null)) {
|
||||
return dispatch(hasError());
|
||||
dispatch(hasError());
|
||||
return;
|
||||
}
|
||||
|
||||
const validKeys = Object.keys(formData)
|
||||
@@ -40,7 +42,8 @@ const validation = (formData, dispatch, next) => {
|
||||
});
|
||||
|
||||
if (empty.length) {
|
||||
return dispatch(hasError());
|
||||
dispatch(hasError());
|
||||
return;
|
||||
}
|
||||
|
||||
// RegExp Validation
|
||||
@@ -59,7 +62,8 @@ const validation = (formData, dispatch, next) => {
|
||||
});
|
||||
|
||||
if (validation.length) {
|
||||
return dispatch(hasError());
|
||||
dispatch(hasError());
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(clearErrors());
|
||||
@@ -90,7 +94,8 @@ export const finishInstall = () => (dispatch, getState) => {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
dispatch(installFailure(`${error.translation_key}`));
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(installFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -113,6 +118,7 @@ export const checkInstall = (next) => (dispatch) => {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
dispatch(checkInstallFailure(`${error.translation_key}`));
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(checkInstallFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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';
|
||||
@@ -20,7 +21,9 @@ export const fetchSettings = () => (dispatch) => {
|
||||
dispatch({type: SETTINGS_RECEIVED, settings});
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch({type: SETTINGS_FETCH_ERROR, error});
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: SETTINGS_FETCH_ERROR, error: errorMessage});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -49,6 +52,8 @@ export const saveSettingsToServer = () => (dispatch, getState) => {
|
||||
dispatch({type: SAVE_SETTINGS_SUCCESS, settings});
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch({type: SAVE_SETTINGS_FAILED, error});
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: SAVE_SETTINGS_FAILED, error: errorMessage});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import * as userTypes from '../constants/users';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
/**
|
||||
* Action disptacher related to users
|
||||
@@ -10,7 +11,11 @@ export const userStatusUpdate = (status, userId, commentId) => {
|
||||
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) => dispatch({type: userTypes.UPDATE_STATUS_FAILURE, error}));
|
||||
.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});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -18,7 +23,11 @@ export const userStatusUpdate = (status, userId, commentId) => {
|
||||
export const sendNotificationEmail = (userId, subject, body) => {
|
||||
return (dispatch) => {
|
||||
return coralApi(`/users/${userId}/email`, {method: 'POST', body: {subject, body}})
|
||||
.catch((error) => dispatch({type: userTypes.USER_EMAIL_FAILURE, error}));
|
||||
.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});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -26,6 +35,10 @@ export const sendNotificationEmail = (userId, subject, body) => {
|
||||
export const enableUsernameEdit = (userId) => {
|
||||
return (dispatch) => {
|
||||
return coralApi(`/users/${userId}/username-enable`, {method: 'POST'})
|
||||
.catch((error) => dispatch({type: userTypes.USERNAME_ENABLE_FAILURE, error}));
|
||||
.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});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ 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 t from 'coral-framework/services/i18n';
|
||||
import Recaptcha from 'react-recaptcha';
|
||||
|
||||
class AdminLogin extends React.Component {
|
||||
@@ -35,7 +34,7 @@ class AdminLogin extends React.Component {
|
||||
const {errorMessage, loginMaxExceeded, recaptchaPublic} = this.props;
|
||||
const signInForm = (
|
||||
<form onSubmit={this.handleSignIn}>
|
||||
{errorMessage && <Alert>{t(`error.${errorMessage}`)}</Alert>}
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
label='Email Address'
|
||||
value={this.state.email}
|
||||
|
||||
@@ -84,12 +84,12 @@ const CoralHeader = ({
|
||||
<MenuItem onClick={handleLogout}>
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
Talk {`v${process.env.VERSION}`}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
{`v${process.env.VERSION}`}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
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);
|
||||
@@ -192,6 +192,7 @@ export default class Moderation extends Component {
|
||||
shortcutsNoteVisible={moderation.shortcutsNoteVisible}
|
||||
open={moderation.modalOpen}
|
||||
onClose={this.onClose}/>
|
||||
|
||||
{moderation.userDetailId && (
|
||||
<UserDetail
|
||||
id={moderation.userDetailId}
|
||||
@@ -203,7 +204,9 @@ export default class Moderation extends Component {
|
||||
acceptComment={props.acceptComment}
|
||||
rejectComment={props.rejectComment} />
|
||||
)}
|
||||
|
||||
<StorySearch
|
||||
assetId={assetId}
|
||||
moderation={this.props.moderation}
|
||||
closeSearch={this.closeSearch}
|
||||
storySearchChange={this.props.storySearchChange}
|
||||
|
||||
@@ -9,6 +9,7 @@ import t from 'coral-framework/services/i18n';
|
||||
import {CSSTransitionGroup} from 'react-transition-group';
|
||||
|
||||
class ModerationQueue extends React.Component {
|
||||
isLoadingMore = false;
|
||||
|
||||
static propTypes = {
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
@@ -23,7 +24,15 @@ class ModerationQueue extends React.Component {
|
||||
}
|
||||
|
||||
loadMore = () => {
|
||||
this.props.loadMore(this.props.activeTab);
|
||||
if (!this.isLoadingMore) {
|
||||
this.isLoadingMore = true;
|
||||
this.props.loadMore(this.props.activeTab)
|
||||
.then(() => this.isLoadingMore = false)
|
||||
.catch((e) => {
|
||||
this.isLoadingMore = false;
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
|
||||
@@ -49,12 +49,14 @@
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.cta {
|
||||
letter-spacing: 1px;
|
||||
.cta > a {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
margin: 0;
|
||||
height: 50px;
|
||||
box-sizing: border-box;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -38,7 +38,11 @@ const StorySearch = (props) => {
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.results}>
|
||||
<p className={styles.cta}>Moderate comments on All Stories</p>
|
||||
{props.assetId &&
|
||||
<div className={styles.cta}>
|
||||
<a onClick={props.goToModerateAll}>Moderate comments on All Stories</a>
|
||||
</div>
|
||||
}
|
||||
<div className={styles.storyList}>
|
||||
|
||||
{
|
||||
@@ -87,7 +91,8 @@ StorySearch.propTypes = {
|
||||
goToStory: PropTypes.func.isRequired,
|
||||
closeSearch: PropTypes.func.isRequired,
|
||||
moderation: PropTypes.object.isRequired,
|
||||
handleSearchChange: PropTypes.func.isRequired
|
||||
handleSearchChange: PropTypes.func.isRequired,
|
||||
assetId: PropTypes.string
|
||||
};
|
||||
|
||||
export default StorySearch;
|
||||
|
||||
@@ -35,9 +35,8 @@
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
font-size: 16px;
|
||||
position: absolute;
|
||||
width: 90%;
|
||||
outline: none;
|
||||
width: calc(100% - 90px);
|
||||
}
|
||||
|
||||
.commentStatuses {
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Button, Drawer, Copy} from 'coral-ui';
|
||||
import styles from './UserDetail.css';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import Comment from './Comment';
|
||||
import styles from './UserDetail.css';
|
||||
import {Button, Drawer} from 'coral-ui';
|
||||
import {Slot} from 'coral-framework/components';
|
||||
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
|
||||
import {actionsMap} from '../helpers/moderationQueueActionsMap';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
|
||||
export default class UserDetail extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
emailCopied: false
|
||||
};
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
hideUserDetail: PropTypes.func.isRequired,
|
||||
@@ -50,16 +45,6 @@ export default class UserDetail extends React.Component {
|
||||
this.props.changeStatus('rejected');
|
||||
}
|
||||
|
||||
showCopied() {
|
||||
this.setState({
|
||||
emailCopied: true
|
||||
}, () => {
|
||||
setTimeout(() => this.setState({
|
||||
emailCopied: false
|
||||
}), 3000);
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
root: {
|
||||
@@ -96,92 +81,92 @@ export default class UserDetail extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer handleClickOutside={hideUserDetail}>
|
||||
<h3>{user.username}</h3>
|
||||
{profile && <input className={styles.profileEmail} readOnly type="text" ref={(ref) => this.profile = ref} value={profile} />}
|
||||
<ClickOutside onClickOutside={hideUserDetail}>
|
||||
<Drawer onClose={hideUserDetail}>
|
||||
<h3>{user.username}</h3>
|
||||
|
||||
<Copy onCopy={() => this.showCopied()} text={profile} className={styles.profileEmail}>
|
||||
<Button className={styles.copyButton}>
|
||||
{this.state.emailCopied ? 'Copied!' : 'Copy'}
|
||||
</Button>
|
||||
</Copy>
|
||||
<div>
|
||||
{profile && <input className={styles.profileEmail} readOnly type="text" ref={(ref) => this.profile = ref} value={profile} />}
|
||||
<ButtonCopyToClipboard className={styles.copyButton} copyText={profile} />
|
||||
</div>
|
||||
|
||||
<Slot
|
||||
fill="userProfile"
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
user={user}
|
||||
/>
|
||||
<p className={styles.memberSince}><strong>Member since</strong> {new Date(user.created_at).toLocaleString()}</p>
|
||||
<hr/>
|
||||
<p>
|
||||
<strong>Account summary</strong>
|
||||
<br/><small className={styles.small}>Data represents the last six months of activity</small>
|
||||
</p>
|
||||
<div className={styles.stats}>
|
||||
<div className={styles.stat}>
|
||||
<p>Total Comments</p>
|
||||
<p>{totalComments}</p>
|
||||
</div>
|
||||
<div className={styles.stat}>
|
||||
<p>Reject Rate</p>
|
||||
<p>{`${(rejectedPercent).toFixed(1)}%`}</p>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
selectedIds.length === 0
|
||||
? (
|
||||
<ul className={styles.commentStatuses}>
|
||||
<li className={tab === 'all' ? styles.active : ''} onClick={this.showAll}>All</li>
|
||||
<li className={tab === 'rejected' ? styles.active : ''} onClick={this.showRejected}>Rejected</li>
|
||||
</ul>
|
||||
)
|
||||
: (
|
||||
<div className={styles.bulkActionGroup}>
|
||||
<Button
|
||||
onClick={bulkAccept}
|
||||
className={styles.bulkAction}
|
||||
cStyle='approve'
|
||||
icon='done'>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={bulkReject}
|
||||
className={styles.bulkAction}
|
||||
cStyle='reject'
|
||||
icon='close'>
|
||||
</Button>
|
||||
{`${selectedIds.length} comments selected`}
|
||||
<Slot
|
||||
fill="userProfile"
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
user={user}
|
||||
/>
|
||||
<p className={styles.memberSince}><strong>Member since</strong> {new Date(user.created_at).toLocaleString()}</p>
|
||||
<hr/>
|
||||
<p>
|
||||
<strong>Account summary</strong>
|
||||
<br/><small className={styles.small}>Data represents the last six months of activity</small>
|
||||
</p>
|
||||
<div className={styles.stats}>
|
||||
<div className={styles.stat}>
|
||||
<p>Total Comments</p>
|
||||
<p>{totalComments}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div>
|
||||
<div className={styles.stat}>
|
||||
<p>Reject Rate</p>
|
||||
<p>{`${(rejectedPercent).toFixed(1)}%`}</p>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
nodes.map((comment, i) => {
|
||||
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
|
||||
const selected = selectedIds.indexOf(comment.id) !== -1;
|
||||
return <Comment
|
||||
key={comment.id}
|
||||
index={i}
|
||||
comment={comment}
|
||||
selected={false}
|
||||
suspectWords={suspectWords}
|
||||
bannedWords={bannedWords}
|
||||
viewUserDetail={() => {}}
|
||||
actions={actionsMap[status]}
|
||||
showBanUserDialog={showBanUserDialog}
|
||||
showSuspendUserDialog={showSuspendUserDialog}
|
||||
acceptComment={this.acceptThenReload}
|
||||
rejectComment={this.rejectThenReload}
|
||||
selected={selected}
|
||||
toggleSelect={toggleSelect}
|
||||
currentAsset={null}
|
||||
currentUserId={this.props.id}
|
||||
minimal={true} />;
|
||||
})
|
||||
selectedIds.length === 0
|
||||
? (
|
||||
<ul className={styles.commentStatuses}>
|
||||
<li className={tab === 'all' ? styles.active : ''} onClick={this.showAll}>All</li>
|
||||
<li className={tab === 'rejected' ? styles.active : ''} onClick={this.showRejected}>Rejected</li>
|
||||
</ul>
|
||||
)
|
||||
: (
|
||||
<div className={styles.bulkActionGroup}>
|
||||
<Button
|
||||
onClick={bulkAccept}
|
||||
className={styles.bulkAction}
|
||||
cStyle='approve'
|
||||
icon='done'>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={bulkReject}
|
||||
className={styles.bulkAction}
|
||||
cStyle='reject'
|
||||
icon='close'>
|
||||
</Button>
|
||||
{`${selectedIds.length} comments selected`}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Drawer>
|
||||
|
||||
<div>
|
||||
{
|
||||
nodes.map((comment, i) => {
|
||||
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
|
||||
const selected = selectedIds.indexOf(comment.id) !== -1;
|
||||
return <Comment
|
||||
key={comment.id}
|
||||
index={i}
|
||||
comment={comment}
|
||||
selected={false}
|
||||
suspectWords={suspectWords}
|
||||
bannedWords={bannedWords}
|
||||
viewUserDetail={() => {}}
|
||||
actions={actionsMap[status]}
|
||||
showBanUserDialog={showBanUserDialog}
|
||||
showSuspendUserDialog={showSuspendUserDialog}
|
||||
acceptComment={this.acceptThenReload}
|
||||
rejectComment={this.rejectThenReload}
|
||||
selected={selected}
|
||||
toggleSelect={toggleSelect}
|
||||
currentAsset={null}
|
||||
currentUserId={this.props.id}
|
||||
minimal={true} />;
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</Drawer>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,8 +190,6 @@ span {
|
||||
}
|
||||
|
||||
&.selected {
|
||||
max-width: 720px;
|
||||
max-height: 410px;
|
||||
}
|
||||
|
||||
.context {
|
||||
|
||||
@@ -41,7 +41,13 @@ class StorySearchContainer extends React.Component {
|
||||
|
||||
goToStory = (id) => {
|
||||
const {router, closeSearch} = this.props;
|
||||
router.push(`/admin/moderate/${id}`);
|
||||
router.push(`/admin/moderate/all/${id}`);
|
||||
closeSearch();
|
||||
}
|
||||
|
||||
goToModerateAll = () => {
|
||||
const {router, closeSearch} = this.props;
|
||||
router.push('/admin/moderate/all');
|
||||
closeSearch();
|
||||
}
|
||||
|
||||
@@ -50,6 +56,7 @@ class StorySearchContainer extends React.Component {
|
||||
<StorySearch
|
||||
search={this.search}
|
||||
goToStory={this.goToStory}
|
||||
goToModerateAll={this.goToModerateAll}
|
||||
handleEsc={this.handleEsc}
|
||||
handleEnter={this.handleEnter}
|
||||
searchValue={this.state.searchValue}
|
||||
|
||||
@@ -76,6 +76,8 @@ const ActionButton = ({children}) => {
|
||||
};
|
||||
|
||||
export default class Comment extends React.Component {
|
||||
isLoadingReplies = false;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -211,14 +213,24 @@ export default class Comment extends React.Component {
|
||||
}
|
||||
|
||||
loadNewReplies = () => {
|
||||
const {replies, replyCount, id} = this.props.comment;
|
||||
if (replyCount > replies.nodes.length) {
|
||||
this.props.loadMore(id).then(() => {
|
||||
this.setState(resetCursors(this.state, this.props));
|
||||
});
|
||||
return;
|
||||
if (!this.isLoadingReplies) {
|
||||
this.isLoadingReplies = true;
|
||||
const {replies, replyCount, id} = this.props.comment;
|
||||
if (replyCount > replies.nodes.length) {
|
||||
this.props.loadMore(id)
|
||||
.then(() => {
|
||||
this.setState(resetCursors(this.state, this.props));
|
||||
this.isLoadingReplies = false;
|
||||
})
|
||||
.catch((e) => {
|
||||
this.isLoadingReplies = false;
|
||||
throw e;
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState(resetCursors);
|
||||
this.isLoadingReplies = false;
|
||||
}
|
||||
this.setState(resetCursors);
|
||||
};
|
||||
|
||||
showReplyBox = () => {
|
||||
@@ -432,28 +444,28 @@ export default class Comment extends React.Component {
|
||||
inline
|
||||
/>
|
||||
|
||||
{ (currentUser &&
|
||||
(comment.user.id === currentUser.id))
|
||||
{ (currentUser && (comment.user.id === currentUser.id)) &&
|
||||
|
||||
/* User can edit/delete their own comment for a short window after posting */
|
||||
? <span className={cn(styles.topRight)}>
|
||||
{
|
||||
commentIsStillEditable(comment) &&
|
||||
<a
|
||||
className={cn(styles.link, {[styles.active]: this.state.isEditing})}
|
||||
onClick={this.onClickEdit}>Edit</a>
|
||||
}
|
||||
</span>
|
||||
/* User can edit/delete their own comment for a short window after posting */
|
||||
<span className={cn(styles.topRight)}>
|
||||
{
|
||||
commentIsStillEditable(comment) &&
|
||||
<a
|
||||
className={cn(styles.link, {[styles.active]: this.state.isEditing})}
|
||||
onClick={this.onClickEdit}>Edit</a>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
{ (currentUser && (comment.user.id !== currentUser.id)) &&
|
||||
|
||||
/* TopRightMenu allows currentUser to ignore other users' comments */
|
||||
: <span className={cn(styles.topRight, styles.topRightMenu)}>
|
||||
<TopRightMenu
|
||||
comment={comment}
|
||||
ignoreUser={ignoreUser}
|
||||
addNotification={addNotification} />
|
||||
</span>
|
||||
<span className={cn(styles.topRight, styles.topRightMenu)}>
|
||||
<TopRightMenu
|
||||
comment={comment}
|
||||
ignoreUser={ignoreUser}
|
||||
addNotification={addNotification} />
|
||||
</span>
|
||||
}
|
||||
|
||||
{
|
||||
this.state.isEditing
|
||||
? <EditableCommentContent
|
||||
|
||||
@@ -80,14 +80,17 @@ export class EditableCommentContent extends React.Component {
|
||||
}
|
||||
successfullyEdited = true;
|
||||
} catch (error) {
|
||||
if (error.translation_key) {
|
||||
addNotification('error', t(`error.${error.translation_key}`));
|
||||
} else if (error.networkError) {
|
||||
addNotification('error', t('error.network_error'));
|
||||
} else {
|
||||
addNotification('error', t('edit_comment.unexpected_error'));
|
||||
throw error;
|
||||
}
|
||||
const errors = error.errors || [error];
|
||||
errors.forEach((e) => {
|
||||
if (e.translation_key) {
|
||||
addNotification('error', t(`error.${e.translation_key}`));
|
||||
} else if (error.networkError) {
|
||||
addNotification('error', t('error.network_error'));
|
||||
} else {
|
||||
addNotification('error', t('edit_comment.unexpected_error'));
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (successfullyEdited) {
|
||||
const status = response.data.editComment.comment.status;
|
||||
|
||||
@@ -23,15 +23,17 @@ class LoadMore extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
loadMore = () => {
|
||||
this.initialState = false;
|
||||
this.props.loadMore();
|
||||
}
|
||||
|
||||
render () {
|
||||
const {topLevel, moreComments, loadMore, replyCount} = this.props;
|
||||
const {topLevel, moreComments, replyCount} = this.props;
|
||||
return moreComments
|
||||
? <div className='coral-load-more'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.initialState = false;
|
||||
loadMore();
|
||||
}}>
|
||||
onClick={this.loadMore}>
|
||||
{topLevel ? t('framework.view_more_comments') : this.replyCountFormat(replyCount)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -53,6 +53,8 @@ function invalidateCursor(invalidated, state, props) {
|
||||
|
||||
class Stream extends React.Component {
|
||||
|
||||
isLoadingMore = false;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = resetCursors(this.state, props);
|
||||
@@ -95,6 +97,18 @@ class Stream extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
loadMoreComments = () => {
|
||||
if (!this.isLoadingMore) {
|
||||
this.isLoadingMore = true;
|
||||
this.props.loadMoreComments()
|
||||
.then(() => this.isLoadingMore = false)
|
||||
.catch((e) => {
|
||||
this.isLoadingMore = false;
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// getVisibileComments returns a list containing comments
|
||||
// which were authored by current user or comes after the `idCursor`.
|
||||
getVisibleComments() {
|
||||
@@ -183,6 +197,7 @@ class Stream extends React.Component {
|
||||
<SuspendedAccount
|
||||
canEditName={user && user.canEditName}
|
||||
editName={editName}
|
||||
currentUsername={user.username}
|
||||
/>}
|
||||
{loggedIn &&
|
||||
!banned &&
|
||||
@@ -288,7 +303,7 @@ class Stream extends React.Component {
|
||||
<LoadMore
|
||||
topLevel={true}
|
||||
moreComments={asset.comments.hasNextPage}
|
||||
loadMore={this.props.loadMoreComments}
|
||||
loadMore={this.loadMoreComments}
|
||||
/>
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,8 @@ class SuspendedAccount extends Component {
|
||||
|
||||
static propTypes = {
|
||||
canEditName: PropTypes.bool,
|
||||
editName: PropTypes.func.isRequired
|
||||
editName: PropTypes.func.isRequired,
|
||||
currentUsername: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
state = {
|
||||
@@ -21,7 +22,11 @@ class SuspendedAccount extends Component {
|
||||
const {editName} = this.props;
|
||||
const {username} = this.state;
|
||||
e.preventDefault();
|
||||
if (validate.username(username)) {
|
||||
|
||||
if (username === this.props.currentUsername) {
|
||||
this.setState({alert: t('error.SAME_USERNAME_PROVIDED')});
|
||||
}
|
||||
else if (validate.username(username)) {
|
||||
editName(username)
|
||||
.then(() => location.reload())
|
||||
.catch((error) => {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
import styles from './Toggleable.css';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const upArrow = <span className={classnames(styles.chevron, styles.up)}></span>;
|
||||
const downArrow = <span className={classnames(styles.chevron, styles.down)}></span>;
|
||||
|
||||
export default class Toggleable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
toggle = () => {
|
||||
this.setState({isOpen: !this.state.isOpen});
|
||||
}
|
||||
|
||||
close = () => {
|
||||
this.setState({isOpen: false});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {children} = this.props;
|
||||
const {isOpen} = this.state;
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.close}>
|
||||
<span className={styles.Toggleable} tabIndex="0" >
|
||||
<span className={styles.toggler}
|
||||
onClick={this.toggle}>{isOpen ? upArrow : downArrow}</span>
|
||||
{isOpen ? children : null}
|
||||
</span>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import {IgnoreUserWizard} from './IgnoreUserWizard';
|
||||
import styles from './TopRightMenu.css';
|
||||
import Toggleable from './Toggleable';
|
||||
|
||||
// TopRightMenu appears as a dropdown in the top right of the comment.
|
||||
// when you click the down cehvron, it expands and shows IgnoreUserWizard
|
||||
@@ -56,38 +54,6 @@ export class TopRightMenu extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
</Toggleable>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const upArrow = <span className={classnames(styles.chevron, styles.up)}></span>;
|
||||
const downArrow = <span className={classnames(styles.chevron, styles.down)}></span>;
|
||||
class Toggleable extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.toggle = this.toggle.bind(this);
|
||||
this.close = this.close.bind(this);
|
||||
this.state = {
|
||||
isOpen: false
|
||||
};
|
||||
}
|
||||
toggle() {
|
||||
this.setState({isOpen: !this.state.isOpen});
|
||||
}
|
||||
close() {
|
||||
this.setState({isOpen: false});
|
||||
}
|
||||
render() {
|
||||
const {children} = this.props;
|
||||
const {isOpen} = this.state;
|
||||
return (
|
||||
|
||||
// /*onBlur={ this.close } */
|
||||
<span className={styles.Toggleable} tabIndex="0" >
|
||||
<span className={styles.toggler}
|
||||
onClick={this.toggle}>{isOpen ? upArrow : downArrow}</span>
|
||||
{isOpen ? children : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,11 @@ function configurePymParent(pymParent, opts) {
|
||||
|
||||
window.document.body.appendChild(snackbar);
|
||||
|
||||
// Notify embed that there was a click outside.
|
||||
document.addEventListener('click', () => {
|
||||
pymParent.sendMessage('click');
|
||||
}, true);
|
||||
|
||||
// Workaround: IOS Safari ignores `width` but respects `min-width` value.
|
||||
pymParent.el.firstChild.style.width = '1px';
|
||||
pymParent.el.firstChild.style.minWidth = '100%';
|
||||
|
||||
@@ -10,7 +10,7 @@ export const fetchAssetFailure = (error) => ({type: actions.FETCH_ASSET_FAILURE,
|
||||
|
||||
const updateAssetSettingsRequest = () => ({type: actions.UPDATE_ASSET_SETTINGS_REQUEST});
|
||||
const updateAssetSettingsSuccess = (settings) => ({type: actions.UPDATE_ASSET_SETTINGS_SUCCESS, settings});
|
||||
const updateAssetSettingsFailure = () => ({type: actions.UPDATE_ASSET_SETTINGS_FAILURE});
|
||||
const updateAssetSettingsFailure = (error) => ({type: actions.UPDATE_ASSET_SETTINGS_FAILURE, error});
|
||||
|
||||
export const updateConfiguration = (newConfig) => (dispatch, getState) => {
|
||||
const assetId = getState().asset.toJS().id;
|
||||
@@ -20,7 +20,10 @@ export const updateConfiguration = (newConfig) => (dispatch, getState) => {
|
||||
dispatch(addNotification('success', t('framework.success_update_settings')));
|
||||
dispatch(updateAssetSettingsSuccess(newConfig));
|
||||
})
|
||||
.catch((error) => dispatch(updateAssetSettingsFailure(error)));
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
dispatch(updateAssetSettingsFailure(error));
|
||||
});
|
||||
};
|
||||
|
||||
export const updateOpenStream = (closedBody) => (dispatch, getState) => {
|
||||
@@ -31,7 +34,10 @@ export const updateOpenStream = (closedBody) => (dispatch, getState) => {
|
||||
dispatch(addNotification('success', t('framework.success_update_settings')));
|
||||
dispatch(fetchAssetSuccess(closedBody));
|
||||
})
|
||||
.catch((error) => dispatch(fetchAssetFailure(error)));
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
dispatch(fetchAssetFailure(error));
|
||||
});
|
||||
};
|
||||
|
||||
const openStream = () => ({type: actions.OPEN_COMMENTS});
|
||||
|
||||
@@ -75,7 +75,9 @@ export const createUsername = (userId, formData) => (dispatch) => {
|
||||
dispatch(updateUsername(formData));
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch(createUsernameFailure(t(`error.${error.translation_key}`)));
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(createUsernameFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -139,16 +141,20 @@ export const fetchSignIn = (formData) => {
|
||||
dispatch(hideSignInDialog());
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
if (error.metadata) {
|
||||
|
||||
// the user might not have a valid email. prompt the user user re-request the confirmation email
|
||||
dispatch(
|
||||
signInFailure(t('error.email_not_verified', error.metadata))
|
||||
);
|
||||
} else {
|
||||
} else if (error.translation_key === 'NOT_AUTHORIZED') {
|
||||
|
||||
// invalid credentials
|
||||
dispatch(signInFailure(t('error.email_password')));
|
||||
dispatch(signInFailure(t('error.email_password'), error.metadata));
|
||||
} else {
|
||||
const str = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(signInFailure(str));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -234,12 +240,8 @@ export const fetchSignUp = (formData) => (dispatch, getState) => {
|
||||
dispatch(signUpSuccess(user));
|
||||
})
|
||||
.catch((error) => {
|
||||
let errorMessage = t(`error.${error.message}`);
|
||||
|
||||
// if there is no translation defined, just show the error string
|
||||
if (errorMessage === `error.${error.message}`) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(signUpFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
@@ -256,8 +258,9 @@ const forgotPasswordSuccess = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS
|
||||
});
|
||||
|
||||
const forgotPasswordFailure = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_FAILURE
|
||||
const forgotPasswordFailure = (error) => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchForgotPassword = (email) => (dispatch, getState) => {
|
||||
@@ -268,7 +271,11 @@ export const fetchForgotPassword = (email) => (dispatch, getState) => {
|
||||
body: {email, loc: redirectUri}
|
||||
})
|
||||
.then(() => dispatch(forgotPasswordSuccess()))
|
||||
.catch((error) => dispatch(forgotPasswordFailure(error)));
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(forgotPasswordFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
@@ -326,7 +333,8 @@ export const checkLogin = () => (dispatch) => {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
dispatch(checkLoginFailure(`${error.translation_key}`));
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(checkLoginFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -360,11 +368,10 @@ export const requestConfirmEmail = (email) => (dispatch, getState) => {
|
||||
.then(() => {
|
||||
dispatch(verifyEmailSuccess());
|
||||
})
|
||||
.catch((err) => {
|
||||
|
||||
// email might have already been verifyed
|
||||
dispatch(verifyEmailFailure(err));
|
||||
throw err;
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(verifyEmailFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ export const editName = (username) => (dispatch) => {
|
||||
dispatch(addNotification('success', t('framework.success_name_update')));
|
||||
})
|
||||
.catch((error) => {
|
||||
dispatch(editUsernameFailure(t(`error.${error.translation_key}`)));
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(editUsernameFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import {Component, cloneElement, Children} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import {pym} from 'coral-framework';
|
||||
|
||||
export default class ClickOutside extends Component {
|
||||
static propTypes = {
|
||||
onClickOutside: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
domNode = null;
|
||||
|
||||
handleClick = (e) => {
|
||||
const {onClickOutside} = this.props;
|
||||
if (!e || !this.domNode.contains(e.target)) {
|
||||
onClickOutside(e);
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.domNode = findDOMNode(this);
|
||||
document.addEventListener('click', this.handleClick, true);
|
||||
pym.onMessage('click', this.handleClick);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('click', this.handleClick, true);
|
||||
pym.messageHandlers.click = pym.messageHandlers.click.filter((h) => h !== this.handleClick);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {children, onClickOutside: _, ...rest} = this.props;
|
||||
return cloneElement(Children.only(children), rest);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export {default as withFragments} from './withFragments';
|
||||
export {default as withMutation} from './withMutation';
|
||||
export {default as withQuery} from './withQuery';
|
||||
|
||||
export {default as withCopyToClipboard} from './withCopyToClipboard';
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Clipboard from 'clipboard';
|
||||
|
||||
export default (WrappedComponent) => {
|
||||
class WithCopyToClipboard extends React.Component {
|
||||
componentDidMount() {
|
||||
const clipboard = new Clipboard(ReactDOM.findDOMNode(this));
|
||||
|
||||
clipboard.on('success', (e) => {
|
||||
e.clearSelection();
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {copyTarget = '', copyText = '', className = '', ...rest} = this.props;
|
||||
|
||||
return <WrappedComponent
|
||||
className={className}
|
||||
data-clipboard-action="copy"
|
||||
data-clipboard-text={copyText}
|
||||
data-clipboard-target={copyTarget}
|
||||
{...rest}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
return WithCopyToClipboard;
|
||||
};
|
||||
@@ -20,7 +20,7 @@ describe('CommentBox', () => {
|
||||
});
|
||||
|
||||
it('should render the CommentBox appropriately', () => {
|
||||
expect(render.contains('<div class="CommentBox"')).to.be.truthy;
|
||||
expect(render.contains('<button class="postCommentButton"')).to.be.truthy;
|
||||
expect(render.contains('<div class="CommentBox"')).to.be.true;
|
||||
expect(render.contains('<button class="postCommentButton"')).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,11 +4,11 @@ import t from 'coral-framework/services/i18n';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
|
||||
import {PopupMenu, Button} from 'coral-ui';
|
||||
import onClickOutside from 'react-onclickoutside';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
|
||||
const name = 'coral-plugin-flags';
|
||||
|
||||
class FlagButton extends Component {
|
||||
export default class FlagButton extends Component {
|
||||
|
||||
state = {
|
||||
showMenu: false,
|
||||
@@ -128,7 +128,7 @@ class FlagButton extends Component {
|
||||
this.setState({message: e.target.value});
|
||||
}
|
||||
|
||||
handleClickOutside () {
|
||||
handleClickOutside = () => {
|
||||
this.closeMenu();
|
||||
}
|
||||
|
||||
@@ -138,79 +138,81 @@ class FlagButton extends Component {
|
||||
const flagged = flaggedByCurrentUser || localPost;
|
||||
const popupMenu = getPopupMenu[this.state.step](this.state.itemType);
|
||||
|
||||
return <div className={`${name}-container`}>
|
||||
<button
|
||||
ref={(ref) => this.flagButton = ref}
|
||||
onClick={!this.props.banned && !flaggedByCurrentUser && !localPost ? this.onReportClick : null}
|
||||
className={`${name}-button`}>
|
||||
{
|
||||
flagged
|
||||
? <span className={`${name}-button-text`}>{t('reported')}</span>
|
||||
: <span className={`${name}-button-text`}>{t('report')}</span>
|
||||
}
|
||||
<i className={`${name}-icon material-icons ${flagged && 'flaggedIcon'}`}
|
||||
style={flagged ? styles.flaggedIcon : {}}
|
||||
aria-hidden={true}>flag</i>
|
||||
</button>
|
||||
{
|
||||
this.state.showMenu &&
|
||||
<div className={`${name}-popup`} ref={(ref) => this.popup = ref}>
|
||||
<PopupMenu>
|
||||
<div className={`${name}-popup-header`}>{popupMenu.header}</div>
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.handleClickOutside}>
|
||||
<div className={`${name}-container`}>
|
||||
<button
|
||||
ref={(ref) => this.flagButton = ref}
|
||||
onClick={!this.props.banned && !flaggedByCurrentUser && !localPost ? this.onReportClick : null}
|
||||
className={`${name}-button`}>
|
||||
{
|
||||
popupMenu.text &&
|
||||
<div className={`${name}-popup-text`}>{popupMenu.text}</div>
|
||||
flagged
|
||||
? <span className={`${name}-button-text`}>{t('reported')}</span>
|
||||
: <span className={`${name}-button-text`}>{t('report')}</span>
|
||||
}
|
||||
{
|
||||
popupMenu.options && <form className={`${name}-popup-form`}>
|
||||
<i className={`${name}-icon material-icons ${flagged && 'flaggedIcon'}`}
|
||||
style={flagged ? styles.flaggedIcon : {}}
|
||||
aria-hidden={true}>flag</i>
|
||||
</button>
|
||||
{
|
||||
this.state.showMenu &&
|
||||
<div className={`${name}-popup`} ref={(ref) => this.popup = ref}>
|
||||
<PopupMenu>
|
||||
<div className={`${name}-popup-header`}>{popupMenu.header}</div>
|
||||
{
|
||||
popupMenu.options.map((option) =>
|
||||
<div key={option.val}>
|
||||
<input
|
||||
className={`${name}-popup-radio`}
|
||||
type="radio"
|
||||
id={option.val}
|
||||
checked={this.state[popupMenu.sets] === option.val}
|
||||
onClick={this.onPopupOptionClick(popupMenu.sets)}
|
||||
value={option.val}/>
|
||||
<label htmlFor={option.val} className={`${name}-popup-radio-label`}>{option.text}</label><br/>
|
||||
</div>
|
||||
)
|
||||
popupMenu.text &&
|
||||
<div className={`${name}-popup-text`}>{popupMenu.text}</div>
|
||||
}
|
||||
{
|
||||
this.state.reason && <div>
|
||||
<label htmlFor={'message'} className={`${name}-popup-radio-label`}>
|
||||
{t('flag_reason')}
|
||||
</label><br/>
|
||||
<textarea
|
||||
className={`${name}-reason-text`}
|
||||
id="message"
|
||||
rows={4}
|
||||
onChange={this.onNoteTextChange}
|
||||
value={this.state.message}/>
|
||||
</div>
|
||||
popupMenu.options && <form className={`${name}-popup-form`}>
|
||||
{
|
||||
popupMenu.options.map((option) =>
|
||||
<div key={option.val}>
|
||||
<input
|
||||
className={`${name}-popup-radio`}
|
||||
type="radio"
|
||||
id={option.val}
|
||||
checked={this.state[popupMenu.sets] === option.val}
|
||||
onClick={this.onPopupOptionClick(popupMenu.sets)}
|
||||
value={option.val}/>
|
||||
<label htmlFor={option.val} className={`${name}-popup-radio-label`}>{option.text}</label><br/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
this.state.reason && <div>
|
||||
<label htmlFor={'message'} className={`${name}-popup-radio-label`}>
|
||||
{t('flag_reason')}
|
||||
</label><br/>
|
||||
<textarea
|
||||
className={`${name}-reason-text`}
|
||||
id="message"
|
||||
rows={4}
|
||||
onChange={this.onNoteTextChange}
|
||||
value={this.state.message}/>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
}
|
||||
</form>
|
||||
}
|
||||
<div className={`${name}-popup-counter`}>
|
||||
{this.state.step + 1} of {getPopupMenu.length}
|
||||
<div className={`${name}-popup-counter`}>
|
||||
{this.state.step + 1} of {getPopupMenu.length}
|
||||
</div>
|
||||
{
|
||||
popupMenu.button && <Button
|
||||
className={`${name}-popup-button`}
|
||||
onClick={this.onPopupContinue}>
|
||||
{popupMenu.button}
|
||||
</Button>
|
||||
}
|
||||
</PopupMenu>
|
||||
</div>
|
||||
{
|
||||
popupMenu.button && <Button
|
||||
className={`${name}-popup-button`}
|
||||
onClick={this.onPopupContinue}>
|
||||
{popupMenu.button}
|
||||
</Button>
|
||||
}
|
||||
</PopupMenu>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>;
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default onClickOutside(FlagButton);
|
||||
|
||||
const styles = {
|
||||
flaggedIcon: {
|
||||
color: '#F00'
|
||||
|
||||
@@ -5,7 +5,7 @@ const renderer = new marked.Renderer();
|
||||
|
||||
// Set link target to `_parent` to work properly in an embed.
|
||||
renderer.link = (href, title, text) =>
|
||||
`<a target="_parent" href="${href}" title="${title}">${text}</a>`;
|
||||
`<a target="_parent" href="${href}" ${title ? `title="${title}"` : ''}>${text}</a>`;
|
||||
|
||||
marked.setOptions({renderer});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import t from 'coral-framework/services/i18n';
|
||||
|
||||
const ModerationLink = (props) => props.isAdmin ? (
|
||||
<div className={styles.moderationLink}>
|
||||
<a href={`/admin/moderate/${props.assetId}`} target="_blank">
|
||||
<a href={`/admin/moderate/all/${props.assetId}`} target="_blank">
|
||||
{t('moderate_this_stream')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import onClickOutside from 'react-onclickoutside';
|
||||
const name = 'coral-plugin-permalinks';
|
||||
import {Button} from 'coral-ui';
|
||||
import styles from './styles.css';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class PermalinkButton extends React.Component {
|
||||
export default class PermalinkButton extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
articleURL: PropTypes.string.isRequired,
|
||||
@@ -28,7 +28,7 @@ class PermalinkButton extends React.Component {
|
||||
this.setState({popoverOpen: !this.state.popoverOpen});
|
||||
}
|
||||
|
||||
handleClickOutside () {
|
||||
handleClickOutside = () => {
|
||||
this.setState({popoverOpen: false});
|
||||
}
|
||||
|
||||
@@ -49,33 +49,33 @@ class PermalinkButton extends React.Component {
|
||||
render () {
|
||||
const {copySuccessful, copyFailure} = this.state;
|
||||
return (
|
||||
<div className={`${name}-container`}>
|
||||
<button
|
||||
ref={(ref) => this.linkButton = ref}
|
||||
onClick={this.toggle}
|
||||
className={`${name}-button`}>
|
||||
{t('permalink')}
|
||||
<i className={`${name}-icon material-icons`} aria-hidden={true}>link</i>
|
||||
</button>
|
||||
<div
|
||||
ref={(ref) => this.popover = ref}
|
||||
className={`${name}-popover ${styles.container} ${this.state.popoverOpen ? 'active' : ''}`}>
|
||||
<input
|
||||
className={`${name}-copy-field`}
|
||||
type='text'
|
||||
ref={(input) => this.permalinkInput = input}
|
||||
value={`${this.props.articleURL}#${this.props.commentId}`}
|
||||
onChange={() => {}} />
|
||||
<Button className={`${name}-copy-button ${copySuccessful ? styles.success : ''} ${copyFailure ? styles.failure : ''}`}
|
||||
onClick={this.copyPermalink} >
|
||||
{!copyFailure && !copySuccessful && 'Copy'}
|
||||
{copySuccessful && 'Copied'}
|
||||
{copyFailure && 'Not supported'}
|
||||
</Button>
|
||||
<ClickOutside onClickOutside={this.handleClickOutside}>
|
||||
<div className={`${name}-container`}>
|
||||
<button
|
||||
ref={(ref) => this.linkButton = ref}
|
||||
onClick={this.toggle}
|
||||
className={`${name}-button`}>
|
||||
{t('permalink')}
|
||||
<i className={`${name}-icon material-icons`} aria-hidden={true}>link</i>
|
||||
</button>
|
||||
<div
|
||||
ref={(ref) => this.popover = ref}
|
||||
className={`${name}-popover ${styles.container} ${this.state.popoverOpen ? 'active' : ''}`}>
|
||||
<input
|
||||
className={`${name}-copy-field`}
|
||||
type='text'
|
||||
ref={(input) => this.permalinkInput = input}
|
||||
value={`${this.props.articleURL}#${this.props.commentId}`}
|
||||
onChange={() => {}} />
|
||||
<Button className={`${name}-copy-button ${copySuccessful ? styles.success : ''} ${copyFailure ? styles.failure : ''}`}
|
||||
onClick={this.copyPermalink} >
|
||||
{!copyFailure && !copySuccessful && 'Copy'}
|
||||
{copySuccessful && 'Copied'}
|
||||
{copyFailure && 'Not supported'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default onClickOutside(PermalinkButton);
|
||||
|
||||
@@ -53,6 +53,16 @@
|
||||
color: #212121;
|
||||
}
|
||||
|
||||
.type--local:hover {
|
||||
background: #d6d5d5;
|
||||
color: #212121;
|
||||
}
|
||||
|
||||
.type--local:active {
|
||||
background: #cccccc;
|
||||
color: #212121;
|
||||
}
|
||||
|
||||
.type--facebook {
|
||||
background-color: #4267b2;
|
||||
border-color: #4267b2;
|
||||
|
||||
@@ -2,20 +2,23 @@ import React from 'react';
|
||||
import styles from './Button.css';
|
||||
import Icon from './Icon';
|
||||
|
||||
const Button = ({cStyle = 'local', children, className, raised = false, full = false, icon = '', ...props}) => (
|
||||
<button
|
||||
className={`
|
||||
${styles.button}
|
||||
${styles[`type--${cStyle}`]}
|
||||
${className}
|
||||
${full ? styles.full : ''}
|
||||
${raised ? styles.raised : ''}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
{icon && <Icon name={icon} className={styles.icon} />}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
export default Button;
|
||||
export default class Button extends React.Component {
|
||||
render() {
|
||||
const {cStyle = 'local', children, className, raised = false, full = false, icon = '', ...props} = this.props;
|
||||
return (
|
||||
<button
|
||||
className={`
|
||||
${styles.button}
|
||||
${styles[`type--${cStyle}`]}
|
||||
${className}
|
||||
${full ? styles.full : ''}
|
||||
${raised ? styles.raised : ''}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
{icon && <Icon name={icon} className={styles.icon} />}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import React, {Component} from 'react';
|
||||
import Clipboard from 'clipboard';
|
||||
|
||||
export default class Copy extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.refCopyButton = this.refCopyButton.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const clipboard = new Clipboard(this.copyButtonEl);
|
||||
|
||||
clipboard.on('success', (e) => {
|
||||
this.props.onCopy();
|
||||
e.clearSelection();
|
||||
});
|
||||
}
|
||||
|
||||
refCopyButton(button) {
|
||||
this.copyButtonEl = button;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {children, target = '', text = ''} = this.props;
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={this.refCopyButton}
|
||||
data-clipboard-action="copy"
|
||||
data-clipboard-text={text}
|
||||
data-clipboard-target={target}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './Drawer.css';
|
||||
import onClickOutside from 'react-onclickoutside';
|
||||
|
||||
const Drawer = ({children, handleClickOutside}) => {
|
||||
const Drawer = ({children, onClose}) => {
|
||||
return (
|
||||
<div className={styles.drawer}>
|
||||
<div className={styles.closeButton} onClick={handleClickOutside}>×</div>
|
||||
<div className={styles.closeButton} onClick={onClose}>×</div>
|
||||
<div className={styles.content}>
|
||||
{children}
|
||||
</div>
|
||||
@@ -15,7 +14,7 @@ const Drawer = ({children, handleClickOutside}) => {
|
||||
|
||||
Drawer.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
handleClickOutside: PropTypes.func.isRequired
|
||||
onClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default onClickOutside(Drawer);
|
||||
export default Drawer;
|
||||
|
||||
@@ -24,4 +24,3 @@ export {default as Option} from './components/Option';
|
||||
export {default as SnackBar} from './components/SnackBar';
|
||||
export {default as TextArea} from './components/TextArea';
|
||||
export {default as Drawer} from './components/Drawer';
|
||||
export {default as Copy} from './components/Copy';
|
||||
|
||||
@@ -161,7 +161,13 @@ const ErrInstallLock = new APIError('install lock active', {
|
||||
// ErrPermissionUpdateUsername is returned when the user does not have permission to update their username.
|
||||
const ErrPermissionUpdateUsername = new APIError('You do not have permission to update your username.', {
|
||||
translation_key: 'EDIT_USERNAME_NOT_AUTHORIZED',
|
||||
status: 500
|
||||
status: 403
|
||||
});
|
||||
|
||||
// ErrSameUsernameProvided is returned when attempting to update a username to the same username.
|
||||
const ErrSameUsernameProvided = new APIError('Same username provided.', {
|
||||
translation_key: 'SAME_USERNAME_PROVIDED',
|
||||
status: 400
|
||||
});
|
||||
|
||||
// ErrLoginAttemptMaximumExceeded is returned when the login maximum is exceeded.
|
||||
@@ -209,6 +215,7 @@ module.exports = {
|
||||
ErrAuthentication,
|
||||
ErrNotAuthorized,
|
||||
ErrPermissionUpdateUsername,
|
||||
ErrSameUsernameProvided,
|
||||
ErrSettingsInit,
|
||||
ErrInstallLock,
|
||||
ErrLoginAttemptMaximumExceeded,
|
||||
|
||||
@@ -4,6 +4,7 @@ const debug = require('debug')('talk:graph:mutators');
|
||||
const Comment = require('./comment');
|
||||
const Action = require('./action');
|
||||
const Tag = require('./tag');
|
||||
const Token = require('./token');
|
||||
const User = require('./user');
|
||||
|
||||
const plugins = require('../../services/plugins');
|
||||
@@ -14,6 +15,7 @@ let mutators = [
|
||||
Comment,
|
||||
Action,
|
||||
Tag,
|
||||
Token,
|
||||
User,
|
||||
|
||||
// Load the plugin mutators from the manager.
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
const errors = require('../../errors');
|
||||
const TokensService = require('../../services/tokens');
|
||||
const {
|
||||
CREATE_TOKEN,
|
||||
REVOKE_TOKEN
|
||||
} = require('../../perms/constants');
|
||||
|
||||
// Creates a new token for a user.
|
||||
const createToken = async ({user}, {name}) => {
|
||||
let {pat, jwt} = await TokensService.create(user.id, name);
|
||||
|
||||
// Attach the token to the PAT.
|
||||
pat.jwt = jwt;
|
||||
|
||||
// Return that PAT!
|
||||
return pat;
|
||||
};
|
||||
|
||||
// Revokes the token from the user.
|
||||
const revokeToken = async ({user}, {id}) => {
|
||||
return TokensService.revoke(user.id, id);
|
||||
};
|
||||
|
||||
module.exports = (context) => {
|
||||
let mutators = {
|
||||
Token: {
|
||||
create: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
revoke: () => Promise.reject(errors.ErrNotAuthorized)
|
||||
}
|
||||
};
|
||||
|
||||
if (context.user && context.user.can(CREATE_TOKEN)) {
|
||||
mutators.Token.create = (input) => createToken(context, input);
|
||||
}
|
||||
|
||||
if (context.user && context.user.can(REVOKE_TOKEN)) {
|
||||
mutators.Token.revoke = (input) => revokeToken(context, input);
|
||||
}
|
||||
|
||||
return mutators;
|
||||
};
|
||||
@@ -39,6 +39,12 @@ const RootMutation = {
|
||||
},
|
||||
removeTag(_, {tag}, {mutators: {Tag}}) {
|
||||
return wrapResponse(null)(Tag.remove(tag));
|
||||
},
|
||||
createToken(_, {input}, {mutators: {Token}}) {
|
||||
return wrapResponse('token')(Token.create(input));
|
||||
},
|
||||
revokeToken(_, {input}, {mutators: {Token}}) {
|
||||
return wrapResponse(null)(Token.revoke(input));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ const {
|
||||
SEARCH_OTHER_USERS,
|
||||
SEARCH_OTHERS_COMMENTS,
|
||||
UPDATE_USER_ROLES,
|
||||
SEARCH_COMMENT_METRICS
|
||||
SEARCH_COMMENT_METRICS,
|
||||
LIST_OWN_TOKENS
|
||||
} = require('../../perms/constants');
|
||||
|
||||
const User = {
|
||||
@@ -46,6 +47,13 @@ const User = {
|
||||
|
||||
return null;
|
||||
},
|
||||
tokens({id, tokens}, args, {user}) {
|
||||
if (!user || ((user.id !== id) && !user.can(LIST_OWN_TOKENS))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return tokens;
|
||||
},
|
||||
ignoredUsers({id}, args, {user, loaders: {Users}}) {
|
||||
|
||||
// Only allow a logged in user that is either the current user or is a staff
|
||||
|
||||
@@ -36,6 +36,23 @@ enum USER_ROLES {
|
||||
MODERATOR
|
||||
}
|
||||
|
||||
# Token is a personal access token associated with a given user.
|
||||
type Token {
|
||||
|
||||
# ID is the unique identifier for the token.
|
||||
id: ID!
|
||||
|
||||
# Name is the description for the token.
|
||||
name: String!
|
||||
|
||||
# Active determines if the token is available to hit the API.
|
||||
active: Boolean!
|
||||
|
||||
# JWT is the actual token to use for authentication, this is only available
|
||||
# on token creation, otherwise it will be null.
|
||||
jwt: String
|
||||
}
|
||||
|
||||
type UserProfile {
|
||||
# the id is an identifier for the user profile (email, facebook id, etc)
|
||||
id: String!
|
||||
@@ -78,6 +95,9 @@ type User {
|
||||
# ignored users.
|
||||
ignoredUsers: [User!]
|
||||
|
||||
# Tokens are the personal access tokens for a given user.
|
||||
tokens: [Token!]
|
||||
|
||||
# returns all comments based on a query.
|
||||
comments(query: CommentsQuery): CommentConnection!
|
||||
|
||||
@@ -906,6 +926,37 @@ type EditCommentResponse implements Response {
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# CreateTokenInput contains the input to create the token.
|
||||
input CreateTokenInput {
|
||||
|
||||
# Name is the description for the token.
|
||||
name: String!
|
||||
}
|
||||
|
||||
# CreateTokenResponse contains the errors related to creating a token.
|
||||
type CreateTokenResponse implements Response {
|
||||
|
||||
# Token is the Token that was created, or null if it failed.
|
||||
token: Token
|
||||
|
||||
# An array of errors relating to the mutation that occured.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# RevokeTokenInput contains the input to revoke the token.
|
||||
input RevokeTokenInput {
|
||||
|
||||
# ID is the JTI for the token.
|
||||
id: ID!
|
||||
}
|
||||
|
||||
# RevokeTokenResponse contains the errors related to revoking a token.
|
||||
type RevokeTokenResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occured.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# All mutations for the application are defined on this object.
|
||||
type RootMutation {
|
||||
|
||||
@@ -945,6 +996,12 @@ type RootMutation {
|
||||
# Ignore comments by another user
|
||||
ignoreUser(id: ID!): IgnoreUserResponse
|
||||
|
||||
# CreateToken will create a token that is attached to the current user.
|
||||
createToken(input: CreateTokenInput!): CreateTokenResponse!
|
||||
|
||||
# RevokeToken will revoke an existing token.
|
||||
revokeToken(input: RevokeTokenInput!): RevokeTokenResponse!
|
||||
|
||||
# Stop Ignoring comments by another user
|
||||
stopIgnoringUser(id: ID!): StopIgnoringUserResponse
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ en:
|
||||
comment_post_banned_word: "Your comment contains one or more words that are not permitted, so it will not be published. If you think this message is incorrect, please contact our moderation team."
|
||||
comment_post_notif: "Your comment has been posted."
|
||||
comment_post_notif_premod: "Thank you for posting. Our moderation team will review your comment shortly."
|
||||
common:
|
||||
copy: 'Copy'
|
||||
community:
|
||||
account_creation_date: "Account Creation Date"
|
||||
active: Active
|
||||
@@ -184,6 +186,7 @@ en:
|
||||
USERNAME_REQUIRED: "Must input a username"
|
||||
EDIT_WINDOW_ENDED: "You can no longer edit this comment. The time window to do so has expired."
|
||||
EDIT_USERNAME_NOT_AUTHORIZED: "You do not have permission to update your username."
|
||||
SAME_USERNAME_PROVIDED: "You must submit a different username."
|
||||
EMAIL_IN_USE: "Email address already in use"
|
||||
EMAIL_REQUIRED: "An email address is required"
|
||||
LOGIN_MAXIMUM_EXCEEDED: "You have made too many unsuccessful password attempts. Please wait."
|
||||
|
||||
@@ -33,6 +33,8 @@ es:
|
||||
comment_post_banned_word: "Tu comentario contiene una o más palabras que no están permitidas en nuestro espacio, por lo que no será publicado. Si crees que es un error, por favor contacta a nuestro equipo de moderación."
|
||||
comment_post_notif: "Tu comentario ha sido publicado."
|
||||
comment_post_notif_premod: "Gracias por el comentario. Nuestro equipo de moderación va a revisarlo muy pronto."
|
||||
common:
|
||||
copy: 'Copiar'
|
||||
community:
|
||||
account_creation_date: "Fecha de creación de la cuenta"
|
||||
active: Activa
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
const mongoose = require('../../services/mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const TokenSchema = new Schema({
|
||||
|
||||
// ID is the JTI of a given JWT's identifier.
|
||||
id: String,
|
||||
|
||||
// Name is given by the user on token creation.
|
||||
name: String,
|
||||
|
||||
// Active is used to determine if the token is valid.
|
||||
active: Boolean
|
||||
});
|
||||
|
||||
module.exports = TokenSchema;
|
||||
@@ -3,6 +3,7 @@ const bcrypt = require('bcrypt');
|
||||
const Schema = mongoose.Schema;
|
||||
const uuid = require('uuid');
|
||||
const TagLinkSchema = require('./schema/tag_link');
|
||||
const TokenSchema = require('./schema/token');
|
||||
const intersection = require('lodash/intersection');
|
||||
const can = require('../perms');
|
||||
|
||||
@@ -86,6 +87,9 @@ const UserSchema = new Schema({
|
||||
// addresses.
|
||||
profiles: [ProfileSchema],
|
||||
|
||||
// Tokens are the individual personal access tokens for a given user.
|
||||
tokens: [TokenSchema],
|
||||
|
||||
// Roles provides an array of roles (as strings) that is associated with a
|
||||
// user.
|
||||
roles: [{
|
||||
|
||||
@@ -193,7 +193,6 @@
|
||||
"react-linkify": "^0.1.3",
|
||||
"react-mdl": "^1.7.2",
|
||||
"react-mdl-selectfield": "^0.2.0",
|
||||
"react-onclickoutside": "^5.11.1",
|
||||
"react-redux": "^4.4.5",
|
||||
"react-router": "^3.0.0",
|
||||
"react-tagsinput": "^3.14.0",
|
||||
|
||||
@@ -14,6 +14,8 @@ module.exports = {
|
||||
REMOVE_COMMENT_TAG: 'REMOVE_COMMENT_TAG',
|
||||
UPDATE_USER_ROLES: 'UPDATE_USER_ROLES',
|
||||
UPDATE_CONFIG: 'UPDATE_CONFIG',
|
||||
CREATE_TOKEN: 'CREATE_TOKEN',
|
||||
REVOKE_TOKEN: 'REVOKE_TOKEN',
|
||||
|
||||
// queries
|
||||
SEARCH_ASSETS: 'SEARCH_ASSETS',
|
||||
@@ -22,6 +24,7 @@ module.exports = {
|
||||
SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS: 'SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS',
|
||||
SEARCH_OTHERS_COMMENTS: 'SEARCH_OTHERS_COMMENTS',
|
||||
SEARCH_COMMENT_METRICS: 'SEARCH_COMMENT_METRICS',
|
||||
LIST_OWN_TOKENS: 'LIST_OWN_TOKENS',
|
||||
SEARCH_COMMENT_STATUS_HISTORY: 'SEARCH_COMMENT_STATUS_HISTORY',
|
||||
|
||||
// subscriptions
|
||||
|
||||
@@ -29,6 +29,10 @@ module.exports = (user, perm) => {
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.UPDATE_CONFIG:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.CREATE_TOKEN:
|
||||
return check(user, ['ADMIN']);
|
||||
case types.REVOKE_TOKEN:
|
||||
return check(user, ['ADMIN']);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ module.exports = (user, perm) => {
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SEARCH_COMMENT_METRICS:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.LIST_OWN_TOKENS:
|
||||
return check(user, ['ADMIN']);
|
||||
case types.SEARCH_COMMENT_STATUS_HISTORY:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
default:
|
||||
|
||||
+19
-4
@@ -1,6 +1,7 @@
|
||||
const passport = require('passport');
|
||||
const UsersService = require('./users');
|
||||
const SettingsService = require('./settings');
|
||||
const TokensService = require('./tokens');
|
||||
const fetch = require('node-fetch');
|
||||
const FormData = require('form-data');
|
||||
const JWT = require('jsonwebtoken');
|
||||
@@ -157,10 +158,7 @@ const HandleLogout = (req, res, next) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the given token is already blacklisted, throw an error if it is.
|
||||
*/
|
||||
const CheckBlacklisted = (jwt) => new Promise((resolve, reject) => {
|
||||
const checkGeneralTokenBlacklist = (jwt) => new Promise((resolve, reject) => {
|
||||
client.get(`jtir[${jwt.jti}]`, (err, expiry) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
@@ -174,6 +172,20 @@ const CheckBlacklisted = (jwt) => new Promise((resolve, reject) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Check if the given token is already blacklisted, throw an error if it is.
|
||||
*/
|
||||
const CheckBlacklisted = async (jwt) => {
|
||||
|
||||
// Check to see if this is a PAT.
|
||||
if (jwt.pat) {
|
||||
return TokensService.validate(jwt.sub, jwt.jti);
|
||||
}
|
||||
|
||||
// It wasn't a PAT! Check to see if it is valid anyways.
|
||||
return checkGeneralTokenBlacklist(jwt);
|
||||
};
|
||||
|
||||
const jwt = require('jsonwebtoken');
|
||||
const JwtStrategy = require('passport-jwt').Strategy;
|
||||
const ExtractJwt = require('passport-jwt').ExtractJwt;
|
||||
@@ -340,6 +352,9 @@ passport.use(new LocalStrategy({
|
||||
passReqToCallback: true
|
||||
}, async (req, email, password, done) => {
|
||||
|
||||
// Normalize email
|
||||
email = email.toLowerCase();
|
||||
|
||||
// We need to check if this request has a recaptcha on it at all, if it does,
|
||||
// we must verify it first. If verification fails, we fail the request early.
|
||||
// We can only do this obviously when recaptcha is enabled.
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
const errors = require('../errors');
|
||||
const UserModel = require('../models/user');
|
||||
const JWT = require('jsonwebtoken');
|
||||
const uuid = require('uuid');
|
||||
|
||||
const {
|
||||
JWT_SECRET,
|
||||
JWT_ISSUER,
|
||||
JWT_AUDIENCE
|
||||
} = require('../config');
|
||||
|
||||
/**
|
||||
* TokenService manages Personal Access Tokens for users. These tokens are
|
||||
* persisted in the database and attached to the user.
|
||||
*/
|
||||
module.exports = class TokenService {
|
||||
|
||||
/**
|
||||
* Creates a token for a user with a given name.
|
||||
*
|
||||
* @param {String} userID the id of the user owning the token
|
||||
* @param {String} tokenName the name of the token to be created
|
||||
*/
|
||||
static async create(userID, tokenName) {
|
||||
|
||||
// Create the token.
|
||||
const payload = {
|
||||
jti: uuid.v4(),
|
||||
iss: JWT_ISSUER,
|
||||
aud: JWT_AUDIENCE,
|
||||
sub: userID,
|
||||
pat: true
|
||||
};
|
||||
|
||||
// Sign the payload.
|
||||
const jwt = JWT.sign(payload, JWT_SECRET, {});
|
||||
|
||||
// Create the PAT.
|
||||
let pat = {
|
||||
id: payload.jti,
|
||||
name: tokenName,
|
||||
active: true
|
||||
};
|
||||
|
||||
// Wait to update the user model with the new PAT.
|
||||
await UserModel.update({id: userID}, {
|
||||
$push: {
|
||||
tokens: pat
|
||||
}
|
||||
});
|
||||
|
||||
return {payload, jwt, pat};
|
||||
}
|
||||
|
||||
/**
|
||||
* Revokes a token and prevents the token from being used. Once a token has
|
||||
* been revoked, it cannot be re-enabled.
|
||||
*
|
||||
* @param {String} userID the id of the user owning the token
|
||||
* @param {String} tokenID the jti of the token to revoke
|
||||
*/
|
||||
static async revoke(userID, tokenID) {
|
||||
let query = {
|
||||
tokens: {
|
||||
$elemMatch: {
|
||||
id: tokenID
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (userID) {
|
||||
query.id = userID;
|
||||
}
|
||||
|
||||
// Revoke the token id.
|
||||
await UserModel.update(query, {
|
||||
$set: {
|
||||
'tokens.$.active': false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a given Token is valid.
|
||||
*
|
||||
* @param {String} userID the user's id that owns the token
|
||||
* @param {String} tokenID the id of the token
|
||||
*/
|
||||
static async validate(userID, tokenID) {
|
||||
|
||||
// Find the user.
|
||||
let user = await UserModel.findOne({
|
||||
id: userID
|
||||
}).select('tokens');
|
||||
if (!user || !user.tokens) {
|
||||
throw new errors.ErrAuthentication('user does not exist');
|
||||
}
|
||||
|
||||
// Extract the token from the user.
|
||||
let token = user.tokens.find(({id}) => id === tokenID);
|
||||
if (!token) {
|
||||
throw new errors.ErrAuthentication('token does not exist');
|
||||
}
|
||||
|
||||
// Check to see if it is active.
|
||||
if (!token.active) {
|
||||
throw new errors.ErrAuthentication('token is not active');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the tokens owned by the user.
|
||||
*
|
||||
* @param {String} userID the id of the user owning the token
|
||||
*/
|
||||
static async list(userID) {
|
||||
|
||||
// Get the user specified by the id.
|
||||
let user = await UserModel.findOne({id: userID}).select('tokens');
|
||||
if (!user || !user.tokens) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return user.tokens;
|
||||
}
|
||||
|
||||
};
|
||||
+39
-23
@@ -528,7 +528,7 @@ module.exports = class UsersService {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {String} id the id of the current user
|
||||
* @param {Object} token a jwt token used to sign in the user
|
||||
*/
|
||||
@@ -858,36 +858,52 @@ module.exports = class UsersService {
|
||||
|
||||
/**
|
||||
* Updates the user's username.
|
||||
* @param {String} id the id of the user to be enabled.
|
||||
* @param {String} username The new username for the user.
|
||||
* @param {String} id The id of the user.
|
||||
* @param {String} username The new username for the user.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static editName(id, username) {
|
||||
return UserModel.update({
|
||||
id,
|
||||
canEditName: true
|
||||
}, {
|
||||
$set: {
|
||||
username: username,
|
||||
lowercaseUsername: username.toLowerCase(),
|
||||
canEditName: false,
|
||||
status: 'PENDING',
|
||||
}
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.nModified <= 0) {
|
||||
return Promise.reject(errors.ErrPermissionUpdateUsername);
|
||||
static async editName(id, username) {
|
||||
try {
|
||||
const result = await UserModel.findOneAndUpdate({
|
||||
id,
|
||||
username: {$ne: username},
|
||||
canEditName: true
|
||||
}, {
|
||||
$set: {
|
||||
username: username,
|
||||
lowercaseUsername: username.toLowerCase(),
|
||||
canEditName: false,
|
||||
status: 'PENDING',
|
||||
}
|
||||
}, {
|
||||
new: true,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
const user = await UsersService.findById(id);
|
||||
if (user === null) {
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
if (!user.canEditName) {
|
||||
throw errors.ErrPermissionUpdateUsername;
|
||||
}
|
||||
|
||||
if (user.username === username) {
|
||||
throw errors.ErrSameUsernameProvided;
|
||||
}
|
||||
|
||||
throw new Error('edit username failed for an unexpected reason');
|
||||
}
|
||||
|
||||
return result;
|
||||
})
|
||||
.catch((err) => {
|
||||
}
|
||||
catch(err) {
|
||||
if (err.code === 11000) {
|
||||
throw errors.ErrUsernameTaken;
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -928,7 +944,7 @@ module.exports = class UsersService {
|
||||
}
|
||||
};
|
||||
|
||||
// Extract all the tokenUserNotFound plugins so we can integrate with other
|
||||
// Extract all the tokenUserNotFound plugins so we can integrate with other
|
||||
// providers.
|
||||
let tokenUserNotFoundHooks = null;
|
||||
|
||||
|
||||
@@ -46,10 +46,27 @@ describe('/api/v1/account/username', () => {
|
||||
.set(passport.inject({id: 'wrongid', roles: []}))
|
||||
.send({username: 'MojoJojo'}))
|
||||
.then(() => {
|
||||
done(new Error('Exected Error'));
|
||||
done(new Error('Expected Error'));
|
||||
})
|
||||
.catch((err) => {
|
||||
expect(err).to.be.truthy;
|
||||
expect(err).to.be.ok;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('it should return an error when the user submits the same username', (done) => {
|
||||
chai.request(app)
|
||||
.post(`/api/v1/users/${mockUser.id}/username-enable`)
|
||||
.set(passport.inject({id: '456', roles: ['ADMIN']}))
|
||||
.then(() => chai.request(app)
|
||||
.put('/api/v1/account/username')
|
||||
.set(passport.inject({id: mockUser.id, roles: []}))
|
||||
.send({username: 'Ana'}))
|
||||
.then(() => {
|
||||
done(new Error('Expected Error'));
|
||||
})
|
||||
.catch((err) => {
|
||||
expect(err).to.be.ok;
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -60,10 +77,10 @@ describe('/api/v1/account/username', () => {
|
||||
.set(passport.inject({id: mockUser.id, roles: []}))
|
||||
.send({username: 'MojoJojo'})
|
||||
.then(() => {
|
||||
done(new Error('Exected Error'));
|
||||
done(new Error('Expected Error'));
|
||||
})
|
||||
.catch((err) => {
|
||||
expect(err).to.be.truthy;
|
||||
expect(err).to.be.ok;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
const TokensService = require('../../../services/tokens');
|
||||
const UsersService = require('../../../services/users');
|
||||
const SettingsService = require('../../../services/settings');
|
||||
|
||||
const chai = require('chai');
|
||||
const chaiAsPromised = require('chai-as-promised');
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
|
||||
const expect = chai.expect;
|
||||
|
||||
describe('services.TokensService', () => {
|
||||
|
||||
let user;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
user = await UsersService.createLocalUser('sockmonster@gmail.com', '2Coral!!', 'Sockmonster');
|
||||
});
|
||||
|
||||
describe('#create', () => {
|
||||
|
||||
it('can create the token without error', async () => {
|
||||
let token = await TokensService.create(user.id, 'Github Token');
|
||||
expect(token).to.be.an.object;
|
||||
expect(token.jwt).to.be.a.string;
|
||||
expect(token.pat).to.be.an.object;
|
||||
|
||||
let pat = token.pat;
|
||||
|
||||
let tokens = await TokensService.list(user.id);
|
||||
expect(tokens).to.have.length(1);
|
||||
expect(tokens[0]).to.have.property('id', pat.id);
|
||||
expect(tokens[0]).to.have.property('name', pat.name);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#revoke', () => {
|
||||
|
||||
it('can revoke a token', async () => {
|
||||
let {pat: {id}} = await TokensService.create(user.id, 'Github Token');
|
||||
|
||||
let tokens = await TokensService.list(user.id);
|
||||
expect(tokens).to.have.length(1);
|
||||
expect(tokens[0]).to.have.property('id', id);
|
||||
expect(tokens[0]).to.have.property('active', true);
|
||||
|
||||
await TokensService.revoke(user.id, id);
|
||||
|
||||
tokens = await TokensService.list(user.id);
|
||||
expect(tokens).to.have.length(1);
|
||||
expect(tokens[0]).to.have.property('id', id);
|
||||
expect(tokens[0]).to.have.property('active', false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#validate', () => {
|
||||
|
||||
it('will allow a valid token', async () => {
|
||||
|
||||
// Create a token.
|
||||
let {pat: {id}} = await TokensService.create(user.id, 'Github Token');
|
||||
|
||||
// Validate it.
|
||||
await TokensService.validate(user.id, id);
|
||||
});
|
||||
|
||||
it('will not allow an invalid token', async () => {
|
||||
|
||||
// Create a token.
|
||||
let {pat: {id}} = await TokensService.create(user.id, 'Github Token');
|
||||
|
||||
// Revoke it.
|
||||
await TokensService.revoke(user.id, id);
|
||||
|
||||
// Validate it.
|
||||
return TokensService.validate(user.id, id).should.eventually.be.rejected;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#list', () => {
|
||||
|
||||
it('lists the tokens for a user', async () => {
|
||||
|
||||
let tokens = await TokensService.list(user.id);
|
||||
expect(tokens).to.have.length(0);
|
||||
|
||||
// Create a token.
|
||||
let {pat: {id}} = await TokensService.create(user.id, 'Github Token');
|
||||
|
||||
tokens = await TokensService.list(user.id);
|
||||
expect(tokens).to.have.length(1);
|
||||
expect(tokens[0]).to.have.property('id', id);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -246,7 +246,7 @@ describe('services.UsersService', () => {
|
||||
done(new Error('Error expected'));
|
||||
})
|
||||
.catch((err) => {
|
||||
expect(err).to.be.truthy;
|
||||
expect(err).to.be.ok;
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -260,7 +260,7 @@ describe('services.UsersService', () => {
|
||||
done(new Error('Error expected'));
|
||||
})
|
||||
.catch((err) => {
|
||||
expect(err).to.be.truthy;
|
||||
expect(err).to.be.ok;
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -272,7 +272,7 @@ describe('services.UsersService', () => {
|
||||
expect(false).to.be.true;
|
||||
})
|
||||
.catch((err) => {
|
||||
expect(err).to.be.truthy;
|
||||
expect(err).to.be.ok;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2041,7 +2041,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.2:
|
||||
create-hash "^1.1.0"
|
||||
inherits "^2.0.1"
|
||||
|
||||
create-react-class@^15.5.1, create-react-class@^15.5.x:
|
||||
create-react-class@^15.5.1:
|
||||
version "15.5.2"
|
||||
resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.5.2.tgz#6a8758348df660b88326a0e764d569f274aad681"
|
||||
dependencies:
|
||||
@@ -6904,12 +6904,6 @@ react-mdl@^1.7.1, react-mdl@^1.7.2:
|
||||
lodash.isequal "^4.4.0"
|
||||
prop-types "^15.5.0"
|
||||
|
||||
react-onclickoutside@^5.11.1:
|
||||
version "5.11.1"
|
||||
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-5.11.1.tgz#00314e52567cf55faba94cabbacd119619070623"
|
||||
dependencies:
|
||||
create-react-class "^15.5.x"
|
||||
|
||||
react-recaptcha@^2.2.6:
|
||||
version "2.2.6"
|
||||
resolved "https://registry.yarnpkg.com/react-recaptcha/-/react-recaptcha-2.2.6.tgz#bb44c1948a39b37d5a41920c73db833e5d8524f9"
|
||||
|
||||
Reference in New Issue
Block a user