mirror of
https://github.com/wassname/talk.git
synced 2026-07-10 03:15:31 +08:00
initial pass at status support
This commit is contained in:
+7
-1
@@ -1,5 +1,11 @@
|
||||
{
|
||||
"exec": "npm-run-all --parallel generate-introspection start:development",
|
||||
"verbose": true,
|
||||
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
|
||||
"ext": "js,json,graphql"
|
||||
"ext": "js,json,graphql",
|
||||
"watch": [
|
||||
".",
|
||||
"bin/cli",
|
||||
"bin/cli-serve"
|
||||
]
|
||||
}
|
||||
|
||||
+9
-100
@@ -106,20 +106,17 @@ async function createUser(options) {
|
||||
}
|
||||
|
||||
const user = await UsersService.createLocalUser(answers.email.trim(), answers.password.trim(), answers.username.trim());
|
||||
console.log(`Created user ${user.id}.`);
|
||||
|
||||
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}.`);
|
||||
});
|
||||
}));
|
||||
for (const role of answers.roles) {
|
||||
await UsersService.addRoleToUser(user.id, role);
|
||||
}
|
||||
}
|
||||
|
||||
util.shutdown();
|
||||
await UsersService.sendEmailConfirmation(user, answers.email.trim());
|
||||
|
||||
console.log(`Created User ${user.id}.`);
|
||||
util.shutdown();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown();
|
||||
@@ -241,12 +238,12 @@ function listUsers() {
|
||||
});
|
||||
|
||||
users.forEach((user) => {
|
||||
let state = user.disabled ? 'Disabled' : 'Enabled';
|
||||
const profile = user.profiles.find(({provider}) => provider === 'local');
|
||||
let state;
|
||||
if (profile && profile.metadata && profile.metadata.confirmed_at) {
|
||||
state += ', Verified';
|
||||
state = 'Verified';
|
||||
} else {
|
||||
state += ', Unverified';
|
||||
state = 'Unverified';
|
||||
}
|
||||
|
||||
table.push([
|
||||
@@ -336,74 +333,6 @@ function removeRole(userID, role) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ban a user
|
||||
* @param {String} userID id of the user to ban
|
||||
*/
|
||||
function ban(userID) {
|
||||
UsersService
|
||||
.setStatus(userID, 'BANNED')
|
||||
.then(() => {
|
||||
console.log(`Banned the User ${userID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies an email address for a user.
|
||||
*
|
||||
@@ -472,26 +401,6 @@ program
|
||||
.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
|
||||
.command('verify <userID> <email>')
|
||||
.description('verifies the given user\'s email address')
|
||||
|
||||
+5
-2
@@ -7,6 +7,8 @@ machine:
|
||||
environment:
|
||||
PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin"
|
||||
NODE_ENV: "test"
|
||||
MOCHA_FILE: "${CIRCLE_TEST_REPORTS}/junit/test-results.xml"
|
||||
MOCHA_REPORTER: "mocha-junit-reporter"
|
||||
pre:
|
||||
# TODO: use the following to add in support for MongoDB 3.4.
|
||||
# # Upgrade the database version to 3.4.
|
||||
@@ -47,10 +49,11 @@ database:
|
||||
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
|
||||
- yarn test
|
||||
# Run the end to end tests
|
||||
- yarn e2e:ci
|
||||
# Check dependancies using nsp.
|
||||
- nsp check
|
||||
- yarn e2e-ci
|
||||
|
||||
deployment:
|
||||
release:
|
||||
|
||||
@@ -14,7 +14,7 @@ export default withQuery(gql`
|
||||
})
|
||||
flaggedUsernamesCount: userCount(query: {
|
||||
action_type: FLAG,
|
||||
statuses: [PENDING]
|
||||
statuses: [SET, CHANGED]
|
||||
})
|
||||
}
|
||||
`, {
|
||||
|
||||
@@ -22,7 +22,7 @@ const withData = withQuery(gql`
|
||||
query TalkAdmin_Community {
|
||||
flaggedUsernamesCount: userCount(query: {
|
||||
action_type: FLAG,
|
||||
statuses: [PENDING]
|
||||
statuses: [SET, CHANGED]
|
||||
})
|
||||
...${getDefinitionName(FlaggedAccounts.fragments.root)}
|
||||
...${getDefinitionName(FlaggedUser.fragments.root)}
|
||||
|
||||
@@ -26,4 +26,4 @@ CloseCommentsInfo.propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default CloseCommentsInfo;
|
||||
export default CloseCommentsInfo;
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
.bio textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
min-height: 100px;
|
||||
margin: 10px 0;
|
||||
border: solid 1px #d8d8d8;
|
||||
}
|
||||
|
||||
.bio h1 {
|
||||
font-size: 16px;
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
.bio p {
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './Bio.css';
|
||||
import {Button} from '../../coral-ui';
|
||||
|
||||
export default ({bio, handleSave, handleInput, handleCancel}) => (
|
||||
<div className={styles.bio}>
|
||||
<h1>Bio</h1>
|
||||
<p>Tell the community about yourself</p>
|
||||
<form>
|
||||
<textarea value={bio} onChange={handleInput} />
|
||||
<div className={styles.actions}>
|
||||
<Button cStyle='cancel' type="button" onClick={handleCancel} raised>Cancel</Button>
|
||||
<Button cStyle='success' type="submit" onClick={handleSave}>Save Changes</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import React, {Component} from 'react';
|
||||
import Bio from '../components/Bio';
|
||||
|
||||
export default class BioContainer extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
bio: props.bio
|
||||
};
|
||||
|
||||
this.handleSave = this.handleSave.bind(this);
|
||||
this.handleInput = this.handleInput.bind(this);
|
||||
this.handleCancel = this.handleCancel.bind(this);
|
||||
}
|
||||
|
||||
handleInput(e) {
|
||||
this.setState({
|
||||
bio: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
handleSave (e) {
|
||||
e.preventDefault();
|
||||
const {userData, saveBio} = this.props;
|
||||
const {bio} = this.state;
|
||||
saveBio(userData.id, {bio});
|
||||
}
|
||||
|
||||
handleCancel () {
|
||||
this.setState({
|
||||
bio: this.props.bio
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
return <Bio
|
||||
bio={this.state.bio}
|
||||
userData={this.props.userData}
|
||||
handleSave={this.handleSave}
|
||||
handleInput={this.handleInput}
|
||||
handleCancel={this.handleCancel}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
@@ -244,4 +244,4 @@ module.exports = {
|
||||
ErrSpecialChars,
|
||||
ErrUsernameTaken,
|
||||
ExtendableError,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const ActionsService = require('../../services/actions');
|
||||
const UsersService = require('../../services/users');
|
||||
const errors = require('../../errors');
|
||||
const {CREATE_ACTION, DELETE_ACTION} = require('../../perms/constants');
|
||||
|
||||
@@ -31,15 +30,6 @@ const createAction = async ({user = {}, pubsub, loaders: {Comments}}, {item_id,
|
||||
metadata
|
||||
});
|
||||
|
||||
if (item_type === 'USERS' && action_type === 'FLAG') {
|
||||
|
||||
// Set the user as pending if it was a user flag and user has no Admin, Staff or Moderation roles
|
||||
let user = await UsersService.findById(item_id);
|
||||
if(!user.isStaff()){
|
||||
await UsersService.setStatus(item_id, 'PENDING');
|
||||
}
|
||||
}
|
||||
|
||||
if (comment) {
|
||||
pubsub.publish('commentFlagged', comment);
|
||||
}
|
||||
|
||||
+88
-30
@@ -1,29 +1,82 @@
|
||||
const errors = require('../../errors');
|
||||
const UserModel = require('../../models/user');
|
||||
const UsersService = require('../../services/users');
|
||||
const {SET_USER_STATUS, SUSPEND_USER, REJECT_USERNAME} = require('../../perms/constants');
|
||||
const {
|
||||
SET_USER_USERNAME_STATUS,
|
||||
SET_USER_BAN_STATUS,
|
||||
SET_USER_SUSPENSION_STATUS,
|
||||
} = require('../../perms/constants');
|
||||
|
||||
const setUserStatus = async ({pubsub}, {id, status}) => {
|
||||
const result = await UsersService.setStatus(id, status);
|
||||
if (result && result.status === 'BANNED') {
|
||||
pubsub.publish('userBanned', result);
|
||||
const setUserUsernameStatus = async (ctx, id, status) => {
|
||||
const user = await UserModel.findOneAndUpdate({id}, {
|
||||
$set: {
|
||||
'status.username.status': status
|
||||
},
|
||||
$push: {
|
||||
'status.username.history': {
|
||||
status,
|
||||
assigned_by: ctx.user.id,
|
||||
created_at: Date.now()
|
||||
}
|
||||
}
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
if (user === null) {
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
if (status === 'REJECTED') {
|
||||
ctx.pubsub.publish('usernameRejected', user);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const suspendUser = async ({pubsub}, {id, message, until}) => {
|
||||
const result = await UsersService.suspendUser(id, message, until);
|
||||
if (result) {
|
||||
pubsub.publish('userSuspended', result);
|
||||
const setUserBanStatus = async (ctx, id, status) => {
|
||||
const user = await UserModel.findOneAndUpdate({id}, {
|
||||
$set: {
|
||||
'status.banned.status': status
|
||||
},
|
||||
$push: {
|
||||
'status.banned.history': {
|
||||
status,
|
||||
assigned_by: ctx.user.id,
|
||||
created_at: Date.now()
|
||||
}
|
||||
}
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
if (user === null) {
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
if (user.banned) {
|
||||
ctx.pubsub.publish('userBanned', user);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const rejectUsername = async ({pubsub}, {id, message}) => {
|
||||
const result = await UsersService.rejectUsername(id, message);
|
||||
if (result) {
|
||||
pubsub.publish('usernameRejected', result);
|
||||
const setUserSuspensionStatus = async (ctx, id, until) => {
|
||||
const user = await UserModel.findOneAndUpdate({id}, {
|
||||
$set: {
|
||||
'status.suspension.until': until
|
||||
},
|
||||
$push: {
|
||||
'status.suspension.history': {
|
||||
until,
|
||||
assigned_by: ctx.user.id,
|
||||
created_at: Date.now()
|
||||
}
|
||||
}
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
if (user === null) {
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
if (user.suspended) {
|
||||
ctx.pubsub.publish('userSuspended', user);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const ignoreUser = ({user}, userToIgnore) => {
|
||||
@@ -34,27 +87,32 @@ const stopIgnoringUser = ({user}, userToStopIgnoring) => {
|
||||
return UsersService.stopIgnoringUsers(user.id, [userToStopIgnoring.id]);
|
||||
};
|
||||
|
||||
module.exports = (context) => {
|
||||
module.exports = (ctx) => {
|
||||
let mutators = {
|
||||
User: {
|
||||
setUserStatus: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
suspendUser: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
rejectUsername: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
ignoreUser: (action) => ignoreUser(context, action),
|
||||
stopIgnoringUser: (action) => stopIgnoringUser(context, action),
|
||||
ignoreUser: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
stopIgnoringUser: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
setUserUsernameStatus: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
setUserBanStatus: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
setUserSuspensionStatus: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
}
|
||||
};
|
||||
|
||||
if (context.user && context.user.can(SET_USER_STATUS)) {
|
||||
mutators.User.setUserStatus = (action) => setUserStatus(context, action);
|
||||
}
|
||||
if (ctx.user) {
|
||||
mutators.User.ignoreUser = (action) => ignoreUser(ctx, action);
|
||||
mutators.User.stopIgnoringUser = (action) => stopIgnoringUser(ctx, action);
|
||||
|
||||
if (context.user && context.user.can(SUSPEND_USER)) {
|
||||
mutators.User.suspendUser = (action) => suspendUser(context, action);
|
||||
}
|
||||
if (ctx.user.can(SET_USER_USERNAME_STATUS)) {
|
||||
mutators.User.setUserUsernameStatus = (id, status) => setUserUsernameStatus(ctx, id, status);
|
||||
}
|
||||
|
||||
if (context.user && context.user.can(REJECT_USERNAME)) {
|
||||
mutators.User.rejectUsername = (action) => rejectUsername(context, action);
|
||||
if (ctx.user.can(SET_USER_BAN_STATUS)) {
|
||||
mutators.User.setUserBanStatus = (id, status) => setUserBanStatus(ctx, id, status);
|
||||
}
|
||||
|
||||
if (ctx.user.can(SET_USER_SUSPENSION_STATUS)) {
|
||||
mutators.User.setUserSuspensionStatus = (id, until) => setUserSuspensionStatus(ctx, id, until);
|
||||
}
|
||||
}
|
||||
|
||||
return mutators;
|
||||
|
||||
@@ -19,14 +19,17 @@ const RootMutation = {
|
||||
deleteAction: async (_, {id}, {mutators: {Action}}) => {
|
||||
await Action.delete({id});
|
||||
},
|
||||
setUserStatus: async (_, {id, status}, {mutators: {User}}) => {
|
||||
await User.setUserStatus({id, status});
|
||||
approveUsername: async (_, {id}, {mutators: {User}}) => {
|
||||
await User.setUserUsernameStatus(id, 'APPROVED');
|
||||
},
|
||||
suspendUser: async (_, {input: {id, message, until}}, {mutators: {User}}) => {
|
||||
await User.suspendUser({id, message, until});
|
||||
rejectUsername: async (_, {id}, {mutators: {User}}) => {
|
||||
await User.setUserUsernameStatus(id, 'REJECTED');
|
||||
},
|
||||
rejectUsername: async (_, {input: {id, message}}, {mutators: {User}}) => {
|
||||
await User.rejectUsername({id, message});
|
||||
setUserSuspensionStatus: async (_, {input: {id, until}}, {mutators: {User}}) => {
|
||||
await User.setUserSuspensionStatus(id, until);
|
||||
},
|
||||
setUserBanStatus: async (_, {input: {id, status}}, {mutators: {User}}) => {
|
||||
await User.setUserBanStatus(id, status);
|
||||
},
|
||||
ignoreUser: async (_, {id}, {mutators: {User}}) => {
|
||||
await User.ignoreUser({id});
|
||||
|
||||
@@ -6,7 +6,6 @@ const {
|
||||
SEARCH_OTHERS_COMMENTS,
|
||||
UPDATE_USER_ROLES,
|
||||
SEARCH_COMMENT_METRICS,
|
||||
VIEW_SUSPENSION_INFO,
|
||||
LIST_OWN_TOKENS
|
||||
} = require('../../perms/constants');
|
||||
|
||||
@@ -84,12 +83,12 @@ const User = {
|
||||
}
|
||||
},
|
||||
|
||||
suspension({id, suspension}, _, {user}) {
|
||||
if (user.id !== id && !user.can(VIEW_SUSPENSION_INFO)) {
|
||||
return null;
|
||||
}
|
||||
return suspension;
|
||||
}
|
||||
// suspension({id, suspension}, _, {user}) {
|
||||
// if (user.id !== id && !user.can(VIEW_SUSPENSION_INFO)) {
|
||||
// return null;
|
||||
// }
|
||||
// return suspension;
|
||||
// }
|
||||
};
|
||||
|
||||
// Decorate the User type resolver with a tags field.
|
||||
|
||||
@@ -27,13 +27,13 @@ const setupFunctions = plugins.get('server', 'setupFunctions').reduce((acc, {plu
|
||||
commentAdded: {
|
||||
filter: (comment, context) => {
|
||||
|
||||
// Only priviledged users can subscribe to all assets.
|
||||
// Only privileged users can subscribe to all assets.
|
||||
if (!args.asset_id && (!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_ADDED))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If user scubsscribes for statuses other than NONE and/or ACCEPTED statuses, it needs
|
||||
// special priviledges.
|
||||
// If user subscribes for statuses other than NONE and/or ACCEPTED statuses, it needs
|
||||
// special privileges.
|
||||
if (
|
||||
(!args.statuses || args.statuses.some((status) => !['NONE', 'ACCEPTED'].includes(status))) &&
|
||||
(!context.user || !context.user.can(SUBSCRIBE_ALL_COMMENT_ADDED))
|
||||
|
||||
+139
-55
@@ -64,8 +64,99 @@ type UserProfile {
|
||||
provider: String!
|
||||
}
|
||||
|
||||
type SuspensionInfo {
|
||||
# USER_STATUS_USERNAME is the different states that a username can be in.
|
||||
enum USER_STATUS_USERNAME {
|
||||
# UNSET is used when the username can be changed, and does not necessarily
|
||||
# require moderator action to become active. This can be used when the user
|
||||
# signs up with a social login and has the option of setting their own
|
||||
# username.
|
||||
UNSET
|
||||
|
||||
# SET is used when the username has been set for the first time, but cannot
|
||||
# change without the username being rejected by a moderator and that moderator
|
||||
# agreeing that the username should be allowed to change.
|
||||
SET
|
||||
|
||||
# APPROVED is used when the username was changed, and subsequently approved by
|
||||
# said moderator.
|
||||
APPROVED
|
||||
|
||||
# REJECTED is used when the username was changed, and subsequently rejected by
|
||||
# said moderator.
|
||||
REJECTED
|
||||
|
||||
# CHANGED is used after a user has changed their username after it was
|
||||
# rejected.
|
||||
CHANGED
|
||||
}
|
||||
|
||||
# UserStatusInput describes the queryable components of the UserStatus.
|
||||
input UserStatusInput {
|
||||
# username will restrict the returned users to only those with the given
|
||||
# username status's. If not provided, no filtering will be performed.
|
||||
username: [USER_STATUS_USERNAME!]
|
||||
|
||||
# banned will restrict the returned users to only those that are, or are not
|
||||
# banned. If not provided, no filtering will be performed.
|
||||
banned: Boolean
|
||||
|
||||
# suspended will restrict the returned users to only those that are, or are not
|
||||
# suspended. If not provided, no filtering will be performed.
|
||||
suspended: Boolean
|
||||
}
|
||||
|
||||
type UsernameStatusHistory {
|
||||
status: USER_STATUS_USERNAME!
|
||||
assigned_by: User
|
||||
created_at: Date!
|
||||
}
|
||||
|
||||
type UsernameStatus {
|
||||
status: USER_STATUS_USERNAME!
|
||||
history: [UsernameStatusHistory!]
|
||||
}
|
||||
|
||||
type BannedStatusHistory {
|
||||
status: Boolean!
|
||||
assigned_by: User
|
||||
created_at: Date!
|
||||
}
|
||||
|
||||
type BannedStatus {
|
||||
status: Boolean!
|
||||
history: [BannedStatusHistory!]
|
||||
}
|
||||
|
||||
type SuspensionStatusHistory {
|
||||
until: Date
|
||||
assigned_by: User
|
||||
created_at: Date!
|
||||
}
|
||||
|
||||
type SuspensionStatus {
|
||||
until: Date
|
||||
history: [SuspensionStatusHistory!]
|
||||
}
|
||||
|
||||
type UserStatus {
|
||||
# username is the status of the username.
|
||||
username: UsernameStatus!
|
||||
|
||||
# banned is the bool that determines if the user is banned or not.
|
||||
banned: BannedStatus!
|
||||
|
||||
# suspension is the date that the user is suspended until.
|
||||
suspension: SuspensionStatus!
|
||||
}
|
||||
|
||||
input UserStateInput {
|
||||
status: UserStatusInput
|
||||
}
|
||||
|
||||
# UserState describes the different permission based details for a user.
|
||||
type UserState {
|
||||
# status describes the statuses of different aspects of the user's details.
|
||||
status: UserStatus
|
||||
}
|
||||
|
||||
# Any person who can author comments, create actions, and view comments on a
|
||||
@@ -114,11 +205,7 @@ type User {
|
||||
reliable: Reliability
|
||||
|
||||
# returns user status
|
||||
status: USER_STATUS
|
||||
|
||||
# returns suspension info. Only available to Admins and Moderators
|
||||
# or on own logged in User.
|
||||
suspension: SuspensionInfo
|
||||
state: UserState
|
||||
}
|
||||
|
||||
# UserConnection represents a paginable subset of a user list.
|
||||
@@ -143,8 +230,7 @@ input UsersQuery {
|
||||
# Users returned will only be ones which have at least one action of this.
|
||||
action_type: ACTION_TYPE
|
||||
|
||||
# Current status of a user..
|
||||
statuses: [USER_STATUS!]
|
||||
state: UserStateInput
|
||||
|
||||
# Limit the number of results to be returned.
|
||||
limit: Int = 10
|
||||
@@ -261,7 +347,7 @@ enum ACTION_TYPE {
|
||||
# CommentsQuery allows the ability to query comments by a specific methods.
|
||||
input CommentsQuery {
|
||||
|
||||
# Author of the commente
|
||||
# Author of the comments.
|
||||
author_id: ID
|
||||
|
||||
# Current status of a comment.
|
||||
@@ -351,8 +437,8 @@ input UserCountQuery {
|
||||
# type.
|
||||
action_type: ACTION_TYPE
|
||||
|
||||
# Current status of a user.
|
||||
statuses: [USER_STATUS]
|
||||
# state queries for a specific subset of users with the given state query.
|
||||
state: UserStateInput
|
||||
}
|
||||
|
||||
type EditInfo {
|
||||
@@ -810,14 +896,6 @@ enum SORT_COMMENTS_BY {
|
||||
REPLIES
|
||||
}
|
||||
|
||||
# All queries that can be executed.
|
||||
enum USER_STATUS {
|
||||
ACTIVE
|
||||
BANNED
|
||||
PENDING
|
||||
APPROVED
|
||||
}
|
||||
|
||||
# Metrics for the assets.
|
||||
enum ASSET_METRICS_SORT {
|
||||
|
||||
@@ -995,26 +1073,13 @@ input CreateDontAgreeInput {
|
||||
}
|
||||
|
||||
# Input for suspendUser mutation.
|
||||
input SuspendUserInput {
|
||||
input SetUserSuspensionStatusInput {
|
||||
|
||||
# id of target user.
|
||||
id: ID!
|
||||
|
||||
# message to be sent to the user.
|
||||
message: String!
|
||||
|
||||
# target user will be suspended until this date.
|
||||
until: Date!
|
||||
}
|
||||
|
||||
# Input for rejectUsername mutation.
|
||||
input RejectUsernameInput {
|
||||
|
||||
# id of target user.
|
||||
id: ID!
|
||||
|
||||
# message to be sent to the user.
|
||||
message: String!
|
||||
until: Date
|
||||
}
|
||||
|
||||
# Configurable settings that can be overridden for the Asset. You must specify
|
||||
@@ -1027,7 +1092,7 @@ input AssetSettingsInput {
|
||||
# moderation is the moderation mode for the asset.
|
||||
moderation: MODERATION_MODE
|
||||
|
||||
# questionBoxEnable will enable the Question Boxs' content to be visable above
|
||||
# questionBoxEnable will enable the Question Boxs' content to be visible above
|
||||
# the comment box.
|
||||
questionBoxEnable: Boolean
|
||||
|
||||
@@ -1075,17 +1140,9 @@ type DeleteActionResponse implements Response {
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# SetUserStatusResponse is the response returned with possibly some errors
|
||||
# relating to the delete action attempt.
|
||||
type SetUserStatusResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# SuspendUserResponse is the response returned with possibly some errors
|
||||
# relating to the suspend action attempt.
|
||||
type SuspendUserResponse implements Response {
|
||||
# SetUserSuspensionStatusResponse is the response returned with possibly some
|
||||
# errors relating to the suspend action attempt.
|
||||
type SetUserSuspensionStatusResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
@@ -1218,7 +1275,7 @@ input UpdateSettingsInput {
|
||||
# comment is posted that it can still be edited by the author.
|
||||
editCommentWindowLength: Int
|
||||
|
||||
# wordlist allows chaninging the available wordlists.
|
||||
# wordlist allows changing the available wordlists.
|
||||
wordlist: UpdateWordlistInput
|
||||
|
||||
# domains allows changing the available lists of domains.
|
||||
@@ -1281,6 +1338,29 @@ type RevokeTokenResponse implements Response {
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# SetUserBanStatusInput contains the input to change the ban status of a given
|
||||
# user.
|
||||
input SetUserBanStatusInput {
|
||||
|
||||
# id is the user to set the ban status on.
|
||||
id: ID!
|
||||
|
||||
# status is the ban status to set on the target user.
|
||||
status: Boolean!
|
||||
}
|
||||
|
||||
type SetUserBanStatusResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
type SetUsernameStatusResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# All mutations for the application are defined on this object.
|
||||
type RootMutation {
|
||||
|
||||
@@ -1299,17 +1379,21 @@ type RootMutation {
|
||||
# Edit a comment
|
||||
editComment(id: ID!, asset_id: ID!, edit: EditCommentInput): EditCommentResponse!
|
||||
|
||||
# Sets User status. Requires the `ADMIN` role.
|
||||
# Sets the suspension status on a given user. Requires the `MODERATOR` role.
|
||||
# Mutation is restricted.
|
||||
setUserStatus(id: ID!, status: USER_STATUS!): SetUserStatusResponse
|
||||
setUserSuspensionStatus(input: SetUserSuspensionStatusInput!): SetUserSuspensionStatusResponse
|
||||
|
||||
# Suspends a user. Requires the `ADMIN` role.
|
||||
# Sets the ban status on a given user. Requires the `MODERATOR` role.
|
||||
# Mutation is restricted.
|
||||
suspendUser(input: SuspendUserInput!): SuspendUserResponse
|
||||
setUserBanStatus(input: SetUserBanStatusInput!): SetUserBanStatusResponse
|
||||
|
||||
# Reject a username. Requires the `ADMIN` role.
|
||||
# Mutation is restricted.
|
||||
rejectUsername(input: RejectUsernameInput!): RejectUsernameResponse
|
||||
# Sets the username status on a given user to `APPROVED`. Requires the
|
||||
# `MODERATOR` role. Mutation is restricted.
|
||||
approveUsername(id: ID!): SetUsernameStatusResponse
|
||||
|
||||
# Sets the username status on a given user to `REJECTED`. Requires the
|
||||
# `MODERATOR` role. Mutation is restricted.
|
||||
rejectUsername(id: ID!): SetUsernameStatusResponse
|
||||
|
||||
# Sets Comment status. Requires the `ADMIN` role.
|
||||
# Mutation is restricted.
|
||||
@@ -1327,7 +1411,7 @@ type RootMutation {
|
||||
|
||||
# Updates the status of an asset allowing you to close/reopen an asset for
|
||||
# commenting.
|
||||
# Mutation is restricted.
|
||||
# Mutation is restricted.
|
||||
updateAssetStatus(id: ID!, input: UpdateAssetStatusInput!): UpdateAssetStatusResponse
|
||||
|
||||
# updateSettings will update the global settings.
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = [
|
||||
'ACTIVE',
|
||||
'BANNED',
|
||||
'PENDING',
|
||||
'APPROVED' // Indicates that the users' username has been approved
|
||||
];
|
||||
@@ -0,0 +1,25 @@
|
||||
module.exports = [
|
||||
|
||||
// UNSET is used when the username can be changed, and does not necessarily
|
||||
// require moderator action to become active. This can be used when the user
|
||||
// signs up with a social login and has the option of setting their own
|
||||
// username.
|
||||
'UNSET',
|
||||
|
||||
// SET is used when the username has been set for the first time, but cannot
|
||||
// change without the username being rejected by a moderator and that moderator
|
||||
// agreeing that the username should be allowed to change.
|
||||
'SET',
|
||||
|
||||
// APPROVED is used when the username was changed, and subsequently approved by
|
||||
// said moderator.
|
||||
'APPROVED',
|
||||
|
||||
// REJECTED is used when the username was changed, and subsequently rejected by
|
||||
// said moderator.
|
||||
'REJECTED',
|
||||
|
||||
// CHANGED is used after a user has changed their username after it was
|
||||
// rejected.
|
||||
'CHANGED',
|
||||
];
|
||||
+107
-34
@@ -10,8 +10,9 @@ const can = require('../perms');
|
||||
// USER_ROLES is the array of roles that is permissible as a user role.
|
||||
const USER_ROLES = require('./enum/user_roles');
|
||||
|
||||
// USER_STATUS is the list of statuses that are permitted for the user status.
|
||||
const USER_STATUS = require('./enum/user_status');
|
||||
// USER_STATUS_USERNAME is the list of statuses that are supported by storing
|
||||
// the username state.
|
||||
const USER_STATUS_USERNAME = require('./enum/user_status_username');
|
||||
|
||||
// ProfileSchema is the mongoose schema defined as the representation of a
|
||||
// User's profile stored in MongoDB.
|
||||
@@ -73,10 +74,6 @@ const UserSchema = new Schema({
|
||||
unique: true
|
||||
},
|
||||
|
||||
// This is true when the user account is disabled, no action should be
|
||||
// acknowledged when they are disabled. Logins are also prevented.
|
||||
disabled: Boolean,
|
||||
|
||||
// This provides a source of identity proof for users who login using the
|
||||
// local provider. A local provider will be assumed for users who do not
|
||||
// have any social profiles.
|
||||
@@ -97,41 +94,85 @@ const UserSchema = new Schema({
|
||||
enum: USER_ROLES
|
||||
}],
|
||||
|
||||
// Status provides a string that says in which state the account is.
|
||||
// When the account is banned, the user login is disabled.
|
||||
// Status stores the user status information regarding permissions,
|
||||
// capabilities and moderation state.
|
||||
status: {
|
||||
type: String,
|
||||
enum: USER_STATUS,
|
||||
default: 'ACTIVE'
|
||||
},
|
||||
|
||||
// Determines whether the user can edit their username.
|
||||
canEditName: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// Username stores the current user status for the username as well as the
|
||||
// history of changes.
|
||||
username: {
|
||||
|
||||
// User's suspension details.
|
||||
suspension: {
|
||||
until: {
|
||||
type: Date,
|
||||
default: null,
|
||||
// Status stores the current username status.
|
||||
status: {
|
||||
type: String,
|
||||
enum: USER_STATUS_USERNAME,
|
||||
},
|
||||
|
||||
// History stores the history of username status changes.
|
||||
history: [{
|
||||
|
||||
// Status stores the historical username status.
|
||||
status: {
|
||||
type: String,
|
||||
enum: USER_STATUS_USERNAME,
|
||||
},
|
||||
|
||||
// assigned_by stores the user id of the user who assigned this status.
|
||||
assigned_by: {type: String, default: null},
|
||||
|
||||
// created_at stores the date when this status was assigned.
|
||||
created_at: {type: Date, default: Date.now}
|
||||
}],
|
||||
},
|
||||
},
|
||||
|
||||
// User's settings
|
||||
settings: {
|
||||
bio: {
|
||||
type: String,
|
||||
default: ''
|
||||
// Banned stores the current user banned status as well as the history of
|
||||
// changes.
|
||||
banned: {
|
||||
|
||||
// Status stores the current user banned status.
|
||||
status: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false,
|
||||
},
|
||||
history: [{
|
||||
|
||||
// Status stores the historical banned status.
|
||||
status: Boolean,
|
||||
|
||||
// assigned_by stores the user id of the user who assigned this status.
|
||||
assigned_by: {type: String, default: null},
|
||||
|
||||
// created_at stores the date when this status was assigned.
|
||||
created_at: {type: Date, default: Date.now}
|
||||
}],
|
||||
},
|
||||
|
||||
// Suspension stores the current user suspension status as well as the
|
||||
// history of changes.
|
||||
suspension: {
|
||||
|
||||
// until is the date that the user is suspended until.
|
||||
until: {
|
||||
type: Date,
|
||||
default: null,
|
||||
},
|
||||
history: [{
|
||||
|
||||
// until is the date that the user is suspended until.
|
||||
until: Date,
|
||||
|
||||
// assigned_by stores the user id of the user who assigned this status.
|
||||
assigned_by: {type: String, default: null},
|
||||
|
||||
// created_at stores the date when this status was assigned.
|
||||
created_at: {type: Date, default: Date.now}
|
||||
}]
|
||||
}
|
||||
},
|
||||
|
||||
ignoresUsers: [{
|
||||
|
||||
// user id of another user
|
||||
type: String,
|
||||
}],
|
||||
// IgnoresUsers is an array of user id's that the current user is ignoring.
|
||||
ignoresUsers: [String],
|
||||
|
||||
// Tags are added by the self or by administrators.
|
||||
tags: [TagLinkSchema],
|
||||
@@ -158,7 +199,7 @@ const UserSchema = new Schema({
|
||||
}
|
||||
});
|
||||
|
||||
// Add the indixies on the user profile data.
|
||||
// Add the index on the user profile data.
|
||||
UserSchema.index({
|
||||
'profiles.id': 1,
|
||||
'profiles.provider': 1
|
||||
@@ -201,6 +242,38 @@ UserSchema.method('can', function(...actions) {
|
||||
return can(this, ...actions);
|
||||
});
|
||||
|
||||
/**
|
||||
* banned returns true when the user is currently banned, and sets the banned
|
||||
* status locally.
|
||||
*/
|
||||
UserSchema.virtual('banned')
|
||||
.get(function() {
|
||||
return this.status.banned.status;
|
||||
})
|
||||
.set(function(status) {
|
||||
this.status.banned.status = status;
|
||||
this.status.banned.history.push({
|
||||
status,
|
||||
created_at: new Date()
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* suspended returns true when the user is currently suspended, and sets the
|
||||
* suspension status locally.
|
||||
*/
|
||||
UserSchema.virtual('suspended')
|
||||
.get(function() {
|
||||
return Boolean(this.status.suspension.until && this.status.suspension.until > new Date());
|
||||
})
|
||||
.set(function(until) {
|
||||
this.status.suspension.until = until;
|
||||
this.status.suspension.history.push({
|
||||
until,
|
||||
created_at: new Date()
|
||||
});
|
||||
});
|
||||
|
||||
// Create the User model.
|
||||
const UserModel = mongoose.model('User', UserSchema);
|
||||
|
||||
|
||||
+24
-37
@@ -6,43 +6,34 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"postinstall": "./bin/cli plugins reconcile --skip-remote",
|
||||
"start": "./bin/cli serve -j -w",
|
||||
"dev-start": "nodemon -w . -w bin/cli -w bin/cli-serve --config .nodemon.json --exec \"yarn generate-introspection && ./bin/cli -c .env serve -j -w\"",
|
||||
"prebuild": "yarn generate-introspection",
|
||||
"build": "WEBPACK=TRUE NODE_ENV=production webpack -p --config webpack.config.js --bail",
|
||||
"prebuild-watch": "yarn generate-introspection",
|
||||
"build-watch": "WEBPACK=TRUE NODE_ENV=development webpack --progress --config webpack.config.js --watch",
|
||||
"lint": "yamllint locales/*.yml && eslint --ext=.js --ext=.json bin/* .",
|
||||
"lint-fix": "yarn lint --fix",
|
||||
"jest-watch": "TEST_MODE=unit NODE_ENV=test jest --watch",
|
||||
"e2e-ci": "./scripts/e2e-ci.sh",
|
||||
"e2e-browserstack": "NODE_ENV=test ./scripts/e2e-browserstack.js --config nightwatch-browserstack.conf.js",
|
||||
"generate-introspection": "WEBPACK=TRUE NODE_ENV=test ./scripts/generateIntrospectionResult.js",
|
||||
"watch": "npm-run-all generate-introspection --parallel watch:*",
|
||||
"watch:client": "NODE_ENV=development webpack --progress --watch",
|
||||
"watch:server": "nodemon --config .nodemon.json",
|
||||
"start:development": "NODE_ENV=development ./bin/cli -c .env serve -j -w",
|
||||
"start:production": "NODE_ENV=production ./bin/cli serve -j -w",
|
||||
"build": "NODE_ENV=production webpack -p --bail",
|
||||
"lint:yaml": "yamllint locales/*.yml",
|
||||
"lint:js": "eslint --ext=.js --ext=.json bin/* .",
|
||||
"lint": "npm-run-all lint:*",
|
||||
"plugins:reconcile": "./bin/cli plugins reconcile",
|
||||
"test": "npm-run-all test:client test:server",
|
||||
"test:server": "TEST_MODE=unit NODE_ENV=test mocha -R ${MOCHA_REPORTER:-spec}",
|
||||
"test:client": "TEST_MODE=unit NODE_ENV=test jest",
|
||||
"test:client:watch": "TEST_MODE=unit NODE_ENV=test jest --watch",
|
||||
"pree2e": "selenium-standalone install",
|
||||
"pree2e:ci": "selenium-standalone install",
|
||||
"pree2e:browserstack": "selenium-standalone install",
|
||||
"e2e": "NODE_ENV=test nightwatch",
|
||||
"test": "TEST_MODE=unit NODE_ENV=test jest && TEST_MODE=unit NODE_ENV=test mocha -R ${MOCHA_REPORTER:-spec}",
|
||||
"test-cover": "TEST_MODE=unit NODE_ENV=test istanbul cover _mocha --report text --check-coverage -- -R spec",
|
||||
"heroku-postbuild": "./bin/cli plugins reconcile && yarn build",
|
||||
"generate-introspection": "WEBPACK=TRUE NODE_ENV=test ./scripts/generateIntrospectionResult.js"
|
||||
"e2e:ci": "./scripts/e2e-ci.sh",
|
||||
"e2e:browserstack": "NODE_ENV=test ./scripts/e2e-browserstack.js --config nightwatch-browserstack.conf.js",
|
||||
"heroku-postbuild": "npm-run-all plugins:reconcile build"
|
||||
},
|
||||
"talk": {
|
||||
"migration": {
|
||||
"minVersion": 1496771633
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"pre-git": {
|
||||
"commit-msg": [],
|
||||
"pre-commit": [
|
||||
"yarn lint",
|
||||
"yarn test"
|
||||
],
|
||||
"pre-push": [
|
||||
"yarn test"
|
||||
],
|
||||
"post-commit": [],
|
||||
"post-merge": []
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/coralproject/talk.git"
|
||||
@@ -122,7 +113,6 @@
|
||||
"imports-loader": "^0.7.1",
|
||||
"inquirer": "^3.2.2",
|
||||
"ioredis": "3.1.4",
|
||||
"istanbul": "^1.1.0-alpha.1",
|
||||
"joi": "^10.6.0",
|
||||
"json-loader": "^0.5.7",
|
||||
"jsonwebtoken": "^7.4.3",
|
||||
@@ -144,6 +134,7 @@
|
||||
"node-emoji": "^1.8.1",
|
||||
"node-fetch": "^1.7.2",
|
||||
"nodemailer": "^2.6.4",
|
||||
"npm-run-all": "^4.1.1",
|
||||
"passport": "^0.4.0",
|
||||
"passport-jwt": "^3.0.0",
|
||||
"passport-local": "^1.0.0",
|
||||
@@ -172,7 +163,6 @@
|
||||
"redux": "^3.6.0",
|
||||
"redux-thunk": "^2.1.0",
|
||||
"resolve": "^1.4.0",
|
||||
"selenium-standalone": "^6.11.0",
|
||||
"semver": "^5.4.1",
|
||||
"simplemde": "^1.11.2",
|
||||
"smoothscroll-polyfill": "^0.3.5",
|
||||
@@ -199,6 +189,7 @@
|
||||
"browserstack-local": "^1.3.0",
|
||||
"chai": "^3.5.0",
|
||||
"chai-as-promised": "^6.0.0",
|
||||
"chai-datetime": "^1.5.0",
|
||||
"chai-http": "^3.0.0",
|
||||
"enzyme": "^3.0.0",
|
||||
"enzyme-adapter-react-15": "^1.0.0",
|
||||
@@ -210,16 +201,12 @@
|
||||
"mocha-junit-reporter": "^1.12.1",
|
||||
"nightwatch": "^0.9.16",
|
||||
"nodemon": "^1.11.0",
|
||||
"pre-git": "^3.15.3",
|
||||
"selenium-standalone": "^6.11.0",
|
||||
"sinon": "^3.2.1",
|
||||
"sinon-chai": "^2.13.0",
|
||||
"yaml-lint": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^8"
|
||||
},
|
||||
"release": {
|
||||
"analyzeCommits": "simple-commit-message"
|
||||
},
|
||||
"snyk": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ module.exports = {
|
||||
DELETE_ACTION: 'DELETE_ACTION',
|
||||
EDIT_NAME: 'EDIT_NAME',
|
||||
EDIT_COMMENT: 'EDIT_COMMENT',
|
||||
REJECT_USERNAME: 'REJECT_USERNAME',
|
||||
SET_USER_STATUS: 'SET_USER_STATUS',
|
||||
SUSPEND_USER: 'SUSPEND_USER',
|
||||
SET_USER_USERNAME_STATUS: 'SET_USER_USERNAME_STATUS',
|
||||
SET_USER_BAN_STATUS: 'SET_USER_BAN_STATUS',
|
||||
SET_USER_SUSPENSION_STATUS: 'SET_USER_SUSPENSION_STATUS',
|
||||
SET_COMMENT_STATUS: 'SET_COMMENT_STATUS',
|
||||
ADD_COMMENT_TAG: 'ADD_COMMENT_TAG',
|
||||
REMOVE_COMMENT_TAG: 'REMOVE_COMMENT_TAG',
|
||||
|
||||
@@ -5,16 +5,13 @@ const subscription = require('./subscription');
|
||||
module.exports = [
|
||||
(user /* , perm*/) => {
|
||||
|
||||
// this runs before everything
|
||||
if (
|
||||
user.status === 'BANNED' ||
|
||||
(user.suspension.until && user.suspension.until > new Date())
|
||||
) {
|
||||
// If a user is banned or currently suspended, then they aren't allowed to
|
||||
// do anything.
|
||||
if (user.banned || user.suspended) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
query,
|
||||
mutation,
|
||||
subscription,
|
||||
]
|
||||
;
|
||||
];
|
||||
|
||||
@@ -8,23 +8,29 @@ module.exports = (user, perm) => {
|
||||
case types.DELETE_ACTION:
|
||||
case types.EDIT_NAME:
|
||||
case types.EDIT_COMMENT:
|
||||
|
||||
// Anyone can do these things if they aren't suspended or banned.
|
||||
return true;
|
||||
|
||||
case types.ADD_COMMENT_TAG:
|
||||
case types.REMOVE_COMMENT_TAG:
|
||||
return check(user, ['ADMIN', 'MODERATOR', 'STAFF']);
|
||||
|
||||
case types.UPDATE_USER_ROLES:
|
||||
case types.REJECT_USERNAME:
|
||||
case types.SET_USER_STATUS:
|
||||
case types.SUSPEND_USER:
|
||||
case types.SET_COMMENT_STATUS:
|
||||
case types.SET_USER_USERNAME_STATUS:
|
||||
case types.SET_USER_BAN_STATUS:
|
||||
case types.SET_USER_SUSPENSION_STATUS:
|
||||
case types.UPDATE_CONFIG:
|
||||
case types.UPDATE_SETTINGS:
|
||||
case types.UPDATE_ASSET_SETTINGS:
|
||||
case types.UPDATE_ASSET_STATUS:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
|
||||
case types.CREATE_TOKEN:
|
||||
case types.REVOKE_TOKEN:
|
||||
return check(user, ['ADMIN']);
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
+4
-10
@@ -4,25 +4,19 @@ const types = require('../constants');
|
||||
module.exports = (user, perm) => {
|
||||
switch (perm) {
|
||||
case types.SEARCH_ASSETS:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SEARCH_OTHER_USERS:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SEARCH_ACTIONS:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SEARCH_OTHERS_COMMENTS:
|
||||
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']);
|
||||
case types.VIEW_SUSPENSION_INFO:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.VIEW_PROTECTED_SETTINGS:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
|
||||
case types.LIST_OWN_TOKENS:
|
||||
return check(user, ['ADMIN']);
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -4,21 +4,15 @@ const types = require('../constants');
|
||||
module.exports = (user, perm) => {
|
||||
switch (perm) {
|
||||
case types.SUBSCRIBE_COMMENT_FLAGGED:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SUBSCRIBE_COMMENT_ACCEPTED:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SUBSCRIBE_COMMENT_REJECTED:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SUBSCRIBE_ALL_COMMENT_EDITED:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SUBSCRIBE_ALL_COMMENT_ADDED:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SUBSCRIBE_ALL_USER_SUSPENDED:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SUBSCRIBE_ALL_USER_BANNED:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SUBSCRIBE_ALL_USERNAME_REJECTED:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -98,13 +98,4 @@ router.put('/password/reset', async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/username', authorization.needed(), async (req, res, next) => {
|
||||
try {
|
||||
await UsersService.editName(req.user.id, req.body.username);
|
||||
res.status(204).end();
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -4,11 +4,7 @@ const UsersService = require('../../../services/users');
|
||||
const mailer = require('../../../services/mailer');
|
||||
const errors = require('../../../errors');
|
||||
const authorization = require('../../../middleware/authorization');
|
||||
const i18n = require('../../../services/i18n');
|
||||
const Limit = require('../../../services/limit');
|
||||
const {
|
||||
ROOT_URL
|
||||
} = require('../../../config');
|
||||
|
||||
router.get('/', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => {
|
||||
|
||||
@@ -60,36 +56,6 @@ router.post('/:user_id/role', authorization.needed('ADMIN', 'MODERATOR'), async
|
||||
}
|
||||
});
|
||||
|
||||
// update the status of a user
|
||||
router.post('/:user_id/status', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => {
|
||||
let {status} = req.body;
|
||||
|
||||
try {
|
||||
let user = await UsersService.setStatus(req.params.user_id, status);
|
||||
if (!user) {
|
||||
return next(errors.ErrNotFound);
|
||||
}
|
||||
|
||||
if (user.status === 'BANNED') {
|
||||
req.pubsub.publish('userBanned', user);
|
||||
}
|
||||
|
||||
// TODO: investigate why this is returning a value? Also why is this a POST vs PUT?
|
||||
res.status(201).json(user.status);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:user_id/username-enable', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => {
|
||||
try {
|
||||
await UsersService.toggleNameEdit(req.params.user_id, true);
|
||||
res.status(204).end();
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:user_id/email', authorization.needed('ADMIN', 'MODERATOR'), async (req, res, next) => {
|
||||
try {
|
||||
let user = await UsersService.findById(req.params.user_id);
|
||||
@@ -114,26 +80,6 @@ router.post('/:user_id/email', authorization.needed('ADMIN', 'MODERATOR'), async
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* SendEmailConfirmation sends a confirmation email to the user.
|
||||
* @param {String} userID the id for the user to send the email to
|
||||
* @param {String} email the email for the user to send the email to
|
||||
*/
|
||||
const SendEmailConfirmation = async (user, email, referer) => {
|
||||
let token = await UsersService.createEmailConfirmToken(user, email, referer);
|
||||
|
||||
return mailer.sendSimple({
|
||||
template: 'email-confirm',
|
||||
locals: {
|
||||
token,
|
||||
rootURL: ROOT_URL,
|
||||
email
|
||||
},
|
||||
subject: i18n.t('email.confirm.subject'),
|
||||
to: email
|
||||
});
|
||||
};
|
||||
|
||||
// create a local user.
|
||||
router.post('/', async (req, res, next) => {
|
||||
const {email, password, username} = req.body;
|
||||
@@ -144,7 +90,7 @@ router.post('/', async (req, res, next) => {
|
||||
|
||||
// Send an email confirmation. The Front end will know about the
|
||||
// requireEmailConfirmation as it's included in the settings get endpoint.
|
||||
await SendEmailConfirmation(user, email, redirectUri);
|
||||
await UsersService.sendEmailConfirmation(user, email, redirectUri);
|
||||
|
||||
res.status(201).json(user);
|
||||
} catch (e) {
|
||||
@@ -152,26 +98,6 @@ router.post('/', async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:user_id/actions', authorization.needed(), async (req, res, next) => {
|
||||
const {
|
||||
action_type,
|
||||
metadata
|
||||
} = req.body;
|
||||
|
||||
try {
|
||||
let action = await UsersService.addAction(req.params.user_id, req.user.id, action_type, metadata);
|
||||
|
||||
// Set the user status to "pending" for review by moderators
|
||||
if (action_type === 'FLAG') {
|
||||
await UsersService.setStatus(req.params.user_id, 'PENDING');
|
||||
}
|
||||
|
||||
res.status(201).json(action);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
});
|
||||
|
||||
// This will allow 1 try every minute.
|
||||
const resendRateLimiter = new Limit('/api/v1/users/resend-verify', 1, '1m');
|
||||
|
||||
@@ -205,7 +131,7 @@ router.post('/resend-verify', async (req, res, next) => {
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
await SendEmailConfirmation(user, email, redirectUri);
|
||||
await UsersService.sendEmailConfirmation(user, email, redirectUri);
|
||||
|
||||
res.status(204).end();
|
||||
} catch (e) {
|
||||
@@ -234,7 +160,7 @@ router.post('/:user_id/email/confirm', authorization.needed('ADMIN', 'MODERATOR'
|
||||
}
|
||||
|
||||
// Send the email to the first local profile that was found.
|
||||
await SendEmailConfirmation(user, localProfile.id);
|
||||
await UsersService.sendEmailConfirmation(user, localProfile.id);
|
||||
|
||||
res.status(204).end();
|
||||
} catch (e) {
|
||||
|
||||
+45
-30
@@ -12,6 +12,7 @@ const {createGraphOptions} = require('../graph');
|
||||
const accepts = require('accepts');
|
||||
const apollo = require('graphql-server-express');
|
||||
const {DISABLE_STATIC_SERVER} = require('../config');
|
||||
const SetupService = require('../services/setup');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -19,29 +20,29 @@ const router = express.Router();
|
||||
// STATIC FILES
|
||||
//==============================================================================
|
||||
|
||||
// If the application is in production mode, then add gzip rewriting for the
|
||||
// content.
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
router.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();
|
||||
});
|
||||
}
|
||||
|
||||
if (!DISABLE_STATIC_SERVER) {
|
||||
|
||||
// If the application is in production mode, then add gzip rewriting for the
|
||||
// content.
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
router.get('*.js', (req, res, next) => {
|
||||
const accept = accepts(req);
|
||||
if (accept.encoding(['gzip']) === 'gzip') {
|
||||
|
||||
// Adjust 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 declare 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();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve the directories under public/dist from this router.
|
||||
*/
|
||||
@@ -100,7 +101,7 @@ if (process.env.NODE_ENV !== 'production') {
|
||||
});
|
||||
});
|
||||
|
||||
// GraphQL documention.
|
||||
// GraphQL documentation.
|
||||
router.get('/admin/docs', (req, res) => {
|
||||
res.render('admin/docs');
|
||||
});
|
||||
@@ -118,14 +119,28 @@ router.use('/embed', require('./embed'));
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
router.use('/assets', require('./assets'));
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
return res.render('article', {
|
||||
title: 'Coral Talk',
|
||||
asset_url: '',
|
||||
asset_id: '',
|
||||
body: '',
|
||||
basePath: '/client/embed/stream'
|
||||
});
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
await SetupService.isAvailable();
|
||||
return res.redirect('/admin/install');
|
||||
} catch (e) {
|
||||
return res.render('article', {
|
||||
title: 'Coral Talk',
|
||||
asset_url: '',
|
||||
asset_id: '',
|
||||
body: '',
|
||||
basePath: '/client/embed/stream'
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
await SetupService.isAvailable();
|
||||
return res.redirect('/admin/install');
|
||||
} catch (e) {
|
||||
return res.redirect('/admin');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -29,7 +29,7 @@ if [[ "${CIRCLE_BRANCH}" == "master" ]]; then
|
||||
|
||||
echo "-- Start e2e for $1 #$try --"
|
||||
|
||||
REPORTS_FOLDER="$CIRCLE_TEST_REPORTS/$1" yarn e2e-browserstack --env "$1"
|
||||
REPORTS_FOLDER="$CIRCLE_TEST_REPORTS/$1" yarn test:e2e:browserstack --env "$1"
|
||||
|
||||
# Determine exit code.
|
||||
result=$?
|
||||
@@ -85,7 +85,7 @@ if [[ "${CIRCLE_BRANCH}" == "master" ]]; then
|
||||
exit $exitCode
|
||||
else
|
||||
# When browserstack is not available test locally using chrome headless.
|
||||
REPORTS_FOLDER="$CIRCLE_TEST_REPORTS/chrome" yarn e2e -- --env chrome-headless
|
||||
REPORTS_FOLDER="$CIRCLE_TEST_REPORTS/chrome" yarn test:e2e --env chrome-headless
|
||||
|
||||
# Will exit with status of last command.
|
||||
exit $?
|
||||
|
||||
@@ -82,7 +82,7 @@ async function onListening() {
|
||||
let bind = typeof addr === 'string'
|
||||
? `pipe ${addr}`
|
||||
: `port ${addr.port}`;
|
||||
debug(`API Server Listening on ${bind}`);
|
||||
console.log(`API Server Listening on ${bind}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,7 +141,7 @@ async function serve({jobs = true, websockets = true} = {}) {
|
||||
|
||||
// Mount the websocket server if requested.
|
||||
if (websockets) {
|
||||
debug(`Websocket Server Listening on ${port}`);
|
||||
console.log(`Websocket Server Listening on ${port}`);
|
||||
|
||||
// Mount the subscriptions server on the application server.
|
||||
createSubscriptionManager(server);
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
const CommentModel = require('../models/comment');
|
||||
const AssetModel = require('../models/asset');
|
||||
const SettingsService = require('./settings');
|
||||
const domainlist = require('./domainlist');
|
||||
const DomainList = require('./domain_list');
|
||||
const errors = require('../errors');
|
||||
const merge = require('lodash/merge');
|
||||
|
||||
@@ -64,7 +64,7 @@ module.exports = class AssetsService {
|
||||
|
||||
// Check the URL to confirm that is in the domain whitelist
|
||||
return Promise.all([
|
||||
domainlist.urlCheck(url),
|
||||
DomainList.urlCheck(url),
|
||||
SettingsService.retrieve()
|
||||
]).then(([whitelisted, settings]) => {
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const debug = require('debug')('talk:services:domainlist');
|
||||
const debug = require('debug')('talk:services:domain_list');
|
||||
const _ = require('lodash');
|
||||
const SettingsService = require('./settings');
|
||||
|
||||
@@ -8,7 +8,7 @@ const {ROOT_URL} = require('../config');
|
||||
* The root domainlist object.
|
||||
* @type {Object}
|
||||
*/
|
||||
class Domainlist {
|
||||
class DomainList {
|
||||
|
||||
constructor() {
|
||||
this.lists = {
|
||||
@@ -35,7 +35,7 @@ class Domainlist {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lists.whitelist = Domainlist.parseList(lists.whitelist);
|
||||
this.lists.whitelist = DomainList.parseList(lists.whitelist);
|
||||
debug(`Added ${lists.whitelist.length} domains to the whitelist.`);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class Domainlist {
|
||||
match(list, url) {
|
||||
|
||||
// Parse the url that we're matching with.
|
||||
const domainToMatch = Domainlist.parseURL(url);
|
||||
const domainToMatch = DomainList.parseURL(url);
|
||||
|
||||
// This will return true in the event that at least one blockword is found
|
||||
// in the phrase.
|
||||
@@ -61,7 +61,7 @@ class Domainlist {
|
||||
* @returns {Boolean} true if the domains match
|
||||
*/
|
||||
static matchMount(url) {
|
||||
return Domainlist.parseURL(url) === Domainlist.parseURL(ROOT_URL);
|
||||
return DomainList.parseURL(url) === DomainList.parseURL(ROOT_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,7 +70,7 @@ class Domainlist {
|
||||
* @return {Array} the parsed list
|
||||
*/
|
||||
static parseList(list) {
|
||||
return _.uniq(list.map((domain) => Domainlist.parseURL(domain)));
|
||||
return _.uniq(list.map((domain) => DomainList.parseURL(domain)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,7 +95,7 @@ class Domainlist {
|
||||
}
|
||||
|
||||
static async urlCheck(url) {
|
||||
const dl = new Domainlist();
|
||||
const dl = new DomainList();
|
||||
|
||||
// Load the domain list.
|
||||
await dl.load();
|
||||
@@ -106,4 +106,4 @@ class Domainlist {
|
||||
|
||||
}
|
||||
|
||||
module.exports = Domainlist;
|
||||
module.exports = DomainList;
|
||||
+42
-251
@@ -1,4 +1,3 @@
|
||||
const assert = require('assert');
|
||||
const uuid = require('uuid');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const errors = require('../errors');
|
||||
@@ -15,7 +14,6 @@ const {
|
||||
const debug = require('debug')('talk:services:users');
|
||||
|
||||
const UserModel = require('../models/user');
|
||||
const USER_STATUS = require('../models/enum/user_status');
|
||||
const USER_ROLES = require('../models/enum/user_roles');
|
||||
|
||||
const RECAPTCHA_WINDOW = '10m'; // 10 minutes.
|
||||
@@ -23,8 +21,9 @@ const RECAPTCHA_INCORRECT_TRIGGER = 5; // after 3 incorrect attempts, recaptcha
|
||||
|
||||
const ActionsService = require('./actions');
|
||||
const MailerService = require('./mailer');
|
||||
const i18n = require('./i18n');
|
||||
const Wordlist = require('./wordlist');
|
||||
const Domainlist = require('./domainlist');
|
||||
const DomainList = require('./domain_list');
|
||||
const {escapeRegExp} = require('./regex');
|
||||
|
||||
const EMAIL_CONFIRM_JWT_SUBJECT = 'email_confirm';
|
||||
@@ -181,13 +180,40 @@ module.exports = class UsersService {
|
||||
lowercaseUsername: username.toLowerCase(),
|
||||
roles: [],
|
||||
profiles: [{id, provider}],
|
||||
canEditName: true
|
||||
status: {
|
||||
username: {
|
||||
status: 'UNSET',
|
||||
history: {
|
||||
status: 'UNSET'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return user.save();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* sendEmailConfirmation sends a confirmation email to the user.
|
||||
* @param {String} user the user to send the email to
|
||||
* @param {String} email the email for the user to send the email to
|
||||
*/
|
||||
static async sendEmailConfirmation(user, email, redirectURI = ROOT_URL) {
|
||||
let token = await UsersService.createEmailConfirmToken(user, email, redirectURI);
|
||||
|
||||
return MailerService.sendSimple({
|
||||
template: 'email-confirm',
|
||||
locals: {
|
||||
token,
|
||||
rootURL: ROOT_URL,
|
||||
email
|
||||
},
|
||||
subject: i18n.t('email.confirm.subject'),
|
||||
to: email
|
||||
});
|
||||
}
|
||||
|
||||
static async changePassword(id, password) {
|
||||
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
|
||||
|
||||
@@ -289,7 +315,15 @@ module.exports = class UsersService {
|
||||
id: email,
|
||||
provider: 'local'
|
||||
}
|
||||
]
|
||||
],
|
||||
status: {
|
||||
username: {
|
||||
status: 'SET',
|
||||
history: {
|
||||
status: 'SET'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -307,36 +341,6 @@ module.exports = class UsersService {
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables a given user account.
|
||||
* @param {String} id id of a user
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
static disableUser(id) {
|
||||
return UserModel.update({
|
||||
id
|
||||
}, {
|
||||
$set: {
|
||||
disabled: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables a given user account.
|
||||
* @param {String} id id of a user
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
static enableUser(id) {
|
||||
return UserModel.update({
|
||||
id
|
||||
}, {
|
||||
$set: {
|
||||
disabled: false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a role to a user.
|
||||
* @param {String} id id of a user
|
||||
@@ -380,131 +384,6 @@ module.exports = class UsersService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set status of a user.
|
||||
* @param {String} id id of a user
|
||||
* @param {String} status status to set
|
||||
* @param {Function} done callback after the operation is complete
|
||||
*/
|
||||
static async setStatus(id, status) {
|
||||
|
||||
// Check to see if the user status is in the allowable set of roles.
|
||||
if (USER_STATUS.indexOf(status) === -1) {
|
||||
|
||||
// User status is not supported! Error out here.
|
||||
throw new Error(`status ${status} is not supported`);
|
||||
}
|
||||
|
||||
// TODO: current updating status behavior is weird.
|
||||
// once a user has been `APPROVED` its status cannot be
|
||||
// changed anymore.
|
||||
const user = await UserModel.findOneAndUpdate({
|
||||
id,
|
||||
status: {
|
||||
$ne: 'APPROVED'
|
||||
}
|
||||
}, {
|
||||
$set: {
|
||||
status
|
||||
}
|
||||
}, {
|
||||
new: true,
|
||||
});
|
||||
|
||||
if (status === 'BANNED') {
|
||||
let localProfile = user.profiles.find((profile) => profile.provider === 'local');
|
||||
if (localProfile) {
|
||||
const options =
|
||||
{
|
||||
template: 'banned', // needed to know which template to render!
|
||||
locals: { // specifies the template locals.
|
||||
body: 'In accordance with The Coral Project’s community guidelines, your account has been banned. You are now longer allowed to comment, flag or engage with our community.'
|
||||
},
|
||||
subject: 'Your account has been banned',
|
||||
to: localProfile.id // This only works if the user has registered via e-mail.
|
||||
// We may want a standard way to access a user's e-mail address in the future
|
||||
};
|
||||
await MailerService.sendSimple(options);
|
||||
}
|
||||
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspend a user until specified time.
|
||||
* @param {String} id id of a user
|
||||
* @param {String} message message to be send to the user
|
||||
* @param {Date} until date until the suspension is valid.
|
||||
*/
|
||||
static async suspendUser(id, message, until) {
|
||||
const user = await UserModel.findOneAndUpdate({id}, {
|
||||
$set: {
|
||||
suspension: {
|
||||
until,
|
||||
},
|
||||
}
|
||||
}, {
|
||||
new: true,
|
||||
});
|
||||
|
||||
if (message) {
|
||||
let localProfile = user.profiles.find((profile) => profile.provider === 'local');
|
||||
if (localProfile) {
|
||||
const options =
|
||||
{
|
||||
template: 'suspension', // needed to know which template to render!
|
||||
locals: { // specifies the template locals.
|
||||
body: message
|
||||
},
|
||||
subject: 'Your account has been suspended',
|
||||
to: localProfile.id // This only works if the user has registered via e-mail.
|
||||
// We may want a standard way to access a user's e-mail address in the future
|
||||
};
|
||||
|
||||
await MailerService.sendSimple(options);
|
||||
}
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject username. It changes the status to BANNED and canEditName to True.
|
||||
* @param {String} id id of a user
|
||||
* @param {String} message message to be send to the user
|
||||
* @param {Date} until date until the suspension is valid.
|
||||
*/
|
||||
static async rejectUsername(id, message) {
|
||||
const user = await UserModel.findOneAndUpdate({id}, {
|
||||
$set: {
|
||||
status: 'BANNED',
|
||||
canEditName: true,
|
||||
}
|
||||
}, {
|
||||
new: true,
|
||||
});
|
||||
|
||||
if (message) {
|
||||
let localProfile = user.profiles.find(({provider}) => provider === 'local');
|
||||
if (localProfile) {
|
||||
const options = {
|
||||
template: 'suspension', // needed to know which template to render!
|
||||
locals: { // specifies the template locals.
|
||||
body: message
|
||||
},
|
||||
subject: 'Email Suspension',
|
||||
to: localProfile.id // This only works if the user has registered via e-mail.
|
||||
// We may want a standard way to access a user's e-mail address in the future
|
||||
};
|
||||
|
||||
await MailerService.sendSimple(options);
|
||||
}
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a user with the id.
|
||||
* @param {String} id user id (uuid)
|
||||
@@ -567,7 +446,7 @@ module.exports = class UsersService {
|
||||
|
||||
const [user, domainValidated] = await Promise.all([
|
||||
UserModel.findOne({profiles: {$elemMatch: {id: email}}}),
|
||||
Domainlist.urlCheck(loc),
|
||||
DomainList.urlCheck(loc),
|
||||
]);
|
||||
if (!user) {
|
||||
|
||||
@@ -579,7 +458,7 @@ module.exports = class UsersService {
|
||||
|
||||
// If the domain didn't match any of the whitelisted domains and if it
|
||||
// didn't match the mount domain, then throw an error.
|
||||
if (!domainValidated && !Domainlist.matchMount(loc)) {
|
||||
if (!domainValidated && !DomainList.matchMount(loc)) {
|
||||
throw new Error('user supplied location exists on non-permitted domain');
|
||||
}
|
||||
|
||||
@@ -727,7 +606,7 @@ module.exports = class UsersService {
|
||||
*/
|
||||
static async createEmailConfirmToken(user, email, referer = ROOT_URL) {
|
||||
if (!email || typeof email !== 'string') {
|
||||
throw new Error('email is required when creating a JWT for resetting passord');
|
||||
throw new Error('email is required when creating a JWT for resetting password');
|
||||
}
|
||||
|
||||
// Conform the email to lowercase.
|
||||
@@ -791,97 +670,12 @@ module.exports = class UsersService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all users with pending 'ADMIN'ation actions.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static moderationQueue() {
|
||||
return UserModel.find({status: 'PENDING'});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives the user the ability to edit their username.
|
||||
* @param {String} id the id of the user to be toggled.
|
||||
* @param {Boolean} canEditName sets whether the user can edit their name.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static toggleNameEdit(id, canEditName) {
|
||||
return UserModel.update({id}, {
|
||||
$set: {canEditName}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the user's username.
|
||||
* @param {String} id The id of the user.
|
||||
* @param {String} username The new username for the user.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static async editName(id, username) {
|
||||
|
||||
// TODO: Revisit this when we revamped User status workflows.
|
||||
const queryUsernameRejected = {
|
||||
id,
|
||||
username: {$ne: username},
|
||||
status: 'BANNED',
|
||||
canEditName: true
|
||||
};
|
||||
|
||||
const queryCreateUsername = {
|
||||
id,
|
||||
status: 'ACTIVE',
|
||||
canEditName: true
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await UserModel.findOneAndUpdate({
|
||||
$or: [queryUsernameRejected, queryCreateUsername],
|
||||
}, {
|
||||
$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) {
|
||||
if (err.code === 11000) {
|
||||
throw errors.ErrUsernameTaken;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore another user
|
||||
* @param {String} id the id of the user that is ignoring another users
|
||||
* @param {Array<String>} usersToIgnore Array of user IDs to ignore
|
||||
*/
|
||||
static async ignoreUsers(id, usersToIgnore) {
|
||||
assert(Array.isArray(usersToIgnore), 'usersToIgnore is an array');
|
||||
assert(usersToIgnore.every((u) => typeof u === 'string'), 'usersToIgnore is an array of string user IDs');
|
||||
if (usersToIgnore.includes(id)) {
|
||||
throw new Error('Users cannot ignore themselves');
|
||||
}
|
||||
@@ -891,7 +685,6 @@ module.exports = class UsersService {
|
||||
throw errors.ErrCannotIgnoreStaff;
|
||||
}
|
||||
|
||||
// TODO: For each usersToIgnore, make sure they exist?
|
||||
return UserModel.update({id}, {
|
||||
$addToSet: {
|
||||
ignoresUsers: {
|
||||
@@ -907,8 +700,6 @@ module.exports = class UsersService {
|
||||
* @param {Array<String>} usersToStopIgnoring Array of user IDs to stop ignoring
|
||||
*/
|
||||
static async stopIgnoringUsers(id, usersToStopIgnoring) {
|
||||
assert(Array.isArray(usersToStopIgnoring), 'usersToStopIgnoring is an array');
|
||||
assert(usersToStopIgnoring.every((u) => typeof u === 'string'), 'usersToStopIgnoring is an array of string user IDs');
|
||||
await UserModel.update({id}, {
|
||||
$pullAll: {
|
||||
ignoresUsers: usersToStopIgnoring
|
||||
|
||||
@@ -26,7 +26,6 @@ beforeEach(async () => {
|
||||
}));
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
after(async function() {
|
||||
mongoose.disconnect();
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
process.on('unhandledRejection', function(reason, promise) {
|
||||
console.error(promise);
|
||||
});
|
||||
@@ -48,7 +48,7 @@ describe('graph.mutations.addTag', () => {
|
||||
Object.entries({
|
||||
'anonymous': undefined,
|
||||
'regular commenter': new UserModel({}),
|
||||
'banned moderator': new UserModel({roles: ['MODERATOR'], status: 'BANNED'})
|
||||
'banned moderator': new UserModel({roles: ['MODERATOR'], banned: true})
|
||||
}).forEach(([ userDescription, user ]) => {
|
||||
it(userDescription, async () => {
|
||||
const context = new Context({user});
|
||||
|
||||
@@ -71,27 +71,28 @@ describe('graph.mutations.createComment', () => {
|
||||
beforeEach(() => AssetModel.create({id: '123'}));
|
||||
|
||||
[
|
||||
{user: new UserModel({status: 'ACTIVE'}), error: null},
|
||||
{user: new UserModel({status: 'BANNED'}), error: 'NOT_AUTHORIZED'},
|
||||
{user: new UserModel({status: 'PENDING'}), error: null},
|
||||
{user: new UserModel({status: 'APPROVED'}), error: null}
|
||||
{user: new UserModel({}), error: null},
|
||||
{user: new UserModel({banned: true}), error: 'NOT_AUTHORIZED'},
|
||||
{user: new UserModel({suspended: new Date((new Date()).getTime() - (10 * 86400000))}), error: null},
|
||||
{user: new UserModel({suspended: new Date((new Date()).getTime() + (10 * 86400000))}), error: 'NOT_AUTHORIZED'},
|
||||
].forEach(({user, error}) => {
|
||||
describe(`user.status=${user.status}`, () => {
|
||||
it(error ? 'does not create the comment' : 'creates the comment', () => {
|
||||
describe(`user.banned=${user.banned} user.suspended=${user.suspended}`, () => {
|
||||
it(error ? 'does not create the comment' : 'creates the comment', async () => {
|
||||
const context = new Context({user});
|
||||
const {data, errors} = await graphql(schema, query, {}, context);
|
||||
|
||||
return graphql(schema, query, {}, context)
|
||||
.then(({data, errors}) => {
|
||||
expect(errors).to.be.undefined;
|
||||
if (error) {
|
||||
expect(data.createComment).to.have.property('comment').null;
|
||||
expect(data.createComment).to.have.property('errors').not.null;
|
||||
expect(data.createComment.errors[0]).to.have.property('translation_key', error);
|
||||
} else {
|
||||
expect(data.createComment).to.have.property('comment').not.null;
|
||||
expect(data.createComment).to.have.property('errors').null;
|
||||
}
|
||||
});
|
||||
expect(errors).to.be.undefined;
|
||||
if (error) {
|
||||
expect(data.createComment).to.have.property('comment').null;
|
||||
expect(data.createComment).to.have.property('errors').not.null;
|
||||
expect(data.createComment.errors[0]).to.have.property('translation_key', error);
|
||||
} else {
|
||||
if (data.createComment.errors && data.createComment.errors.length > 0) {
|
||||
console.error(data.createComment.errors);
|
||||
}
|
||||
expect(data.createComment).to.have.property('errors').null;
|
||||
expect(data.createComment).to.have.property('comment').not.null;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -109,7 +110,7 @@ describe('graph.mutations.createComment', () => {
|
||||
beforeEach(() => asset.save());
|
||||
|
||||
it(error ? 'does not create the comment' : 'creates the comment', () => {
|
||||
const context = new Context({user: new UserModel({status: 'ACTIVE'})});
|
||||
const context = new Context({user: new UserModel({})});
|
||||
|
||||
return graphql(schema, query, {}, context)
|
||||
.then(({data, errors}) => {
|
||||
@@ -142,7 +143,7 @@ describe('graph.mutations.createComment', () => {
|
||||
beforeEach(() => AssetModel.create({id: '123', settings: {moderation}}));
|
||||
|
||||
it(`creates comment with status=${status}`, () => {
|
||||
const context = new Context({user: new UserModel({status: 'ACTIVE'})});
|
||||
const context = new Context({user: new UserModel()});
|
||||
|
||||
return graphql(schema, query, {}, context)
|
||||
.then(({data, errors}) => {
|
||||
@@ -172,33 +173,30 @@ describe('graph.mutations.createComment', () => {
|
||||
].forEach(({message, body, status, flagged}) => {
|
||||
describe(message, () => {
|
||||
|
||||
it(`should create a comment with status=${status} and it ${flagged ? 'should' : 'should not'} be flagged`, () => {
|
||||
const context = new Context({user: new UserModel({status: 'ACTIVE'})});
|
||||
it(`should create a comment with status=${status} and it ${flagged ? 'should' : 'should not'} be flagged`, async () => {
|
||||
const context = new Context({user: new UserModel({})});
|
||||
|
||||
return graphql(schema, query, {}, context, {
|
||||
const {data, errors} = await graphql(schema, query, {}, context, {
|
||||
input: {
|
||||
asset_id: '123',
|
||||
body
|
||||
}
|
||||
})
|
||||
.then(({data, errors}) => {
|
||||
expect(errors).to.be.undefined;
|
||||
expect(data.createComment).to.have.property('comment').not.null;
|
||||
expect(data.createComment.comment).to.have.property('status', status);
|
||||
expect(data.createComment).to.have.property('errors').null;
|
||||
});
|
||||
|
||||
return ActionModel.find({
|
||||
item_id: data.createComment.comment.id,
|
||||
action_type: 'FLAG'
|
||||
});
|
||||
})
|
||||
.then((actions) => {
|
||||
if (flagged) {
|
||||
expect(actions).to.have.length(1);
|
||||
} else {
|
||||
expect(actions).to.have.length(0);
|
||||
}
|
||||
});
|
||||
expect(errors).to.be.undefined;
|
||||
expect(data.createComment).to.have.property('comment').not.null;
|
||||
expect(data.createComment.comment).to.have.property('status', status);
|
||||
expect(data.createComment).to.have.property('errors').null;
|
||||
|
||||
const actions = await ActionModel.find({
|
||||
item_id: data.createComment.comment.id,
|
||||
action_type: 'FLAG'
|
||||
});
|
||||
if (flagged) {
|
||||
expect(actions).to.have.length(1);
|
||||
} else {
|
||||
expect(actions).to.have.length(0);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
@@ -217,30 +215,26 @@ describe('graph.mutations.createComment', () => {
|
||||
].forEach(({roles, tag}) => {
|
||||
describe(`user.roles=${JSON.stringify(roles)}`, () => {
|
||||
|
||||
it(`creates comment ${tag ? `with tag=${tag}` : 'without tags'}`, () => {
|
||||
it(`creates comment ${tag ? `with tag=${tag}` : 'without tags'}`, async () => {
|
||||
const context = new Context({user: new UserModel({roles})});
|
||||
|
||||
return graphql(schema, query, {}, context)
|
||||
.then(({data, errors}) => {
|
||||
if (errors) {
|
||||
console.error(errors);
|
||||
}
|
||||
expect(errors).to.be.undefined;
|
||||
expect(data.createComment).to.have.property('comment').not.null;
|
||||
expect(data.createComment).to.have.property('errors').null;
|
||||
const {data, errors} = await graphql(schema, query, {}, context);
|
||||
|
||||
return CommentsService.findById(data.createComment.comment.id);
|
||||
})
|
||||
.then(({tags}) => {
|
||||
if (tag) {
|
||||
expect(tags).to.have.length(1);
|
||||
expect(tags[0].tag.name).to.have.equal(tag);
|
||||
} else {
|
||||
expect(tags).length(0);
|
||||
}
|
||||
});
|
||||
if (errors) {
|
||||
console.error(errors);
|
||||
}
|
||||
expect(errors).to.be.undefined;
|
||||
expect(data.createComment).to.have.property('comment').not.null;
|
||||
expect(data.createComment).to.have.property('errors').null;
|
||||
|
||||
const {tags} = await CommentsService.findById(data.createComment.comment.id);
|
||||
if (tag) {
|
||||
expect(tags).to.have.length(1);
|
||||
expect(tags[0].tag.name).to.have.equal(tag);
|
||||
} else {
|
||||
expect(tags).length(0);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ describe('graph.mutations.removeTag', () => {
|
||||
Object.entries({
|
||||
'anonymous': undefined,
|
||||
'regular commenter': new UserModel({}),
|
||||
'banned moderator': new UserModel({roles: ['MODERATOR'], status: 'BANNED'})
|
||||
'banned moderator': new UserModel({roles: ['MODERATOR'], banned: true})
|
||||
}).forEach(([userDescription, user]) => {
|
||||
it(userDescription, async function () {
|
||||
const context = new Context({user});
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
const {graphql} = require('graphql');
|
||||
|
||||
const schema = require('../../../../graph/schema');
|
||||
const Context = require('../../../../graph/context');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
const UserModel = require('../../../../models/user');
|
||||
const UsersService = require('../../../../services/users');
|
||||
|
||||
const {expect} = require('chai');
|
||||
|
||||
describe('graph.mutations.setUserBanStatus', () => {
|
||||
let user;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
|
||||
user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA');
|
||||
});
|
||||
|
||||
const setUserBanStatusMutation = `
|
||||
mutation SetUserBanStatus($user_id: ID!, $status: Boolean!) {
|
||||
setUserBanStatus(input: {
|
||||
id: $user_id,
|
||||
status: $status
|
||||
}) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
[
|
||||
{self: true, error: 'NOT_AUTHORIZED', roles: null},
|
||||
{self: true, error: 'NOT_AUTHORIZED', roles: ['STAFF']},
|
||||
{self: true, error: 'NOT_AUTHORIZED', roles: []},
|
||||
{error: 'NOT_AUTHORIZED', roles: null},
|
||||
{error: 'NOT_AUTHORIZED', roles: ['STAFF']},
|
||||
{error: 'NOT_AUTHORIZED', roles: []},
|
||||
{error: false, roles: ['MODERATOR']},
|
||||
{error: false, roles: ['ADMIN']},
|
||||
{error: false, roles: ['ADMIN', 'MODERATOR']},
|
||||
].forEach(({self, error, roles}) => {
|
||||
it(`${error ? 'can not' : 'can'} ban ${self ? 'themself' : 'another user'} as a user with roles ${roles && roles.length ? roles : JSON.stringify(roles)}`, async () => {
|
||||
const actor = new UserModel({roles});
|
||||
|
||||
// If we're testing self assign, set the id of the actor to the user
|
||||
// we're acting on.
|
||||
if (self) {
|
||||
actor.id = user.id;
|
||||
}
|
||||
|
||||
const ctx = new Context({user: actor});
|
||||
|
||||
const {data, errors} = await graphql(schema, setUserBanStatusMutation, {}, ctx, {
|
||||
user_id: user.id,
|
||||
status: true
|
||||
});
|
||||
|
||||
if (errors && errors.length > 0) {
|
||||
console.error(errors);
|
||||
}
|
||||
expect(errors).to.be.undefined;
|
||||
if (error) {
|
||||
expect(data.setUserBanStatus).to.have.property('errors').not.null;
|
||||
expect(data.setUserBanStatus.errors[0]).to.have.property('translation_key', error);
|
||||
} else {
|
||||
expect(data.setUserBanStatus).to.be.null;
|
||||
|
||||
user = await UserModel.findOne({id: user.id});
|
||||
|
||||
expect(user.status.banned.status).to.be.true;
|
||||
expect(user.status.banned.history).to.have.length(1);
|
||||
expect(user.status.banned.history[0]).to.have.property('status', true);
|
||||
expect(user.status.banned.history[0]).to.have.property('assigned_by', actor.id);
|
||||
expect(user.status.banned.history[0]).to.have.property('created_at').not.null;
|
||||
|
||||
expect(user.banned).to.be.true;
|
||||
|
||||
const res = await graphql(schema, setUserBanStatusMutation, {}, ctx, {
|
||||
user_id: user.id,
|
||||
status: false
|
||||
});
|
||||
if (res.errors && res.errors.length > 0) {
|
||||
console.error(res.errors);
|
||||
}
|
||||
expect(res.errors).to.be.undefined;
|
||||
expect(res.data.setUserBanStatus).to.be.null;
|
||||
|
||||
user = await UserModel.findOne({id: user.id});
|
||||
|
||||
expect(user.status.banned.status).to.be.false;
|
||||
expect(user.status.banned.history).to.have.length(2);
|
||||
expect(user.status.banned.history[0]).to.have.property('status').to.be.true;
|
||||
expect(user.status.banned.history[0]).to.have.property('assigned_by', actor.id);
|
||||
expect(user.status.banned.history[0]).to.have.property('created_at').not.null;
|
||||
expect(user.status.banned.history[1]).to.have.property('status').to.be.false;
|
||||
expect(user.status.banned.history[1]).to.have.property('assigned_by', actor.id);
|
||||
expect(user.status.banned.history[1]).to.have.property('created_at').not.null;
|
||||
|
||||
expect(user.banned).to.be.false;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
const {graphql} = require('graphql');
|
||||
const timekeeper = require('timekeeper');
|
||||
|
||||
const schema = require('../../../../graph/schema');
|
||||
const Context = require('../../../../graph/context');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
const UserModel = require('../../../../models/user');
|
||||
const UsersService = require('../../../../services/users');
|
||||
|
||||
const chai = require('chai');
|
||||
chai.use(require('chai-datetime'));
|
||||
const {expect} = chai;
|
||||
|
||||
describe('graph.mutations.setUserSuspensionStatus', () => {
|
||||
let user;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
|
||||
user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA');
|
||||
});
|
||||
|
||||
const setUserSuspensionStatusMutation = `
|
||||
mutation SetUserUsernameStatus($user_id: ID!, $until: Date) {
|
||||
setUserSuspensionStatus(input: {
|
||||
id: $user_id,
|
||||
until: $until
|
||||
}) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
[
|
||||
{self: true, error: 'NOT_AUTHORIZED', roles: null},
|
||||
{self: true, error: 'NOT_AUTHORIZED', roles: ['STAFF']},
|
||||
{self: true, error: 'NOT_AUTHORIZED', roles: []},
|
||||
{error: 'NOT_AUTHORIZED', roles: null},
|
||||
{error: 'NOT_AUTHORIZED', roles: ['STAFF']},
|
||||
{error: 'NOT_AUTHORIZED', roles: []},
|
||||
{error: false, roles: ['MODERATOR']},
|
||||
{error: false, roles: ['ADMIN']},
|
||||
{error: false, roles: ['ADMIN', 'MODERATOR']},
|
||||
].forEach(({self, error, roles}) => {
|
||||
it(`${error ? 'can not' : 'can'} suspend ${self ? 'themself' : 'another user'} as a user with roles ${roles && roles.length ? roles : JSON.stringify(roles)}`, async () => {
|
||||
const actor = new UserModel({roles});
|
||||
|
||||
// If we're testing self assign, set the id of the actor to the user
|
||||
// we're acting on.
|
||||
if (self) {
|
||||
actor.id = user.id;
|
||||
}
|
||||
|
||||
const ctx = new Context({user: actor});
|
||||
|
||||
const now = new Date();
|
||||
const oneHourFromNow = new Date(new Date(now).setHours(now.getHours() + 1));
|
||||
|
||||
const {data, errors} = await graphql(schema, setUserSuspensionStatusMutation, {}, ctx, {
|
||||
user_id: user.id,
|
||||
until: oneHourFromNow
|
||||
});
|
||||
|
||||
if (errors && errors.length > 0) {
|
||||
console.error(errors);
|
||||
}
|
||||
expect(errors).to.be.undefined;
|
||||
if (error) {
|
||||
expect(data.setUserSuspensionStatus).to.have.property('errors').not.null;
|
||||
expect(data.setUserSuspensionStatus.errors[0]).to.have.property('translation_key', error);
|
||||
} else {
|
||||
expect(data.setUserSuspensionStatus).to.be.null;
|
||||
|
||||
user = await UserModel.findOne({id: user.id});
|
||||
|
||||
// Mongoose messes with the date, check within a 2 second window.
|
||||
expect(user.status.suspension.until).to.be.withinTime(new Date(oneHourFromNow.getTime() - 1000), new Date(oneHourFromNow.getTime() + 1000));
|
||||
expect(user.status.suspension.history).to.have.length(1);
|
||||
expect(user.status.suspension.history[0]).to.have.property('until').to.be.withinTime(new Date(oneHourFromNow.getTime() - 1000), new Date(oneHourFromNow.getTime() + 1000));
|
||||
expect(user.status.suspension.history[0]).to.have.property('assigned_by', actor.id);
|
||||
expect(user.status.suspension.history[0]).to.have.property('created_at').not.null;
|
||||
|
||||
expect(user.suspended).to.be.true;
|
||||
timekeeper.travel(new Date(oneHourFromNow.getTime() + 10000));
|
||||
expect(user.suspended).to.be.false;
|
||||
timekeeper.reset();
|
||||
|
||||
const res = await graphql(schema, setUserSuspensionStatusMutation, {}, ctx, {
|
||||
user_id: user.id,
|
||||
until: null
|
||||
});
|
||||
if (res.errors && res.errors.length > 0) {
|
||||
console.error(res.errors);
|
||||
}
|
||||
expect(res.errors).to.be.undefined;
|
||||
expect(res.data.setUserSuspensionStatus).to.be.null;
|
||||
|
||||
user = await UserModel.findOne({id: user.id});
|
||||
|
||||
// Mongoose messes with the date, check within a 2 second window.
|
||||
expect(user.status.suspension.until).to.be.null;
|
||||
expect(user.status.suspension.history).to.have.length(2);
|
||||
expect(user.status.suspension.history[0]).to.have.property('until').to.be.withinTime(new Date(oneHourFromNow.getTime() - 1000), new Date(oneHourFromNow.getTime() + 1000));
|
||||
expect(user.status.suspension.history[0]).to.have.property('assigned_by', actor.id);
|
||||
expect(user.status.suspension.history[0]).to.have.property('created_at').not.null;
|
||||
expect(user.status.suspension.history[1]).to.have.property('until').to.be.null;
|
||||
expect(user.status.suspension.history[1]).to.have.property('assigned_by', actor.id);
|
||||
expect(user.status.suspension.history[1]).to.have.property('created_at').not.null;
|
||||
|
||||
expect(user.suspended).to.be.false;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
const {graphql} = require('graphql');
|
||||
|
||||
const schema = require('../../../../graph/schema');
|
||||
const Context = require('../../../../graph/context');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
const UserModel = require('../../../../models/user');
|
||||
const UsersService = require('../../../../services/users');
|
||||
|
||||
const chai = require('chai');
|
||||
chai.use(require('chai-datetime'));
|
||||
const {expect} = chai;
|
||||
|
||||
[
|
||||
{status: 'APPROVED', name: 'approve', mutation: 'approveUsername'},
|
||||
{status: 'REJECTED', name: 'reject', mutation: 'rejectUsername'}
|
||||
].forEach(({status, name, mutation}) => {
|
||||
describe(`graph.mutations.${mutation}`, () => {
|
||||
let user;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
|
||||
user = await UsersService.createLocalUser('usernameA@example.com', 'password', 'usernameA');
|
||||
});
|
||||
|
||||
const setUserUsernameStatusMutation = `
|
||||
mutation SetUserUsernameStatus($user_id: ID!) {
|
||||
${mutation}(id: $user_id) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
[
|
||||
{self: true, error: 'NOT_AUTHORIZED', roles: null},
|
||||
{self: true, error: 'NOT_AUTHORIZED', roles: ['STAFF']},
|
||||
{self: true, error: 'NOT_AUTHORIZED', roles: []},
|
||||
{error: 'NOT_AUTHORIZED', roles: null},
|
||||
{error: 'NOT_AUTHORIZED', roles: ['STAFF']},
|
||||
{error: 'NOT_AUTHORIZED', roles: []},
|
||||
{error: false, roles: ['MODERATOR']},
|
||||
{error: false, roles: ['ADMIN']},
|
||||
{error: false, roles: ['ADMIN', 'MODERATOR']},
|
||||
].forEach(({self, error, roles}) => {
|
||||
it(`${error ? 'can not' : 'can'} ${name} a username with the user roles ${roles && roles.length ? roles : JSON.stringify(roles)}${self ? ' on themself' : ''}`, async () => {
|
||||
const actor = new UserModel({roles});
|
||||
|
||||
// If we're testing self assign, set the id of the actor to the user
|
||||
// we're acting on.
|
||||
if (self) {
|
||||
actor.id = user.id;
|
||||
}
|
||||
|
||||
const ctx = new Context({user: actor});
|
||||
|
||||
const {data, errors} = await graphql(schema, setUserUsernameStatusMutation, {}, ctx, {
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
if (errors && errors.length > 0) {
|
||||
console.error(errors);
|
||||
}
|
||||
expect(errors).to.be.undefined;
|
||||
if (error) {
|
||||
expect(data[mutation]).to.have.property('errors').not.null;
|
||||
expect(data[mutation].errors[0]).to.have.property('translation_key', error);
|
||||
} else {
|
||||
expect(data[mutation]).to.be.null;
|
||||
|
||||
user = await UserModel.findOne({id: user.id});
|
||||
|
||||
expect(user.status.username.status).to.equal(status);
|
||||
expect(user.status.username.history).to.have.length(2);
|
||||
expect(user.status.username.history[0]).to.have.property('status', 'SET');
|
||||
expect(user.status.username.history[0]).to.have.property('assigned_by').is.null;
|
||||
expect(user.status.username.history[0]).to.have.property('created_at').not.null;
|
||||
expect(user.status.username.history[1]).to.have.property('status', status);
|
||||
expect(user.status.username.history[1]).to.have.property('assigned_by', actor.id);
|
||||
expect(user.status.username.history[1]).to.have.property('created_at').not.null;
|
||||
|
||||
expect(user.status.username.history[1].created_at).afterTime(user.status.username.history[0].created_at);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -22,8 +22,7 @@ describe('graph.mutations.updateAssetSettings', () => {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}`;
|
||||
|
||||
describe('context with different user roles', () => {
|
||||
|
||||
|
||||
@@ -21,8 +21,7 @@ describe('graph.mutations.updateSettings', () => {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}`;
|
||||
|
||||
describe('context with different user roles', () => {
|
||||
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
const passport = require('../../../passport');
|
||||
|
||||
const app = require('../../../../../app');
|
||||
|
||||
const UsersService = require('../../../../../services/users');
|
||||
const SettingsService = require('../../../../../services/settings');
|
||||
const settings = {id: '1', moderation: 'PRE', wordlist: {banned: ['bad words'], suspect: ['suspect words']}};
|
||||
|
||||
const chai = require('chai');
|
||||
chai.should();
|
||||
chai.use(require('chai-http'));
|
||||
const expect = chai.expect;
|
||||
|
||||
describe('/api/v1/account/username', () => {
|
||||
let mockUser;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init(settings);
|
||||
mockUser = await UsersService.createLocalUser('ana@gmail.com', '123321123', 'Ana');
|
||||
});
|
||||
|
||||
describe('#put', () => {
|
||||
it('it should enable a user to edit their username if canEditName is enabled', async () => {
|
||||
await chai.request(app)
|
||||
.post(`/api/v1/users/${mockUser.id}/username-enable`)
|
||||
.set(passport.inject({id: '456', roles: ['ADMIN']}));
|
||||
|
||||
const res = await chai.request(app)
|
||||
.put('/api/v1/account/username')
|
||||
.set(passport.inject({id: mockUser.id, roles: []}))
|
||||
.send({username: 'MojoJojo'});
|
||||
|
||||
expect(res).to.have.status(204);
|
||||
});
|
||||
|
||||
it('it should return an error if the wrong user tries to edit a username', async () => {
|
||||
await chai.request(app)
|
||||
.post(`/api/v1/users/${mockUser.id}/username-enable`)
|
||||
.set(passport.inject({id: '456', roles: ['ADMIN']}));
|
||||
|
||||
let res = chai.request(app)
|
||||
.put('/api/v1/account/username')
|
||||
.set(passport.inject({id: 'wrongid', roles: []}))
|
||||
.send({username: 'MojoJojo'});
|
||||
|
||||
return expect(res).to.eventually.be.rejected;
|
||||
});
|
||||
|
||||
it('it should return an error when the user tries to edit their username if canEditName is disabled', () => {
|
||||
let res = chai.request(app)
|
||||
.put('/api/v1/account/username')
|
||||
.set(passport.inject({id: mockUser.id, roles: []}))
|
||||
.send({username: 'MojoJojo'});
|
||||
|
||||
return expect(res).to.eventually.be.rejected;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -48,52 +48,3 @@ describe('/api/v1/users/:user_id/email/confirm', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/api/v1/users/:user_id/actions', () => {
|
||||
|
||||
let mockUser;
|
||||
|
||||
beforeEach(() => SettingsService.init(settings).then(() => {
|
||||
return UsersService.createLocalUser('ana@gmail.com', '123321123', 'Ana');
|
||||
})
|
||||
.then((user) => {
|
||||
mockUser = user;
|
||||
}));
|
||||
|
||||
describe('#post', () => {
|
||||
it('it should update actions', () => {
|
||||
return chai.request(app)
|
||||
.post(`/api/v1/users/${mockUser.id}/actions`)
|
||||
.set(passport.inject({id: '456', roles: ['ADMIN']}))
|
||||
.send({'action_type': 'FLAG', metadata: {reason: 'Bio is too awesome.'}})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(201);
|
||||
expect(res).to.have.body;
|
||||
expect(res.body).to.have.property('action_type', 'FLAG');
|
||||
expect(res.body).to.have.property('item_id', mockUser.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/api/v1/users/:user_id/username-enable', () => {
|
||||
let mockUser;
|
||||
|
||||
beforeEach(() => SettingsService.init(settings).then(() => {
|
||||
return UsersService.createLocalUser('ana@gmail.com', '123321123', 'Ana');
|
||||
})
|
||||
.then((user) => {
|
||||
mockUser = user;
|
||||
}));
|
||||
|
||||
describe('#post', () => {
|
||||
it('it should enable a user to edit their username', () => {
|
||||
return chai.request(app)
|
||||
.post(`/api/v1/users/${mockUser.id}/username-enable`)
|
||||
.set(passport.inject({id: '456', roles: ['ADMIN']}))
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(204);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
const expect = require('chai').expect;
|
||||
const Domainlist = require('../../../services/domainlist');
|
||||
const DomainList = require('../../../services/domain_list');
|
||||
const SettingsService = require('../../../services/settings');
|
||||
|
||||
describe('services.Domainlist', () => {
|
||||
describe('services.DomainList', () => {
|
||||
|
||||
const domainlists = {
|
||||
const domainLists = {
|
||||
whitelist: [
|
||||
'nytimes.com',
|
||||
'wapo.com'
|
||||
]
|
||||
};
|
||||
|
||||
let domainlist = new Domainlist();
|
||||
let domainList = new DomainList();
|
||||
const settings = {id: '1', moderation: 'PRE', domainlist: {whitelist: ['nytimes.com', 'wapo.com']}};
|
||||
|
||||
beforeEach(() => SettingsService.init(settings));
|
||||
|
||||
describe('#init', () => {
|
||||
|
||||
before(() => domainlist.upsert(domainlists));
|
||||
before(() => domainList.upsert(domainLists));
|
||||
|
||||
it('has entries', () => {
|
||||
expect(domainlist.lists.whitelist).to.not.be.empty;
|
||||
expect(domainList.lists.whitelist).to.not.be.empty;
|
||||
});
|
||||
|
||||
});
|
||||
@@ -92,21 +92,21 @@ describe('services.Domainlist', () => {
|
||||
['google.Ca:80', 'google.ca'],
|
||||
['google.Ca:443', 'google.ca'],
|
||||
].forEach(([domain, hostname]) => {
|
||||
expect(Domainlist.parseURL(domain), `domain ${domain} should be parsed as ${hostname}`).to.equal(hostname);
|
||||
expect(DomainList.parseURL(domain), `domain ${domain} should be parsed as ${hostname}`).to.equal(hostname);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#match', () => {
|
||||
|
||||
const whiteList = Domainlist.parseList(domainlists['whitelist']);
|
||||
const whiteList = DomainList.parseList(domainLists['whitelist']);
|
||||
|
||||
it('does match on an included domain', () => {
|
||||
[
|
||||
'http://wapo.com',
|
||||
'nytimes.com'
|
||||
].forEach((domain) => {
|
||||
expect(domainlist.match(whiteList, domain)).to.be.true;
|
||||
expect(domainList.match(whiteList, domain)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('services.Domainlist', () => {
|
||||
'www.badsite.com',
|
||||
'otherexample.com'
|
||||
].forEach((domain) => {
|
||||
expect(domainlist.match(whiteList, domain)).to.be.false;
|
||||
expect(domainList.match(whiteList, domain)).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
describe('services.scraper', () => {
|
||||
describe('#create', () => {
|
||||
it('should create a new kue job');
|
||||
});
|
||||
|
||||
describe('#scrape', () => {
|
||||
it('should scrape complete information');
|
||||
it('should scrape what it can');
|
||||
});
|
||||
|
||||
describe('#update', () => {
|
||||
it('should update the database record entries from the meta');
|
||||
});
|
||||
|
||||
describe('#process', () => {
|
||||
it('should start the processor to scrape assets');
|
||||
});
|
||||
|
||||
describe('#shutdown', () => {
|
||||
it('should shutdown the job processor');
|
||||
});
|
||||
});
|
||||
+53
-116
@@ -151,21 +151,6 @@ describe('services.UsersService', () => {
|
||||
|
||||
});
|
||||
|
||||
describe('#setStatus', () => {
|
||||
it('should set the status to active', () => {
|
||||
return UsersService
|
||||
.setStatus(mockUsers[0].id, 'ACTIVE')
|
||||
.then(() => UsersService.findById(mockUsers[0].id))
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('status', 'ACTIVE');
|
||||
})
|
||||
.then(() => {
|
||||
expect(MailerService.sendSimple).to.not.have.been.called;
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ignoreUser', () => {
|
||||
it('should add user id to ignoredUsers set', async () => {
|
||||
const user = mockUsers[0];
|
||||
@@ -194,54 +179,6 @@ describe('services.UsersService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ban', () => {
|
||||
it('should set the status to banned', () => {
|
||||
return UsersService
|
||||
.setStatus(mockUsers[0].id, 'BANNED')
|
||||
.then(() => UsersService.findById(mockUsers[0].id))
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('status', 'BANNED');
|
||||
})
|
||||
.then(() => {
|
||||
expect(MailerService.sendSimple).to.have.been.calledWithMatch({
|
||||
template: 'banned',
|
||||
to: mockUsers[0].profiles[0].id
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should still disable and ban the user if there is no comment', () => {
|
||||
return UsersService
|
||||
.setStatus(mockUsers[0].id, 'BANNED')
|
||||
.then(() => UsersService.findById(mockUsers[0].id))
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('status', 'BANNED');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#unban', () => {
|
||||
it('should set the status to active', () => {
|
||||
return UsersService
|
||||
.setStatus(mockUsers[0].id, 'ACTIVE')
|
||||
.then(() => UsersService.findById(mockUsers[0].id))
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('status', 'ACTIVE');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#toggleNameEdit', () => {
|
||||
it('should toggle the canEditName field', () => {
|
||||
return UsersService
|
||||
.toggleNameEdit(mockUsers[0].id, true)
|
||||
.then(() => UsersService.findById(mockUsers[0].id))
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('canEditName', true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#search', () => {
|
||||
it('should return all the results without a value', async () => {
|
||||
expect(await UsersService.search()).to.have.length(3);
|
||||
@@ -286,63 +223,63 @@ describe('services.UsersService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#editName', () => {
|
||||
it('should let the user edit their username if the proper toggle is set', () => {
|
||||
return UsersService
|
||||
.toggleNameEdit(mockUsers[0].id, true)
|
||||
.then(() => UsersService.editName(mockUsers[0].id, 'Jojo'))
|
||||
.then(() => UsersService.findById(mockUsers[0].id))
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('username', 'Jojo');
|
||||
expect(user).to.have.property('canEditName', false);
|
||||
});
|
||||
});
|
||||
// describe('#editName', () => {
|
||||
// it('should let the user edit their username if the proper toggle is set', () => {
|
||||
// return UsersService
|
||||
// .toggleNameEdit(mockUsers[0].id, true)
|
||||
// .then(() => UsersService.editName(mockUsers[0].id, 'Jojo'))
|
||||
// .then(() => UsersService.findById(mockUsers[0].id))
|
||||
// .then((user) => {
|
||||
// expect(user).to.have.property('username', 'Jojo');
|
||||
// expect(user).to.have.property('canEditName', false);
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should let the user submit the same username if user is not banned (create username)', () => {
|
||||
return UsersService
|
||||
.toggleNameEdit(mockUsers[0].id, true)
|
||||
.then(() => UsersService.editName(mockUsers[0].id, mockUsers[0].username))
|
||||
.then(() => UsersService.findById(mockUsers[0].id))
|
||||
.then((user) => {
|
||||
expect(user).to.have.property('username', mockUsers[0].username);
|
||||
expect(user).to.have.property('canEditName', false);
|
||||
});
|
||||
});
|
||||
// it('should let the user submit the same username if user is not banned (create username)', () => {
|
||||
// return UsersService
|
||||
// .toggleNameEdit(mockUsers[0].id, true)
|
||||
// .then(() => UsersService.editName(mockUsers[0].id, mockUsers[0].username))
|
||||
// .then(() => UsersService.findById(mockUsers[0].id))
|
||||
// .then((user) => {
|
||||
// expect(user).to.have.property('username', mockUsers[0].username);
|
||||
// expect(user).to.have.property('canEditName', false);
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should return error when a banned user submits the same username (rejected username)', () => {
|
||||
return UsersService
|
||||
.toggleNameEdit(mockUsers[0].id, true)
|
||||
.then(() => UsersService.setStatus(mockUsers[0].id, 'BANNED'))
|
||||
.then(() => UsersService.editName(mockUsers[0].id, mockUsers[0].username))
|
||||
.then(() => UsersService.findById(mockUsers[0].id))
|
||||
.then(() => {
|
||||
throw new Error('Error expected');
|
||||
})
|
||||
.catch((err) => {
|
||||
expect(err.status).to.equal(400);
|
||||
expect(err.translation_key).to.equal('SAME_USERNAME_PROVIDED');
|
||||
});
|
||||
});
|
||||
// it('should return error when a banned user submits the same username (rejected username)', () => {
|
||||
// return UsersService
|
||||
// .toggleNameEdit(mockUsers[0].id, true)
|
||||
// .then(() => UsersService.setStatus(mockUsers[0].id, 'BANNED'))
|
||||
// .then(() => UsersService.editName(mockUsers[0].id, mockUsers[0].username))
|
||||
// .then(() => UsersService.findById(mockUsers[0].id))
|
||||
// .then(() => {
|
||||
// throw new Error('Error expected');
|
||||
// })
|
||||
// .catch((err) => {
|
||||
// expect(err.status).to.equal(400);
|
||||
// expect(err.translation_key).to.equal('SAME_USERNAME_PROVIDED');
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should return an error if canEditName is false', async () => {
|
||||
return expect(UsersService.editName(mockUsers[0].id, 'Jojo')).to.eventually.be.rejected;
|
||||
});
|
||||
// it('should return an error if canEditName is false', async () => {
|
||||
// return expect(UsersService.editName(mockUsers[0].id, 'Jojo')).to.eventually.be.rejected;
|
||||
// });
|
||||
|
||||
it('should return an error if the username is already taken', async () => {
|
||||
await UsersService.toggleNameEdit(mockUsers[0].id, true);
|
||||
return expect(UsersService.editName(mockUsers[0].id, 'Marvel')).to.eventually.be.rejected;
|
||||
});
|
||||
// it('should return an error if the username is already taken', async () => {
|
||||
// await UsersService.toggleNameEdit(mockUsers[0].id, true);
|
||||
// return expect(UsersService.editName(mockUsers[0].id, 'Marvel')).to.eventually.be.rejected;
|
||||
// });
|
||||
|
||||
it('should not allow non-alphanumeric characters in usernames', () => {
|
||||
return UsersService
|
||||
.isValidUsername('hi🖕')
|
||||
.then(() => {
|
||||
expect(false).to.be.true;
|
||||
})
|
||||
.catch((err) => {
|
||||
expect(err).to.be.ok;
|
||||
});
|
||||
});
|
||||
});
|
||||
// it('should not allow non-alphanumeric characters in usernames', () => {
|
||||
// return UsersService
|
||||
// .isValidUsername('hi🖕')
|
||||
// .then(() => {
|
||||
// expect(false).to.be.true;
|
||||
// })
|
||||
// .catch((err) => {
|
||||
// expect(err).to.be.ok;
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user