mirror of
https://github.com/wassname/talk.git
synced 2026-07-03 06:54:33 +08:00
Merge branch 'slots-debug' of github.com:coralproject/talk into slots-debug
* 'slots-debug' of github.com:coralproject/talk: Asset Manager Tutorial fixed some missing calls to service fixed bug with endpoint Restore the userCreated event moved user deletion into graph
This commit is contained in:
+3
-1
@@ -15,6 +15,7 @@ const SetupService = require('../services/setup');
|
||||
const UsersService = require('../services/users');
|
||||
const MigrationService = require('../services/migration');
|
||||
const errors = require('../errors');
|
||||
const Context = require('../graph/context');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
@@ -184,7 +185,8 @@ const performSetup = async () => {
|
||||
},
|
||||
]);
|
||||
|
||||
let { user: newUser } = await SetupService.setup({
|
||||
const ctx = Context.forSystem();
|
||||
let { user: newUser } = await SetupService.setup(ctx, {
|
||||
settings: settings.toObject(),
|
||||
user: {
|
||||
email: user.email,
|
||||
|
||||
+20
-75
@@ -7,8 +7,6 @@
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const inquirer = require('inquirer');
|
||||
const { graphql } = require('graphql');
|
||||
const helpers = require('../services/migration/helpers');
|
||||
const { stripIndent } = require('common-tags');
|
||||
const Table = require('cli-table');
|
||||
|
||||
@@ -21,31 +19,15 @@ inquirer.registerPrompt(
|
||||
require('inquirer-autocomplete-prompt')
|
||||
);
|
||||
|
||||
const schema = require('../graph/schema');
|
||||
const Context = require('../graph/context');
|
||||
const UsersService = require('../services/users');
|
||||
const UserModel = require('../models/user');
|
||||
const CommentModel = require('../models/comment');
|
||||
const ActionModel = require('../models/action');
|
||||
const USER_ROLES = require('../models/enum/user_roles');
|
||||
const mongoose = require('../services/mongoose');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
|
||||
/**
|
||||
* transforms a specific action to a removal action on the target model.
|
||||
*/
|
||||
const actionDecrTransformer = ({ item_id, action_type, group_id }) => ({
|
||||
query: { id: item_id },
|
||||
update: {
|
||||
$inc: {
|
||||
[`action_counts.${action_type.toLowerCase()}`]: -1,
|
||||
[`action_counts.${action_type.toLowerCase()}_${group_id.toLowerCase()}`]: -1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Deletes a user and cleans up their associated verifications.
|
||||
*/
|
||||
@@ -76,66 +58,29 @@ async function deleteUser(userID) {
|
||||
return util.shutdown();
|
||||
}
|
||||
|
||||
const { transformSingleWithCursor } = helpers({
|
||||
queryBatchSize: 10000,
|
||||
updateBatchSize: 10000,
|
||||
});
|
||||
const ctx = Context.forSystem();
|
||||
|
||||
console.warn("Removing user's actions");
|
||||
|
||||
// Remove all actions against comments.
|
||||
await transformSingleWithCursor(
|
||||
ActionModel.collection.find({ user_id: user.id, item_type: 'COMMENTS' }),
|
||||
actionDecrTransformer,
|
||||
CommentModel
|
||||
const { data, errors } = await ctx.graphql(
|
||||
`
|
||||
mutation DeleteUser($user_id: ID!) {
|
||||
delUser(id: $user_id) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ user_id: user.id }
|
||||
);
|
||||
if (errors) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
// Remove all actions against users.
|
||||
await transformSingleWithCursor(
|
||||
ActionModel.collection.find({ user_id: user.id, item_type: 'USERS' }),
|
||||
actionDecrTransformer,
|
||||
UserModel
|
||||
);
|
||||
if (data.errors) {
|
||||
throw data.errors;
|
||||
}
|
||||
|
||||
// Remove all the user's actions.
|
||||
await ActionModel.where({ user_id: user.id })
|
||||
.setOptions({ multi: true })
|
||||
.remove();
|
||||
|
||||
console.warn("Removing user's comments");
|
||||
|
||||
// Removes all the user's reply counts on each of the comments that they
|
||||
// have commented on.
|
||||
await transformSingleWithCursor(
|
||||
CommentModel.collection.aggregate([
|
||||
{ $match: { author_id: user.id } },
|
||||
{
|
||||
$group: {
|
||||
_id: '$parent_id',
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
]),
|
||||
({ _id: parent_id, count }) => ({
|
||||
query: { id: parent_id },
|
||||
update: {
|
||||
$inc: {
|
||||
reply_count: -1 * count,
|
||||
},
|
||||
},
|
||||
}),
|
||||
CommentModel
|
||||
);
|
||||
|
||||
// Remove all the user's comments.
|
||||
await CommentModel.where({ author_id: user.id })
|
||||
.setOptions({ multi: true })
|
||||
.remove();
|
||||
|
||||
console.warn('Removing the user');
|
||||
|
||||
// Remove the user.
|
||||
await user.remove();
|
||||
console.log('User was deleted.');
|
||||
|
||||
util.shutdown();
|
||||
} catch (err) {
|
||||
@@ -197,7 +142,7 @@ async function searchUsers() {
|
||||
value = '';
|
||||
}
|
||||
|
||||
const { data, errors } = await graphql(schema, searchQuery, {}, ctx, {
|
||||
const { data, errors } = await ctx.graphql(searchQuery, {
|
||||
value,
|
||||
});
|
||||
if (errors && errors.length > 0) {
|
||||
|
||||
@@ -100,6 +100,8 @@ sidebar:
|
||||
children:
|
||||
- title: Authentication
|
||||
url: /integrating/authentication/
|
||||
- title: Asset Managment
|
||||
url: /integrating/asset-management/
|
||||
- title: Configuring the Comment Stream
|
||||
url: /integrating/configuring-comment-stream/
|
||||
- title: Configuring the Admin
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
---
|
||||
title: Asset Management
|
||||
permalink: /integrating/asset-management/
|
||||
---
|
||||
|
||||
One of the most frequent questions that we get asked by organizations trying to
|
||||
integrate Talk is: _How do we hook our CMS up to Talk so that articles are in
|
||||
sync?_
|
||||
|
||||
This guide is designed to explain the steps to take your base installation of
|
||||
Talk and configure it to allow only assets pushed into it from your CMS, and
|
||||
keep your URL/title in sync. We won't cover here how to install the plugin, as
|
||||
it is covered in our [Plugins Overview](/talk/plugins/).
|
||||
|
||||
## Why do we need to create a plugin?
|
||||
|
||||
By default, Talk will use "Lazy Asset Creation" to dynamically generate Assets
|
||||
in Talk in order to make it easier for lighter installations. In order to have
|
||||
more strict control over this flow, we will create a plugin that will:
|
||||
|
||||
1. Disable "Lazy Asset Creation" by [Overriding a Resolver](#Overriding-a-Resolver).
|
||||
2. Create Assets from our CMS by [Creating a New Asset Route](#Creating-a-New-Asset-Route).
|
||||
3. Facilitate updates from our CMS to keep Talk in sync by [Creating an Asset Update Route](#Creating-an-Asset-Update-Route).
|
||||
|
||||
We will then modify our embed so that we can [Target the Asset](#Target-the-Asset).
|
||||
|
||||
But first we should grab our basic plugin structure:
|
||||
|
||||
```sh
|
||||
# clone our example repo (that comes with all the code below!)
|
||||
git clone https://github.com/coralproject/talk-plugin-asset-manager-example.git
|
||||
|
||||
# checkout the step-1 tag that starts us off with the basic file structure of
|
||||
# the plugin.
|
||||
git checkout step-1
|
||||
```
|
||||
|
||||
## Overriding a Resolver
|
||||
|
||||
First we'll replace the content of the `resolver.js` file with the following:
|
||||
|
||||
```js
|
||||
// We'll need to modify the behavior of how assets are
|
||||
// "resolved" in Talk, so we override the base asset resolver
|
||||
// for the RootQuery type.
|
||||
module.exports = {
|
||||
RootQuery: {
|
||||
asset: async (root, args, ctx) => {
|
||||
// We'll grab the id of the asset being requested
|
||||
// such that we'll be able to lookup the asset.
|
||||
const { id } = args;
|
||||
if (!id) {
|
||||
// If the ID isn't provided, we don't want to do
|
||||
// anything.
|
||||
return null;
|
||||
}
|
||||
|
||||
// A mouthful for sure, but we need to use the loader
|
||||
// that is available on the graph context in order to
|
||||
// lookup the asset by ID.
|
||||
const asset = await ctx.loaders.Assets.getByID.load(id);
|
||||
if (!asset) {
|
||||
// If the asset can't be found, we don't want to do
|
||||
// anything.
|
||||
return null;
|
||||
}
|
||||
|
||||
// Send the asset back.
|
||||
return asset;
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
This serves to override the default asset resolver. You can of course, override
|
||||
any other field in the schema to perform whatever action your business needs
|
||||
require, including adding additional resolvers! You can refer to our
|
||||
[GraphQL API Docs](/talk/reference/graphql/) to see what other fields you can
|
||||
override.
|
||||
|
||||
Without this, Talk will continue to use the "Lazy Asset Creation" to handle
|
||||
resolving the `asset` edge, which is what we want to change.
|
||||
|
||||
_Note, you can also get to this point by running `git checkout step-2`!_
|
||||
|
||||
## Creating a New Asset Route
|
||||
|
||||
In order to create Assets now, we have to get our CMS to push those into Talk,
|
||||
the easiest way to do this is by creating a custom route. We won't cover
|
||||
specific CMS integrations, but will assume that there is some type of webhook
|
||||
system you are able to utilize that will trigger when a new article is created.
|
||||
|
||||
We'll replace the contents of the `router.js` file with the following:
|
||||
|
||||
```js
|
||||
// This file we'll create routes that will facilitate asset creation and
|
||||
// updates.
|
||||
|
||||
const authz = require('middleware/authorization');
|
||||
|
||||
module.exports = router => {
|
||||
// We'll respond to a POST request on the following route where the request
|
||||
// must have a valid ADMIN access token.
|
||||
router.post(
|
||||
'/api/v1/plugin/asset-manager-example',
|
||||
authz.needed('ADMIN'),
|
||||
async (req, res, next) => {
|
||||
// Get the graph context from the request.
|
||||
const { context } = req;
|
||||
|
||||
// Grab from the graph context, the AssetModel that we can use to create
|
||||
// the new Asset. Lots of object destructuring here, but this lets us keep
|
||||
// the important business logic cleaner.
|
||||
const { connectors: { models: { Assets } } } = context;
|
||||
|
||||
try {
|
||||
// Now we can create the asset that was passed to us in the body of the
|
||||
// request as JSON. Check the schema of the Asset model by looking at:
|
||||
// https://github.com/coralproject/talk/blob/master/models/asset.js
|
||||
await Assets.create(req.body);
|
||||
|
||||
// Let your webhook callback know we got it!
|
||||
return res.status(204).end();
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
This request handler when mounted on Talk will allow your CMS to send a POST
|
||||
request to `${TALK_ROOT_URL}/api/v1/plugin/asset-manager-example` with the
|
||||
Asset as a JSON payload. In order to protect the endpoint from abuse, we add the
|
||||
authorization middleware. This middleware essentially says, _you must be an
|
||||
admin to hit this route_. We need to generate a token that can be used by your
|
||||
CMS using the Talk cli tool:
|
||||
|
||||
```sh
|
||||
# find or create an admin user that can be used as the basis for the token
|
||||
./bin/cli users list
|
||||
|
||||
# create a token for the user with the given id
|
||||
./bin/cli token create ${USER_ID} cms-token
|
||||
```
|
||||
|
||||
You can attach the generated token to the request a few ways:
|
||||
|
||||
1. HTTP Header:
|
||||
|
||||
curl ${TALK_ROOT_URL}/api/v1/plugin/asset-manager-example \
|
||||
-XPOST \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "${ASSET_JSON}"
|
||||
|
||||
2. Query Parameter:
|
||||
|
||||
curl ${TALK_ROOT_URL}/api/v1/plugin/asset-manager-example?access_token=${TOKEN}
|
||||
-XPOST \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "${ASSET_JSON}"
|
||||
|
||||
Where `${ASSET_JSON}` is the JSON for your Asset matching the
|
||||
[AssetSchema](https://github.com/coralproject/talk/blob/master/models/asset.js).
|
||||
|
||||
_Note, you can also get to this point by running `git checkout step-3`!_
|
||||
|
||||
## Creating an Asset Update Route
|
||||
|
||||
Now imagine the situation where you decide that you want to change the url slug
|
||||
of the page, or update the title, now Talk is out of sync! Let's fix that.
|
||||
|
||||
Update your `router.js` to the following:
|
||||
|
||||
```js
|
||||
// This file we'll create routes that will facilitate asset creation and
|
||||
// updates.
|
||||
|
||||
const authz = require('middleware/authorization');
|
||||
|
||||
module.exports = router => {
|
||||
// We'll respond to a POST request on the following route where the request
|
||||
// must have a valid ADMIN access token.
|
||||
router.post(
|
||||
'/api/v1/plugin/asset-manager-example',
|
||||
authz.needed('ADMIN'),
|
||||
async (req, res, next) => {
|
||||
// Get the graph context from the request.
|
||||
const { context } = req;
|
||||
|
||||
// Grab from the graph context, the AssetModel that we can use to create
|
||||
// the new Asset. Lots of object destructuring here, but this lets us keep
|
||||
// the important business logic cleaner.
|
||||
const { connectors: { models: { Assets } } } = context;
|
||||
|
||||
try {
|
||||
// Now we can create the asset that was passed to us in the body of the
|
||||
// request as JSON. Check the schema of the Asset model by looking at:
|
||||
// https://github.com/coralproject/talk/blob/master/models/asset.js
|
||||
await Assets.create(req.body);
|
||||
|
||||
// Let your webhook callback know we got it!
|
||||
return res.status(204).end();
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// We'll respond to a PUT request on the following route where the request
|
||||
// must also have a valid ADMIN access token.
|
||||
router.put(
|
||||
'/api/v1/plugin/asset-manager-example/:id',
|
||||
authz.needed('ADMIN'),
|
||||
async (req, res, next) => {
|
||||
// Get the graph context from the request.
|
||||
const { context } = req;
|
||||
|
||||
// Grab from the graph context, the AssetModel that we can use to update
|
||||
// the Asset. Lots of object destructuring here, but this lets us keep
|
||||
// the important business logic cleaner.
|
||||
const { connectors: { models: { Assets } } } = context;
|
||||
|
||||
try {
|
||||
// Now we can lookup the asset we're updating and apply out updates to
|
||||
// the model atomically.
|
||||
const asset = await Assets.findOneAndUpdate(
|
||||
{ id: req.params.id },
|
||||
req.body,
|
||||
{
|
||||
// We want to validate the model being updated.
|
||||
runValidators: true,
|
||||
}
|
||||
);
|
||||
if (!asset) {
|
||||
// The asset indicated by the ID wasn't found, let the webhook know!
|
||||
return res.status(404).end();
|
||||
}
|
||||
|
||||
// Let your webhook callback know we got it!
|
||||
return res.status(204).end();
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
As you can see from the previous step of [Creating a New Asset Route](#Creating-a-New-Asset-Route)
|
||||
, we have added the new `PUT` route to the router. This is a simple addition
|
||||
that allows your CMS to call into Talk when the asset has updated it's title,
|
||||
it's url (or really anything in the [AssetSchema](https://github.com/coralproject/talk/blob/master/models/asset.js)) to keep the Talk Admin and links up to date.
|
||||
|
||||
Following the previous example, you can issue the request as follows:
|
||||
|
||||
```sh
|
||||
curl ${TALK_ROOT_URL}/api/v1/plugin/asset-manager-example/${ASSET_ID} \
|
||||
-XPUT \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "${ASSET_JSON}"
|
||||
```
|
||||
|
||||
The difference from the previous curl example, is that this one changes the
|
||||
method from a `POST` to a `PUT`, and we add the `${ASSET_ID}` to the end of the
|
||||
url.
|
||||
|
||||
_Note, you can also get to this point by running `git checkout step-4`!_
|
||||
|
||||
## Target the Asset
|
||||
|
||||
Now that we have a way to create and update Assets, we now need a way to
|
||||
reference it. One of the most important fields in the Asset model, is the `id`.
|
||||
This `id` can be one generated from your CMS, or some other system, but must
|
||||
be kept consistent.
|
||||
|
||||
When you install Talk, and visit the admin panel, we can see under
|
||||
`/admin/configure` in the tab for Tech Settings, an embed snippet:
|
||||
|
||||
```html
|
||||
<div id="coral_talk_stream"></div>
|
||||
<script src="${TALK_ROOT_URL}static/embed.js" async onload="
|
||||
Coral.Talk.render(document.getElementById('coral_talk_stream'), {
|
||||
talk: '${TALK_ROOT_URL}'
|
||||
});
|
||||
"></script>
|
||||
```
|
||||
|
||||
We'll modify this to the following:
|
||||
|
||||
```html
|
||||
<div id="coral_talk_stream"></div>
|
||||
<script src="${TALK_ROOT_URL}static/embed.js" async onload="
|
||||
Coral.Talk.render(document.getElementById('coral_talk_stream'), {
|
||||
talk: '${TALK_ROOT_URL}',
|
||||
asset_id: '${ASSET_ID}'
|
||||
});
|
||||
"></script>
|
||||
```
|
||||
|
||||
Adding the `asset_id` parameter to the render function will accomplish a very
|
||||
important task. It will provide Talk with the specific ID of the asset to
|
||||
associate with the displayed page. This is important because even if you update
|
||||
the URL in the future, the embed will still reference the correct Asset. The
|
||||
`${ASSET_ID}` should be replaced by your CMS with the correct Asset id using
|
||||
your desired scripting/templating tools.
|
||||
|
||||
At this point, you should have a fully built Talk plugin that can be paired with
|
||||
some work on your CMS to create a fully integrated asset management pipeline!
|
||||
|
||||
To view the fully completed source code, visit
|
||||
https://github.com/coralproject/talk-plugin-asset-manager-example.
|
||||
@@ -318,7 +318,13 @@ module.exports = {
|
||||
|
||||
let user;
|
||||
try {
|
||||
user = await UsersService.findOrCreateExternalUser(profile);
|
||||
const { id, provider, displayName } = profile;
|
||||
user = await UsersService.findOrCreateExternalUser(
|
||||
req.context,
|
||||
id,
|
||||
provider,
|
||||
displayName
|
||||
);
|
||||
} catch (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const errors = require('../../errors');
|
||||
const UsersService = require('../../services/users');
|
||||
const migrationHelpers = require('../../services/migration/helpers');
|
||||
const {
|
||||
CHANGE_USERNAME,
|
||||
SET_USERNAME,
|
||||
@@ -7,6 +8,7 @@ const {
|
||||
SET_USER_BAN_STATUS,
|
||||
SET_USER_SUSPENSION_STATUS,
|
||||
UPDATE_USER_ROLES,
|
||||
DELETE_USER,
|
||||
} = require('../../perms/constants');
|
||||
|
||||
const setUserUsernameStatus = async (ctx, id, status) => {
|
||||
@@ -70,6 +72,87 @@ const setRole = (ctx, id, role) => {
|
||||
return UsersService.setRole(id, role);
|
||||
};
|
||||
|
||||
/**
|
||||
* transforms a specific action to a removal action on the target model.
|
||||
*/
|
||||
const actionDecrTransformer = ({ item_id, action_type, group_id }) => ({
|
||||
query: { id: item_id },
|
||||
update: {
|
||||
$inc: {
|
||||
[`action_counts.${action_type.toLowerCase()}`]: -1,
|
||||
[`action_counts.${action_type.toLowerCase()}_${group_id.toLowerCase()}`]: -1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// delUser will delete a given user with the specified id.
|
||||
const delUser = async (ctx, id) => {
|
||||
const { connectors: { models: { User, Action, Comment } } } = ctx;
|
||||
|
||||
// Find the user we're removing.
|
||||
const user = await User.findOne({ id });
|
||||
if (!user) {
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
// Get the query transformer we'll use to help batch process the user
|
||||
// deletion.
|
||||
const { transformSingleWithCursor } = migrationHelpers({
|
||||
queryBatchSize: 10000,
|
||||
updateBatchSize: 10000,
|
||||
});
|
||||
|
||||
// Remove all actions against comments.
|
||||
await transformSingleWithCursor(
|
||||
Action.collection.find({ user_id: user.id, item_type: 'COMMENTS' }),
|
||||
actionDecrTransformer,
|
||||
Comment
|
||||
);
|
||||
|
||||
// Remove all actions against users.
|
||||
await transformSingleWithCursor(
|
||||
Action.collection.find({ user_id: user.id, item_type: 'USERS' }),
|
||||
actionDecrTransformer,
|
||||
User
|
||||
);
|
||||
|
||||
// Remove all the user's actions.
|
||||
await Action.where({ user_id: user.id })
|
||||
.setOptions({ multi: true })
|
||||
.remove();
|
||||
|
||||
// Removes all the user's reply counts on each of the comments that they
|
||||
// have commented on.
|
||||
await transformSingleWithCursor(
|
||||
Comment.collection.aggregate([
|
||||
{ $match: { author_id: user.id } },
|
||||
{
|
||||
$group: {
|
||||
_id: '$parent_id',
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
]),
|
||||
({ _id: parent_id, count }) => ({
|
||||
query: { id: parent_id },
|
||||
update: {
|
||||
$inc: {
|
||||
reply_count: -1 * count,
|
||||
},
|
||||
},
|
||||
}),
|
||||
Comment
|
||||
);
|
||||
|
||||
// Remove all the user's comments.
|
||||
await Comment.where({ author_id: user.id })
|
||||
.setOptions({ multi: true })
|
||||
.remove();
|
||||
|
||||
// Remove the user.
|
||||
await user.remove();
|
||||
};
|
||||
|
||||
module.exports = ctx => {
|
||||
let mutators = {
|
||||
User: {
|
||||
@@ -81,6 +164,7 @@ module.exports = ctx => {
|
||||
setUserUsernameStatus: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
setUsername: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
stopIgnoringUser: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
del: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -116,6 +200,10 @@ module.exports = ctx => {
|
||||
mutators.User.setUserSuspensionStatus = (id, until, message) =>
|
||||
setUserSuspensionStatus(ctx, id, until, message);
|
||||
}
|
||||
|
||||
if (ctx.user.can(DELETE_USER)) {
|
||||
mutators.User.del = id => delUser(ctx, id);
|
||||
}
|
||||
}
|
||||
|
||||
return mutators;
|
||||
|
||||
@@ -136,6 +136,9 @@ const RootMutation = {
|
||||
forceScrapeAsset: async (_, { id }, { mutators: { Asset } }) => {
|
||||
await Asset.scrape(id);
|
||||
},
|
||||
delUser: async (_, { id }, { mutators: { User } }) => {
|
||||
await User.del(id);
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = RootMutation;
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
const {
|
||||
SUBSCRIBE_COMMENT_ACCEPTED,
|
||||
SUBSCRIBE_COMMENT_REJECTED,
|
||||
SUBSCRIBE_COMMENT_FLAGGED,
|
||||
SUBSCRIBE_COMMENT_RESET,
|
||||
SUBSCRIBE_ALL_COMMENT_EDITED,
|
||||
SUBSCRIBE_ALL_COMMENT_ADDED,
|
||||
SUBSCRIBE_ALL_USER_SUSPENDED,
|
||||
SUBSCRIBE_ALL_COMMENT_EDITED,
|
||||
SUBSCRIBE_ALL_USER_BANNED,
|
||||
SUBSCRIBE_ALL_USERNAME_REJECTED,
|
||||
SUBSCRIBE_ALL_USER_CREATED,
|
||||
SUBSCRIBE_ALL_USER_SUSPENDED,
|
||||
SUBSCRIBE_ALL_USERNAME_APPROVED,
|
||||
SUBSCRIBE_ALL_USERNAME_FLAGGED,
|
||||
SUBSCRIBE_ALL_USERNAME_CHANGED,
|
||||
SUBSCRIBE_ALL_USERNAME_FLAGGED,
|
||||
SUBSCRIBE_ALL_USERNAME_REJECTED,
|
||||
SUBSCRIBE_COMMENT_ACCEPTED,
|
||||
SUBSCRIBE_COMMENT_FLAGGED,
|
||||
SUBSCRIBE_COMMENT_REJECTED,
|
||||
SUBSCRIBE_COMMENT_RESET,
|
||||
} = require('../../perms/constants');
|
||||
|
||||
const merge = require('lodash/merge');
|
||||
@@ -139,6 +140,8 @@ const setupFunctions = {
|
||||
}
|
||||
return !args.user_id || user.id === args.user_id;
|
||||
},
|
||||
userCreated: (options, args, user, ctx) =>
|
||||
ctx.user && ctx.user.can(SUBSCRIBE_ALL_USER_CREATED),
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -153,19 +156,17 @@ module.exports = plugins.get('server', 'setupFunctions').reduce(
|
||||
|
||||
return merge(acc, setupFunctions);
|
||||
},
|
||||
Object.keys(setupFunctions)
|
||||
.map(key => {
|
||||
const filter = setupFunctions[key];
|
||||
|
||||
return {
|
||||
[key]: (options, args) => ({
|
||||
[key]: {
|
||||
filter: (user, ctx) => filter(options, args, user, ctx),
|
||||
},
|
||||
}),
|
||||
};
|
||||
})
|
||||
.reduce((setupFunction, setupFunctions) => {
|
||||
return merge(setupFunctions, setupFunction);
|
||||
}, {})
|
||||
// Process the default setupFunctions.
|
||||
Object.entries(setupFunctions)
|
||||
.map(([key, filter]) => ({
|
||||
[key]: (options, args) => ({
|
||||
[key]: {
|
||||
filter: (user, ctx) => filter(options, args, user, ctx),
|
||||
},
|
||||
}),
|
||||
}))
|
||||
.reduce(
|
||||
(setupFunction, setupFunctions) => merge(setupFunctions, setupFunction),
|
||||
{}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1426,6 +1426,12 @@ type ForceScrapeAssetResponse implements Response {
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
type DelUserResponse 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 {
|
||||
|
||||
@@ -1523,6 +1529,9 @@ type RootMutation {
|
||||
|
||||
# forceScrapeAsset will force scrape the Asset with the given ID.
|
||||
forceScrapeAsset(id: ID!): ForceScrapeAssetResponse
|
||||
|
||||
# delUser will delete the user with the specified id.
|
||||
delUser(id: ID!): DelUserResponse
|
||||
}
|
||||
|
||||
type UsernameChangedPayload {
|
||||
@@ -1588,6 +1597,10 @@ type Subscription {
|
||||
# Get an update whenever a username has been changed. `user_id` must match id
|
||||
# of current user except for users with the `ADMIN` or `MODERATOR` role.
|
||||
usernameChanged(user_id: ID): UsernameChangedPayload
|
||||
|
||||
# Get an update whenever a user is created. Only accessible to users with the
|
||||
# `ADMIN` or `MODERATOR` roles.
|
||||
userCreated: User
|
||||
}
|
||||
|
||||
################################################################################
|
||||
|
||||
@@ -18,4 +18,5 @@ module.exports = {
|
||||
UPDATE_ASSET_SETTINGS: 'UPDATE_ASSET_SETTINGS',
|
||||
UPDATE_ASSET_STATUS: 'UPDATE_ASSET_STATUS',
|
||||
UPDATE_SETTINGS: 'UPDATE_SETTINGS',
|
||||
DELETE_USER: 'DELETE_USER',
|
||||
};
|
||||
|
||||
@@ -11,4 +11,5 @@ module.exports = {
|
||||
SUBSCRIBE_ALL_USERNAME_APPROVED: 'SUBSCRIBE_ALL_USERNAME_APPROVED',
|
||||
SUBSCRIBE_ALL_USERNAME_FLAGGED: 'SUBSCRIBE_ALL_USERNAME_FLAGGED',
|
||||
SUBSCRIBE_ALL_USERNAME_CHANGED: 'SUBSCRIBE_ALL_USERNAME_CHANGED',
|
||||
SUBSCRIBE_ALL_USER_CREATED: 'SUBSCRIBE_ALL_USER_CREATED',
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ module.exports = (user, perm) => {
|
||||
case types.SUBSCRIBE_ALL_USERNAME_APPROVED:
|
||||
case types.SUBSCRIBE_ALL_USERNAME_FLAGGED:
|
||||
case types.SUBSCRIBE_ALL_USERNAME_CHANGED:
|
||||
case types.SUBSCRIBE_ALL_USER_CREATED:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -25,7 +25,14 @@ module.exports = passport => {
|
||||
async (req, accessToken, refreshToken, profile, done) => {
|
||||
let user;
|
||||
try {
|
||||
user = await UsersService.findOrCreateExternalUser(profile);
|
||||
const { id, provider, displayName } = profile;
|
||||
|
||||
user = await UsersService.findOrCreateExternalUser(
|
||||
req.context,
|
||||
id,
|
||||
provider,
|
||||
displayName
|
||||
);
|
||||
} catch (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
@@ -24,9 +24,16 @@ module.exports = passport => {
|
||||
async (req, accessToken, refreshToken, profile, done) => {
|
||||
let user;
|
||||
try {
|
||||
user = await UsersService.findOrCreateExternalUser(profile);
|
||||
const { id, provider, displayName } = profile;
|
||||
|
||||
user = await UsersService.findOrCreateExternalUser(
|
||||
req.context,
|
||||
id,
|
||||
provider,
|
||||
displayName
|
||||
);
|
||||
} catch (err) {
|
||||
return done(err.toString());
|
||||
return done(err);
|
||||
}
|
||||
|
||||
return ValidateUserLogin(profile, user, done);
|
||||
|
||||
@@ -21,7 +21,10 @@ router.post('/', async (req, res, next) => {
|
||||
const { settings, user: { email, password, username } } = req.body;
|
||||
|
||||
try {
|
||||
await SetupService.setup({ settings, user: { email, password, username } });
|
||||
await SetupService.setup(req.context, {
|
||||
settings,
|
||||
user: { email, password, username },
|
||||
});
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
|
||||
@@ -11,7 +11,13 @@ router.post('/', async (req, res, next) => {
|
||||
const redirectUri = req.header('X-Pym-Url') || req.header('Referer');
|
||||
|
||||
try {
|
||||
let user = await UsersService.createLocalUser(email, password, username);
|
||||
// Adjusted the user creation endpoint.
|
||||
let user = await UsersService.createLocalUser(
|
||||
req.context,
|
||||
email,
|
||||
password,
|
||||
username
|
||||
);
|
||||
|
||||
// Send an email confirmation. The Front end will know about the
|
||||
// requireEmailConfirmation as it's included in the settings get endpoint.
|
||||
|
||||
+7
-2
@@ -61,7 +61,7 @@ module.exports = class SetupService {
|
||||
/**
|
||||
* This will perform the setup.
|
||||
*/
|
||||
static async setup({ settings, user: { email, password, username } }) {
|
||||
static async setup(ctx, { settings, user: { email, password, username } }) {
|
||||
// Validate the settings first.
|
||||
await SetupService.validate({
|
||||
settings,
|
||||
@@ -79,7 +79,12 @@ module.exports = class SetupService {
|
||||
// Settings are created! Create the user.
|
||||
|
||||
// Create the user.
|
||||
let user = await UsersService.createLocalUser(email, password, username);
|
||||
let user = await UsersService.createLocalUser(
|
||||
ctx,
|
||||
email,
|
||||
password,
|
||||
username
|
||||
);
|
||||
|
||||
// Grant them administrative privileges and confirm the email account.
|
||||
await Promise.all([
|
||||
|
||||
+13
-24
@@ -416,7 +416,7 @@ class UsersService {
|
||||
* @param {Object} profile - User social/external profile
|
||||
* @param {Function} done [description]
|
||||
*/
|
||||
static async findOrCreateExternalUser({ id, provider, displayName }) {
|
||||
static async findOrCreateExternalUser(ctx, id, provider, displayName) {
|
||||
let user = await UserModel.findOne({
|
||||
profiles: {
|
||||
$elemMatch: {
|
||||
@@ -452,6 +452,9 @@ class UsersService {
|
||||
// Save the user in the database.
|
||||
await user.save();
|
||||
|
||||
// Emit that the user was created.
|
||||
ctx.pubsub.publish('userCreated', user);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -502,23 +505,6 @@ class UsersService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates local users.
|
||||
* @param {Array} users Users to create
|
||||
* @return {Promise} Resolves with the users that were created
|
||||
*/
|
||||
static createLocalUsers(users) {
|
||||
return Promise.all(
|
||||
users.map(user => {
|
||||
return UsersService.createLocalUser(
|
||||
user.email,
|
||||
user.password,
|
||||
user.username
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the requested username for blocked words and special chars
|
||||
* @param {String} username word to be checked for profanity
|
||||
@@ -553,24 +539,24 @@ class UsersService {
|
||||
*/
|
||||
static isValidPassword(password) {
|
||||
if (!password) {
|
||||
return Promise.reject(errors.ErrMissingPassword);
|
||||
throw errors.ErrMissingPassword;
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
return Promise.reject(errors.ErrPasswordTooShort);
|
||||
throw errors.ErrPasswordTooShort;
|
||||
}
|
||||
|
||||
return Promise.resolve(password);
|
||||
return password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the local user with a given email, password, and name.
|
||||
* @param {Object} ctx application context for the request
|
||||
* @param {String} email email of the new user
|
||||
* @param {String} password plaintext password of the new user
|
||||
* @param {String} username name of the display user
|
||||
* @param {Function} done callback
|
||||
* @param {String} username name of the display user
|
||||
*/
|
||||
static async createLocalUser(email, password, username) {
|
||||
static async createLocalUser(ctx, email, password, username) {
|
||||
if (!email) {
|
||||
throw errors.ErrMissingEmail;
|
||||
}
|
||||
@@ -617,6 +603,9 @@ class UsersService {
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Emit that the user was created.
|
||||
ctx.pubsub.publish('userCreated', user);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@ describe('graph.mutations.changeUsername', () => {
|
||||
let user;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
const ctx = Context.forSystem();
|
||||
user = await UsersService.createLocalUser(
|
||||
ctx,
|
||||
'test@test.com',
|
||||
'testpassword1!',
|
||||
'kirk'
|
||||
|
||||
@@ -16,8 +16,10 @@ describe('graph.mutations.editComment', () => {
|
||||
beforeEach(async () => {
|
||||
timekeeper.reset();
|
||||
await SettingsService.init();
|
||||
const ctx = Context.forSystem();
|
||||
asset = await AssetModel.create({});
|
||||
user = await UsersService.createLocalUser(
|
||||
ctx,
|
||||
'usernameA@example.com',
|
||||
'password',
|
||||
'usernameA'
|
||||
@@ -124,7 +126,9 @@ describe('graph.mutations.editComment', () => {
|
||||
body: `hello there! ${String(Math.random()).slice(2)}`,
|
||||
});
|
||||
|
||||
const ctx = Context.forSystem();
|
||||
const userB = await UsersService.createLocalUser(
|
||||
ctx,
|
||||
'usernameB@example.com',
|
||||
'password',
|
||||
'usernameB'
|
||||
|
||||
@@ -34,12 +34,15 @@ describe('graph.mutations.ignoreUser', () => {
|
||||
});
|
||||
|
||||
it('users can ignoreUser', async () => {
|
||||
const ctx = Context.forSystem();
|
||||
let currentUser = await UsersService.createLocalUser(
|
||||
ctx,
|
||||
'usernameA@example.com',
|
||||
'password',
|
||||
'usernameA'
|
||||
);
|
||||
const userToIgnore = await UsersService.createLocalUser(
|
||||
ctx,
|
||||
'usernameB@example.com',
|
||||
'password',
|
||||
'usernameB'
|
||||
@@ -83,7 +86,9 @@ describe('graph.mutations.ignoreUser', () => {
|
||||
});
|
||||
|
||||
it('users cannot ignore themselves', async () => {
|
||||
const ctx = Context.forSystem();
|
||||
const user = await UsersService.createLocalUser(
|
||||
ctx,
|
||||
'usernameA@example.com',
|
||||
'password',
|
||||
'usernameA'
|
||||
@@ -124,18 +129,22 @@ describe('graph.mutations.stopIgnoringUser', () => {
|
||||
// We're going to ignore 2 users,
|
||||
// then stopIgnoring 1 of them
|
||||
// then assert myIgnoredUsers only lists the one remaining
|
||||
const ctx = Context.forSystem();
|
||||
let currentUser = await UsersService.createLocalUser(
|
||||
ctx,
|
||||
'usernameA@example.com',
|
||||
'password',
|
||||
'usernameA'
|
||||
);
|
||||
const usersToIgnore = await Promise.all([
|
||||
UsersService.createLocalUser(
|
||||
ctx,
|
||||
'usernameB@example.com',
|
||||
'password',
|
||||
'usernameB'
|
||||
),
|
||||
UsersService.createLocalUser(
|
||||
ctx,
|
||||
'usernameC@example.com',
|
||||
'password',
|
||||
'usernameC'
|
||||
|
||||
@@ -17,7 +17,9 @@ describe('graph.mutations.banUser', () => {
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
|
||||
const ctx = Context.forSystem();
|
||||
user = await UsersService.createLocalUser(
|
||||
ctx,
|
||||
'usernameA@example.com',
|
||||
'password',
|
||||
'usernameA'
|
||||
|
||||
@@ -19,7 +19,9 @@ describe('graph.mutations.suspendUser', () => {
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
|
||||
const ctx = Context.forSystem();
|
||||
user = await UsersService.createLocalUser(
|
||||
ctx,
|
||||
'usernameA@example.com',
|
||||
'password',
|
||||
'usernameA'
|
||||
|
||||
@@ -19,7 +19,9 @@ const { expect } = chai;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
|
||||
const ctx = Context.forSystem();
|
||||
user = await UsersService.createLocalUser(
|
||||
ctx,
|
||||
'usernameA@example.com',
|
||||
'password',
|
||||
'usernameA'
|
||||
|
||||
@@ -17,23 +17,28 @@ describe('graph.queries.asset', () => {
|
||||
{ id: '1', url: 'https://example.com/?id=1' },
|
||||
{ id: '2', url: 'https://example.com/?id=2' },
|
||||
]);
|
||||
users = await UsersService.createLocalUsers([
|
||||
{
|
||||
email: 'usernameA@example.com',
|
||||
password: 'password',
|
||||
username: 'usernameA',
|
||||
},
|
||||
{
|
||||
email: 'usernameB@example.com',
|
||||
password: 'password',
|
||||
username: 'usernameB',
|
||||
},
|
||||
{
|
||||
email: 'usernameC@example.com',
|
||||
password: 'password',
|
||||
username: 'usernameC',
|
||||
},
|
||||
]);
|
||||
const ctx = Context.forSystem();
|
||||
users = await Promise.all(
|
||||
[
|
||||
{
|
||||
email: 'usernameA@example.com',
|
||||
password: 'password',
|
||||
username: 'usernameA',
|
||||
},
|
||||
{
|
||||
email: 'usernameB@example.com',
|
||||
password: 'password',
|
||||
username: 'usernameB',
|
||||
},
|
||||
{
|
||||
email: 'usernameC@example.com',
|
||||
password: 'password',
|
||||
username: 'usernameC',
|
||||
},
|
||||
].map(({ email, username, password }) =>
|
||||
UsersService.createLocalUser(ctx, email, password, username)
|
||||
)
|
||||
);
|
||||
comments = await Promise.all(
|
||||
[0, 0, 1, 1].map(idx =>
|
||||
CommentsService.publicCreate({
|
||||
|
||||
@@ -14,8 +14,9 @@ describe('graph.queries.user', () => {
|
||||
let user;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
|
||||
const ctx = Context.forSystem();
|
||||
user = await UsersService.createLocalUser(
|
||||
ctx,
|
||||
'usernameA@example.com',
|
||||
'password',
|
||||
'usernameA'
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
const app = require('../../../../../app');
|
||||
const Context = require('../../../../../graph/context');
|
||||
const UsersService = require('../../../../../services/users');
|
||||
|
||||
const chai = require('chai');
|
||||
chai.should();
|
||||
chai.use(require('chai-http'));
|
||||
const expect = chai.expect;
|
||||
|
||||
const UsersService = require('../../../../../services/users');
|
||||
|
||||
describe('/api/v1/auth', () => {
|
||||
describe('#get', () => {
|
||||
it('should return nothing when no user is logged in', () => {
|
||||
@@ -32,7 +32,9 @@ describe('/api/v1/auth/local', () => {
|
||||
};
|
||||
await SettingsService.init(settings);
|
||||
|
||||
const ctx = Context.forSystem();
|
||||
mockUser = await UsersService.createLocalUser(
|
||||
ctx,
|
||||
'maria@gmail.com',
|
||||
'password!',
|
||||
'Maria'
|
||||
|
||||
@@ -3,6 +3,7 @@ const passport = require('../../../passport');
|
||||
const app = require('../../../../../app');
|
||||
const mailer = require('../../../../../services/mailer');
|
||||
|
||||
const Context = require('../../../../../graph/context');
|
||||
const SettingsService = require('../../../../../services/settings');
|
||||
const settings = {
|
||||
id: '1',
|
||||
@@ -20,19 +21,16 @@ const UsersService = require('../../../../../services/users');
|
||||
describe('/api/v1/users/:user_id/email/confirm', () => {
|
||||
let mockUser;
|
||||
|
||||
beforeEach(() =>
|
||||
SettingsService.init(settings)
|
||||
.then(() => {
|
||||
return UsersService.createLocalUser(
|
||||
'ana@gmail.com',
|
||||
'123321123',
|
||||
'Ana'
|
||||
);
|
||||
})
|
||||
.then(user => {
|
||||
mockUser = user;
|
||||
})
|
||||
);
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init(settings);
|
||||
const ctx = Context.forSystem();
|
||||
mockUser = await UsersService.createLocalUser(
|
||||
ctx,
|
||||
'ana@gmail.com',
|
||||
'123321123',
|
||||
'Ana'
|
||||
);
|
||||
});
|
||||
|
||||
describe('#post', () => {
|
||||
it('should send an email when we hit the endpoint', () => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
const CommentModel = require('../../../models/comment');
|
||||
const ActionModel = require('../../../models/action');
|
||||
|
||||
const UsersService = require('../../../services/users');
|
||||
const SettingsService = require('../../../services/settings');
|
||||
const CommentModel = require('../../../models/comment');
|
||||
const CommentsService = require('../../../services/comments');
|
||||
const Context = require('../../../graph/context');
|
||||
const SettingsService = require('../../../services/settings');
|
||||
const UsersService = require('../../../services/users');
|
||||
|
||||
const settings = {
|
||||
id: '1',
|
||||
@@ -119,9 +119,14 @@ describe('services.CommentsService', () => {
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init(settings);
|
||||
|
||||
const ctx = Context.forSystem();
|
||||
await Promise.all([
|
||||
CommentModel.create(comments),
|
||||
UsersService.createLocalUsers(users),
|
||||
Promise.all(
|
||||
users.map(({ email, password, username }) =>
|
||||
UsersService.createLocalUser(ctx, email, password, username)
|
||||
)
|
||||
),
|
||||
ActionModel.create(actions),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const TagsService = require('../../../services/tags');
|
||||
const UsersService = require('../../../services/users');
|
||||
const SettingsService = require('../../../services/settings');
|
||||
|
||||
const CommentModel = require('../../../models/comment');
|
||||
const Context = require('../../../graph/context');
|
||||
|
||||
const chai = require('chai');
|
||||
const expect = chai.expect;
|
||||
@@ -11,7 +11,9 @@ describe('services.TagsService', () => {
|
||||
let comment, user;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
const ctx = Context.forSystem();
|
||||
user = await UsersService.createLocalUser(
|
||||
ctx,
|
||||
'stampi@gmail.com',
|
||||
'1Coral!!',
|
||||
'Stampi'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const TokensService = require('../../../services/tokens');
|
||||
const UsersService = require('../../../services/users');
|
||||
const SettingsService = require('../../../services/settings');
|
||||
const Context = require('../../../graph/context');
|
||||
|
||||
const chai = require('chai');
|
||||
chai.use(require('chai-as-promised'));
|
||||
@@ -10,7 +11,9 @@ describe('services.TokensService', () => {
|
||||
let user;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
const ctx = Context.forSystem();
|
||||
user = await UsersService.createLocalUser(
|
||||
ctx,
|
||||
'sockmonster@gmail.com',
|
||||
'2Coral!!',
|
||||
'Sockmonster'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const UsersService = require('../../../services/users');
|
||||
const SettingsService = require('../../../services/settings');
|
||||
const mailer = require('../../../services/mailer');
|
||||
const Context = require('../../../graph/context');
|
||||
|
||||
const chai = require('chai');
|
||||
chai.use(require('chai-as-promised'));
|
||||
@@ -18,23 +19,28 @@ describe('services.UsersService', () => {
|
||||
};
|
||||
|
||||
await SettingsService.init(settings);
|
||||
mockUsers = await UsersService.createLocalUsers([
|
||||
{
|
||||
email: 'stampi@gmail.com',
|
||||
username: 'Stampi',
|
||||
password: '1Coral!-',
|
||||
},
|
||||
{
|
||||
email: 'sockmonster@gmail.com',
|
||||
username: 'Sockmonster',
|
||||
password: '2Coral!2',
|
||||
},
|
||||
{
|
||||
email: 'marvel@gmail.com',
|
||||
username: 'Marvel',
|
||||
password: '3Coral!3',
|
||||
},
|
||||
]);
|
||||
const ctx = Context.forSystem();
|
||||
mockUsers = await Promise.all(
|
||||
[
|
||||
{
|
||||
email: 'stampi@gmail.com',
|
||||
username: 'Stampi',
|
||||
password: '1Coral!-',
|
||||
},
|
||||
{
|
||||
email: 'sockmonster@gmail.com',
|
||||
username: 'Sockmonster',
|
||||
password: '2Coral!2',
|
||||
},
|
||||
{
|
||||
email: 'marvel@gmail.com',
|
||||
username: 'Marvel',
|
||||
password: '3Coral!3',
|
||||
},
|
||||
].map(({ email, username, password }) =>
|
||||
UsersService.createLocalUser(ctx, email, password, username)
|
||||
)
|
||||
);
|
||||
|
||||
sinon.spy(mailer, 'send');
|
||||
});
|
||||
@@ -102,19 +108,16 @@ describe('services.UsersService', () => {
|
||||
|
||||
describe('#createLocalUser', () => {
|
||||
it('should not create a user with duplicate username', () => {
|
||||
return UsersService.createLocalUsers([
|
||||
{
|
||||
email: 'otrostampi@gmail.com',
|
||||
username: 'StampiTheSecond',
|
||||
password: '1Coralito!',
|
||||
},
|
||||
])
|
||||
.then(user => {
|
||||
expect(user).to.be.null;
|
||||
})
|
||||
.catch(error => {
|
||||
expect(error).to.not.be.null;
|
||||
});
|
||||
const ctx = Context.forSystem();
|
||||
|
||||
return expect(
|
||||
UsersService.createLocalUser(
|
||||
ctx,
|
||||
'otrostampi@gmail.com',
|
||||
'1Coralito!',
|
||||
'Stampi'
|
||||
)
|
||||
).be.rejected;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user