mirror of
https://github.com/wassname/talk.git
synced 2026-06-28 18:45:55 +08:00
refactored resolvers, cleaned
This commit is contained in:
@@ -9,6 +9,7 @@ const subscriptions = require('./subscriptions');
|
||||
const resolvers = require('./resolvers');
|
||||
const mutators = require('./mutators');
|
||||
const loaders = require('./loaders');
|
||||
const schema = require('./schema');
|
||||
|
||||
// Models.
|
||||
const Action = require('../models/action');
|
||||
@@ -88,6 +89,7 @@ const defaultConnectors = {
|
||||
resolvers,
|
||||
mutators,
|
||||
loaders,
|
||||
schema,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ const schema = require('./schema');
|
||||
const Context = require('./context');
|
||||
const { createSubscriptionManager } = require('./subscriptions');
|
||||
const { ENABLE_TRACING } = require('../config');
|
||||
const connectors = require('./connectors');
|
||||
|
||||
module.exports = {
|
||||
createGraphOptions: req => ({
|
||||
@@ -17,4 +18,5 @@ module.exports = {
|
||||
cacheControl: ENABLE_TRACING,
|
||||
}),
|
||||
createSubscriptionManager,
|
||||
connectors,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { SEARCH_OTHER_USERS } = require('../../perms/constants');
|
||||
const { decorateUserField } = require('./util');
|
||||
|
||||
const Action = {
|
||||
__resolveType({ action_type }) {
|
||||
@@ -11,14 +11,8 @@ const Action = {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
// This will load the user for the specific action. We'll limit this to the
|
||||
// admin users only or the current logged in user.
|
||||
user({ user_id }, _, { loaders: { Users }, user }) {
|
||||
if (user && (user.can(SEARCH_OTHER_USERS) || user_id === user.id)) {
|
||||
return Users.getByID.load(user_id);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
decorateUserField(Action, 'user', 'user_id');
|
||||
|
||||
module.exports = Action;
|
||||
|
||||
+10
-10
@@ -1,4 +1,6 @@
|
||||
const { decorateWithTags } = require('./util');
|
||||
const { property } = require('lodash');
|
||||
const { SEARCH_ACTIONS } = require('../../perms/constants');
|
||||
const { decorateWithTags, decorateWithPermissionCheck } = require('./util');
|
||||
|
||||
const Comment = {
|
||||
hasParent({ parent_id }) {
|
||||
@@ -29,15 +31,8 @@ const Comment = {
|
||||
|
||||
return Comments.getByQuery(query);
|
||||
},
|
||||
replyCount({ reply_count }) {
|
||||
// A simple remap from the underlying database model to the graph model.
|
||||
return reply_count;
|
||||
},
|
||||
actions({ id }, _, { user, loaders: { Actions } }) {
|
||||
if (!user || !user.can('SEARCH_ACTIONS')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
replyCount: property('reply_count'),
|
||||
actions({ id }, _, { loaders: { Actions } }) {
|
||||
return Actions.getByID.load(id);
|
||||
},
|
||||
action_summaries({ id, action_summaries }, _, { loaders: { Actions } }) {
|
||||
@@ -65,4 +60,9 @@ const Comment = {
|
||||
// Decorate the Comment type resolver with a tags field.
|
||||
decorateWithTags(Comment);
|
||||
|
||||
// Protect direct action access.
|
||||
decorateWithPermissionCheck(Comment, {
|
||||
actions: [SEARCH_ACTIONS],
|
||||
});
|
||||
|
||||
module.exports = Comment;
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
const FlagAction = {
|
||||
// Stored in the metadata, extract and return.
|
||||
message({ metadata: { message } }) {
|
||||
return message;
|
||||
},
|
||||
reason({ group_id }) {
|
||||
return group_id;
|
||||
},
|
||||
user({ user_id }, _, { loaders: { Users } }) {
|
||||
if (!user_id) {
|
||||
return null;
|
||||
}
|
||||
const { decorateUserField } = require('./util');
|
||||
const { property } = require('lodash');
|
||||
|
||||
return Users.getByID.load(user_id);
|
||||
},
|
||||
const FlagAction = {
|
||||
message: property('metadata.message'),
|
||||
reason: property('group_id'),
|
||||
};
|
||||
|
||||
decorateUserField(FlagAction, 'user', 'user_id');
|
||||
|
||||
module.exports = FlagAction;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const { property } = require('lodash');
|
||||
|
||||
const FlagActionSummary = {
|
||||
reason({ group_id }) {
|
||||
return group_id;
|
||||
},
|
||||
reason: property('group_id'),
|
||||
};
|
||||
|
||||
module.exports = FlagActionSummary;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const _ = require('lodash');
|
||||
const { merge } = require('lodash');
|
||||
const debug = require('debug')('talk:graph:resolvers');
|
||||
|
||||
const Action = require('./action');
|
||||
@@ -70,7 +70,7 @@ resolvers = plugins
|
||||
.reduce((acc, { plugin, resolvers }) => {
|
||||
debug(`added plugin '${plugin.name}'`);
|
||||
|
||||
return _.merge(acc, resolvers);
|
||||
return merge(acc, resolvers);
|
||||
}, resolvers);
|
||||
|
||||
module.exports = resolvers;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const { decorateWithPermissionCheck, checkSelfField } = require('./util');
|
||||
const {
|
||||
SEARCH_ASSETS,
|
||||
SEARCH_OTHERS_COMMENTS,
|
||||
@@ -5,11 +6,7 @@ const {
|
||||
} = require('../../perms/constants');
|
||||
|
||||
const RootQuery = {
|
||||
assets(_, { query }, { loaders: { Assets }, user }) {
|
||||
if (user == null || !user.can(SEARCH_ASSETS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
assets(_, { query }, { loaders: { Assets } }) {
|
||||
return Assets.getByQuery(query);
|
||||
},
|
||||
asset(_, query, { loaders: { Assets } }) {
|
||||
@@ -33,11 +30,7 @@ const RootQuery = {
|
||||
return Comments.get.load(id);
|
||||
},
|
||||
|
||||
async commentCount(_, { query }, { user, loaders: { Comments, Assets } }) {
|
||||
if (user == null || !user.can(SEARCH_OTHERS_COMMENTS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async commentCount(_, { query }, { loaders: { Comments, Assets } }) {
|
||||
const { asset_url, asset_id } = query;
|
||||
if (
|
||||
(!asset_id || asset_id.length === 0) &&
|
||||
@@ -53,11 +46,7 @@ const RootQuery = {
|
||||
return Comments.getCountByQuery(query);
|
||||
},
|
||||
|
||||
async userCount(_, { query }, { user, loaders: { Users } }) {
|
||||
if (user == null || !user.can(SEARCH_OTHER_USERS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async userCount(_, { query }, { loaders: { Users } }) {
|
||||
return Users.getCountByQuery(query);
|
||||
},
|
||||
|
||||
@@ -72,23 +61,37 @@ const RootQuery = {
|
||||
},
|
||||
|
||||
// this returns an arbitrary user
|
||||
user(_, { id }, { user, loaders: { Users } }) {
|
||||
if (user == null || !user.can(SEARCH_OTHER_USERS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
user(_, { id }, { loaders: { Users } }) {
|
||||
return Users.getByID.load(id);
|
||||
},
|
||||
|
||||
// This endpoint is used for loading the user moderation queues (users whose username has been flagged),
|
||||
// so hide it in the event that we aren't an admin.
|
||||
users(_, { query }, { user, loaders: { Users } }) {
|
||||
if (user == null || !user.can(SEARCH_OTHER_USERS)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
users(_, { query }, { loaders: { Users } }) {
|
||||
return Users.getByQuery(query);
|
||||
},
|
||||
};
|
||||
|
||||
// Protect some query fields that are privileged.
|
||||
decorateWithPermissionCheck(RootQuery, {
|
||||
assets: [SEARCH_ASSETS],
|
||||
users: [SEARCH_OTHER_USERS],
|
||||
userCount: [SEARCH_OTHER_USERS],
|
||||
commentCount: [SEARCH_OTHERS_COMMENTS],
|
||||
});
|
||||
|
||||
// Protect the user field so only users who have permission to look up another
|
||||
// user may do so as well as a user looking up themselves.
|
||||
decorateWithPermissionCheck(
|
||||
RootQuery,
|
||||
{
|
||||
user: [SEARCH_OTHER_USERS],
|
||||
},
|
||||
(obj, { id }, { user }) => {
|
||||
if (user && user.id === id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = RootQuery;
|
||||
|
||||
@@ -1,40 +1,23 @@
|
||||
const Subscription = {
|
||||
commentAdded(comment) {
|
||||
return comment;
|
||||
},
|
||||
commentEdited(comment) {
|
||||
return comment;
|
||||
},
|
||||
commentAccepted(comment) {
|
||||
return comment;
|
||||
},
|
||||
commentRejected(comment) {
|
||||
return comment;
|
||||
},
|
||||
commentReset(comment) {
|
||||
return comment;
|
||||
},
|
||||
commentFlagged(comment) {
|
||||
return comment;
|
||||
},
|
||||
userBanned(user) {
|
||||
return user;
|
||||
},
|
||||
userSuspended(user) {
|
||||
return user;
|
||||
},
|
||||
usernameApproved(user) {
|
||||
return user;
|
||||
},
|
||||
usernameRejected(user) {
|
||||
return user;
|
||||
},
|
||||
usernameFlagged(user) {
|
||||
return user;
|
||||
},
|
||||
usernameChanged(payload) {
|
||||
return payload;
|
||||
},
|
||||
};
|
||||
const Subscription = {};
|
||||
|
||||
// All of the subscription endpoints need to have an object to serialize when
|
||||
// pushing out via PubSub, this simply ensures that all these entries will
|
||||
// return the root object from the subscription to the PubSub framework.
|
||||
[
|
||||
'commentAdded',
|
||||
'commentEdited',
|
||||
'commentAccepted',
|
||||
'commentRejected',
|
||||
'commentReset',
|
||||
'commentFlagged',
|
||||
'userBanned',
|
||||
'userSuspended',
|
||||
'usernameApproved',
|
||||
'usernameRejected',
|
||||
'usernameFlagged',
|
||||
'usernameChanged',
|
||||
].forEach(field => {
|
||||
Subscription[field] = obj => obj;
|
||||
});
|
||||
|
||||
module.exports = Subscription;
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
const { SEARCH_OTHER_USERS } = require('../../perms/constants');
|
||||
const { decorateUserField } = require('./util');
|
||||
|
||||
const TagLink = {
|
||||
assigned_by({ assigned_by }, _, { user, loaders: { Users } }) {
|
||||
if (user && user.can(SEARCH_OTHER_USERS) && assigned_by != null) {
|
||||
return Users.getByID.load(assigned_by);
|
||||
}
|
||||
},
|
||||
};
|
||||
const TagLink = {};
|
||||
|
||||
decorateUserField(TagLink, 'assigned_by');
|
||||
|
||||
module.exports = TagLink;
|
||||
|
||||
+38
-55
@@ -1,4 +1,8 @@
|
||||
const { decorateWithTags } = require('./util');
|
||||
const {
|
||||
decorateWithTags,
|
||||
decorateWithPermissionCheck,
|
||||
checkSelfField,
|
||||
} = require('./util');
|
||||
const KarmaService = require('../../services/karma');
|
||||
const {
|
||||
SEARCH_ACTIONS,
|
||||
@@ -7,86 +11,65 @@ const {
|
||||
VIEW_USER_ROLE,
|
||||
LIST_OWN_TOKENS,
|
||||
VIEW_USER_STATUS,
|
||||
VIEW_USER_EMAIL,
|
||||
} = require('../../perms/constants');
|
||||
const { property } = require('lodash');
|
||||
|
||||
const User = {
|
||||
action_summaries({ id }, _, { loaders: { Actions } }) {
|
||||
return Actions.getSummariesByItemID.load(id);
|
||||
},
|
||||
actions({ id }, _, { user, loaders: { Actions } }) {
|
||||
// Only return the actions if the user is not an admin.
|
||||
if (user && user.can(SEARCH_ACTIONS)) {
|
||||
return Actions.getByID.load(id);
|
||||
}
|
||||
return Actions.getByID.load(id);
|
||||
},
|
||||
comments({ id }, { query }, { loaders: { Comments }, user }) {
|
||||
// If there is no user, or there is a user, but they are requesting someone
|
||||
// else's comments, and they aren't allowed, don't return then anything!
|
||||
if (!user || (user.id !== id && !user.can(SEARCH_OTHERS_COMMENTS))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
comments({ id }, { query }, { loaders: { Comments } }) {
|
||||
// Set the author id on the query.
|
||||
query.author_id = id;
|
||||
|
||||
return Comments.getByQuery(query);
|
||||
},
|
||||
profiles({ profiles }, _, { user }) {
|
||||
// if the user is not an admin, do not return the profiles
|
||||
if (user && user.can(SEARCH_OTHER_USERS)) {
|
||||
return profiles;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
tokens({ id, tokens }, args, { user }) {
|
||||
if (!user || (user.id !== id && !user.can(LIST_OWN_TOKENS))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return tokens;
|
||||
},
|
||||
ignoredUsers({ id }, args, { user, loaders: { Users } }) {
|
||||
// Only allow a logged in user that is either the current user or is a staff
|
||||
// member to access the ignoredUsers of a given user.
|
||||
if (!user || (user.id !== id && !user.can(SEARCH_OTHER_USERS))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ignoredUsers({ ignoresUsers }, args, { user, loaders: { Users } }) {
|
||||
// Return nothing if there is nothing to query for.
|
||||
if (!user.ignoresUsers || user.ignoresUsers.length <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Users.getByID.loadMany(user.ignoresUsers);
|
||||
},
|
||||
role({ id, role }, _, { user }) {
|
||||
// If the user is not an admin, only return the current user's roles.
|
||||
if (user && (user.can(VIEW_USER_ROLE) || user.id === id)) {
|
||||
return role;
|
||||
}
|
||||
|
||||
return null;
|
||||
return Users.getByID.loadMany(ignoresUsers);
|
||||
},
|
||||
|
||||
// Extract the reliability from the user metadata if they have permission.
|
||||
reliable(user, _, { user: requestingUser }) {
|
||||
if (requestingUser && requestingUser.can(SEARCH_ACTIONS)) {
|
||||
return KarmaService.model(user);
|
||||
}
|
||||
},
|
||||
reliable: user => KarmaService.model(user),
|
||||
|
||||
state(user, args, ctx) {
|
||||
if (
|
||||
ctx.user &&
|
||||
(ctx.user.id === user.id || ctx.user.can(VIEW_USER_STATUS))
|
||||
) {
|
||||
return user;
|
||||
}
|
||||
},
|
||||
// The state requires the whole user object to make decisions.
|
||||
state: user => user,
|
||||
|
||||
// Get the first email on the user.
|
||||
email: property('firstEmail'),
|
||||
};
|
||||
|
||||
// Decorate the User type resolver with a tags field.
|
||||
decorateWithTags(User);
|
||||
|
||||
// decorate the fields on the User resolver with a permission check where the
|
||||
// current user can also get their own properties.
|
||||
decorateWithPermissionCheck(
|
||||
User,
|
||||
{
|
||||
actions: [SEARCH_ACTIONS],
|
||||
email: [VIEW_USER_EMAIL],
|
||||
state: [VIEW_USER_STATUS],
|
||||
role: [VIEW_USER_ROLE],
|
||||
ignoredUsers: [SEARCH_OTHER_USERS],
|
||||
tokens: [LIST_OWN_TOKENS],
|
||||
profiles: [SEARCH_OTHER_USERS],
|
||||
comments: [SEARCH_OTHERS_COMMENTS],
|
||||
},
|
||||
checkSelfField('id')
|
||||
);
|
||||
|
||||
// Decorate the fields on the User resolver where the current user has no impact
|
||||
// on the resolvability of a field.
|
||||
decorateWithPermissionCheck(User, { reliable: [SEARCH_ACTIONS] });
|
||||
|
||||
module.exports = User;
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
const { decorateWithPermissionCheck, checkSelfField } = require('./util');
|
||||
const { VIEW_USER_STATUS } = require('../../perms/constants');
|
||||
|
||||
const UserState = {
|
||||
status: (user, args, ctx) => {
|
||||
if (
|
||||
ctx.user &&
|
||||
(ctx.user.id === user.id || ctx.user.can(VIEW_USER_STATUS))
|
||||
) {
|
||||
return user.status;
|
||||
}
|
||||
},
|
||||
};
|
||||
const UserState = {};
|
||||
|
||||
decorateWithPermissionCheck(
|
||||
UserState,
|
||||
{ status: [VIEW_USER_STATUS] },
|
||||
checkSelfField('id')
|
||||
);
|
||||
|
||||
module.exports = UserState;
|
||||
|
||||
+168
-32
@@ -2,59 +2,171 @@ const {
|
||||
ADD_COMMENT_TAG,
|
||||
SEARCH_OTHER_USERS,
|
||||
} = require('../../perms/constants');
|
||||
const property = require('lodash/property');
|
||||
const { property, isBoolean } = require('lodash');
|
||||
|
||||
/**
|
||||
* Decorates the typeResolver with the tags field.
|
||||
* getResolver will get the resolver from the typeResolver or apply the default
|
||||
* resolver.
|
||||
*
|
||||
* @param {Object} typeResolver the type resolver
|
||||
* @param {String} field the field name of the resolver we're getting
|
||||
*/
|
||||
const decorateWithTags = typeResolver => {
|
||||
typeResolver.tags = ({ tags = [] }, _, { user }) => {
|
||||
if (user && user.can(ADD_COMMENT_TAG)) {
|
||||
return tags;
|
||||
const getResolver = (typeResolver, field) => {
|
||||
if (field in typeResolver) {
|
||||
return typeResolver[field];
|
||||
}
|
||||
|
||||
return property(field);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} typeResolver the type resolver
|
||||
* @param {String} field the name of the field being wrapped
|
||||
* @param {Function} customCheck the function that can return a boolean
|
||||
* indicating the resolution status
|
||||
* @param {Function} skipFieldResolver the optional skip resolver that can be used
|
||||
* skip out from the oldFieldResolver.
|
||||
*/
|
||||
const wrapCheck = (
|
||||
typeResolver,
|
||||
field,
|
||||
customCheck,
|
||||
skipFieldResolver = getResolver(typeResolver, field)
|
||||
) => {
|
||||
// Cache the old field resolver. In the event that the check does not return
|
||||
// with a boolean, we'll use this.
|
||||
const oldFieldResolver = getResolver(typeResolver, field);
|
||||
|
||||
// Override the field resolver on the type resolver with this wrapped
|
||||
// function.
|
||||
typeResolver[field] = (obj, args, ctx, info) => {
|
||||
const decision = customCheck(obj, args, ctx, info);
|
||||
if (isBoolean(decision)) {
|
||||
if (decision) {
|
||||
// The custom check returns a boolean true, so we should execute the
|
||||
// underlying field resolver (which may just be the old resolver).
|
||||
return skipFieldResolver(obj, args, ctx, info);
|
||||
}
|
||||
|
||||
// The custom check returns a boolean false, then we should return null,
|
||||
// because the check explicity said that we weren't allowed to access that
|
||||
// field.
|
||||
return null;
|
||||
}
|
||||
|
||||
return tags.filter(t => t.tag.permissions.public);
|
||||
// The custom check yielded no decision, so we should just fall back to the
|
||||
// oldFieldResolver.
|
||||
return oldFieldResolver(obj, args, ctx, info);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* checkPermissions checks that the current user has all the required
|
||||
* permissions.
|
||||
*
|
||||
* @param {Object} ctx graph context
|
||||
* @param {Array<String>} permissions permissions that the user must have
|
||||
*/
|
||||
const checkPermissions = (ctx, permissions) =>
|
||||
!ctx.user || !ctx.user.can(...permissions);
|
||||
|
||||
/**
|
||||
* wrapCheckPermissions will wrap a specific field with a permission check.
|
||||
*
|
||||
* @param {Object} typeResolver the type resolver
|
||||
* @param {String} field the field name of the resolver we're wrapping
|
||||
* @param {Array<String>} permissions array of permissions to check against
|
||||
* @param {Function} fieldResolver base resolver for the field
|
||||
*/
|
||||
const wrapCheckPermissions = (
|
||||
typeResolver,
|
||||
field,
|
||||
permissions,
|
||||
skipFieldResolver = getResolver(typeResolver, field)
|
||||
) =>
|
||||
wrapCheck(
|
||||
typeResolver,
|
||||
field,
|
||||
(obj, args, ctx) => {
|
||||
if (checkPermissions(ctx, permissions)) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
skipFieldResolver
|
||||
);
|
||||
|
||||
/**
|
||||
* decorateWithPermissionCheck will decorate the field resolver with
|
||||
* permission checks.
|
||||
*
|
||||
* @param {Object} typeResolver the type resolver
|
||||
* @param {Object} protect the object with field -> Array<String> of permissions
|
||||
* @param {Function} customCheck a function that can return a boolean based on a
|
||||
* custom check
|
||||
*/
|
||||
const decorateWithPermissionCheck = (typeResolver, protect) => {
|
||||
const decorateWithPermissionCheck = (
|
||||
typeResolver,
|
||||
protect,
|
||||
customCheck = null
|
||||
) => {
|
||||
for (const [field, permissions] of Object.entries(protect)) {
|
||||
let fieldResolver = property(field);
|
||||
if (field in typeResolver) {
|
||||
fieldResolver = typeResolver[field];
|
||||
const baseFieldResolver = getResolver(typeResolver, field);
|
||||
wrapCheckPermissions(typeResolver, field, permissions, baseFieldResolver);
|
||||
|
||||
if (customCheck !== null) {
|
||||
wrapCheck(typeResolver, field, customCheck, baseFieldResolver);
|
||||
}
|
||||
|
||||
typeResolver[field] = (obj, args, ctx, info) => {
|
||||
if (!ctx.user || !ctx.user.can(...permissions)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fieldResolver(obj, args, ctx, info);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* decorateUserField will decorate the user field accesses with correct
|
||||
* permission checks.
|
||||
* checkSelf will check if the current object is the same as the current user.
|
||||
*
|
||||
* @param {String} referenceField the field for the user id to check.
|
||||
*/
|
||||
const checkSelfField = referenceField => (obj, args, ctx) => {
|
||||
if (
|
||||
ctx.user &&
|
||||
obj[referenceField] !== null &&
|
||||
ctx.user.id === obj[referenceField]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* wrapCheckSelf wraps a typeResolver with a check for self (if the type is
|
||||
* referencing the current user).
|
||||
*
|
||||
* @param {Object} typeResolver the type resolver
|
||||
* @param {String} field the field to decorate
|
||||
* @param {String} referenceField the field to pull the user id from.
|
||||
* @param {Function} fieldResolver base resolver for the field
|
||||
*/
|
||||
const decorateUserField = (typeResolver, field) => {
|
||||
const wrapCheckSelf = (
|
||||
typeResolver,
|
||||
field,
|
||||
referenceField = field,
|
||||
fieldResolver = getResolver(typeResolver, field)
|
||||
) =>
|
||||
wrapCheck(typeResolver, field, checkSelfField(referenceField), fieldResolver);
|
||||
|
||||
/**
|
||||
* decorateUserField will decorate the user field accesses with correct
|
||||
* permission checks and will load the user.
|
||||
*
|
||||
* @param {Object} typeResolver the type resolver
|
||||
* @param {String} field the field to decorate
|
||||
* @param {String} referenceField the field to pull the user id from.
|
||||
*/
|
||||
const decorateUserField = (typeResolver, field, referenceField = field) => {
|
||||
// The default resolver for the user decorator is loading the user by id.
|
||||
let fieldResolver = (obj, args, ctx) => {
|
||||
if (!obj[field]) {
|
||||
if (!obj[referenceField]) {
|
||||
return null;
|
||||
}
|
||||
return ctx.loaders.Users.getByID.load(obj[field]);
|
||||
return ctx.loaders.Users.getByID.load(obj[referenceField]);
|
||||
};
|
||||
|
||||
// The resolver can be overridden however. This decorator will simply wrap the
|
||||
@@ -63,16 +175,39 @@ const decorateUserField = (typeResolver, field) => {
|
||||
fieldResolver = typeResolver[field];
|
||||
}
|
||||
|
||||
typeResolver[field] = (obj, args, ctx, info) => {
|
||||
if (
|
||||
!ctx.user ||
|
||||
obj[field] === null ||
|
||||
(ctx.user.id !== obj[field] && !ctx.user.can(SEARCH_OTHER_USERS))
|
||||
) {
|
||||
return null;
|
||||
// Wrap the current fieldResolver with the resolver which will return the
|
||||
// user.
|
||||
wrapCheckPermissions(
|
||||
typeResolver,
|
||||
field,
|
||||
[SEARCH_OTHER_USERS],
|
||||
fieldResolver
|
||||
);
|
||||
|
||||
// Wrap the checked resolver with a check to see if the current user is the
|
||||
// one being retrieved, in which case we should allow it.
|
||||
wrapCheckSelf(typeResolver, field, referenceField, fieldResolver);
|
||||
};
|
||||
|
||||
/**
|
||||
* decorateWithTags decorates a typeResolver with a tags getter that will
|
||||
* sanitize the tag output for users without permission to see non-public
|
||||
* tags.
|
||||
*
|
||||
* @param {Object} typeResolver base type resolver
|
||||
* @param {Function} fieldResolver the field resolver that gets the tags
|
||||
*/
|
||||
const decorateWithTags = (
|
||||
typeResolver,
|
||||
fieldResolver = getResolver(typeResolver, 'tags')
|
||||
) => {
|
||||
typeResolver.tags = async (obj, args, ctx, info) => {
|
||||
const tags = await fieldResolver(obj, args, ctx, info);
|
||||
if (checkPermissions(ctx, [ADD_COMMENT_TAG])) {
|
||||
return tags;
|
||||
}
|
||||
|
||||
return fieldResolver(obj, args, ctx, info);
|
||||
return tags.filter(t => t.tag.permissions.public);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -80,4 +215,5 @@ module.exports = {
|
||||
decorateUserField,
|
||||
decorateWithTags,
|
||||
decorateWithPermissionCheck,
|
||||
checkSelfField,
|
||||
};
|
||||
|
||||
+17
-13
@@ -63,7 +63,7 @@ type Token {
|
||||
}
|
||||
|
||||
type UserProfile {
|
||||
# the id is an identifier for the user profile (email, facebook id, etc)
|
||||
# The id is an identifier for the user profile (email, facebook id, etc)
|
||||
id: String!
|
||||
|
||||
# name of the provider attached to the authentication mode
|
||||
@@ -184,13 +184,17 @@ type User {
|
||||
# Actions completed on the parent.
|
||||
actions: [Action!]
|
||||
|
||||
# the current roles of the user.
|
||||
# The current roles of the user.
|
||||
role: USER_ROLES
|
||||
|
||||
# the current profiles of the user.
|
||||
# The current profiles of the user.
|
||||
profiles: [UserProfile]
|
||||
|
||||
# the tags on the user
|
||||
# The primary email address of the user. Only accessible to Administrators or
|
||||
# the current user.
|
||||
email: String
|
||||
|
||||
# The tags on the user.
|
||||
tags: [TagLink!]
|
||||
|
||||
# ignored users.
|
||||
@@ -421,7 +425,7 @@ input CommentCountQuery {
|
||||
# The URL that the asset is located on.
|
||||
asset_url: String
|
||||
|
||||
# the parent of the comment that we want to retrieve.
|
||||
# The parent of the comment that we want to retrieve.
|
||||
parent_id: ID
|
||||
|
||||
# comments returned will only be ones which have at least one action of this
|
||||
@@ -479,13 +483,13 @@ type Comment {
|
||||
# The body history of the comment.
|
||||
body_history: [CommentBodyHistory!]!
|
||||
|
||||
# the tags on the comment
|
||||
# The tags on the comment
|
||||
tags: [TagLink!]
|
||||
|
||||
# the user who authored the comment.
|
||||
# The user who authored the comment.
|
||||
user: User
|
||||
|
||||
# the replies that were made to the comment.
|
||||
# The replies that were made to the comment.
|
||||
replies(query: RepliesQuery = {}): CommentConnection!
|
||||
|
||||
# replyCount is the number of replies with a depth of 1. Only direct replies
|
||||
@@ -765,7 +769,7 @@ type Settings {
|
||||
infoBoxContent: String
|
||||
|
||||
# questionBoxEnable will enable the Question Box's content to be visible above
|
||||
# the comment box.
|
||||
# The comment box.
|
||||
questionBoxEnable: Boolean
|
||||
|
||||
# questionBoxContent is the content of the Question Box.
|
||||
@@ -858,7 +862,7 @@ type Asset {
|
||||
# The date that the asset was created.
|
||||
created_at: Date
|
||||
|
||||
# the tags on the asset
|
||||
# The tags on the asset
|
||||
tags: [TagLink!]
|
||||
|
||||
# The author(s) of the asset.
|
||||
@@ -1091,7 +1095,7 @@ input AssetSettingsInput {
|
||||
moderation: MODERATION_MODE
|
||||
|
||||
# questionBoxEnable will enable the Question Boxs' content to be visible above
|
||||
# the comment box.
|
||||
# The comment box.
|
||||
questionBoxEnable: Boolean
|
||||
|
||||
# questionBoxContent is the content of the Question Box.
|
||||
@@ -1167,7 +1171,7 @@ input ModifyTagInput {
|
||||
item_type: TAGGABLE_ITEM_TYPE!
|
||||
|
||||
# asset_id is used when the item_type is `COMMENTS`, the is needed to rectify
|
||||
# the settings to get the asset specific tags/settings.
|
||||
# The settings to get the asset specific tags/settings.
|
||||
asset_id: ID
|
||||
}
|
||||
|
||||
@@ -1225,7 +1229,7 @@ input UpdateSettingsInput {
|
||||
infoBoxContent: String
|
||||
|
||||
# questionBoxEnable will enable the Question Box's content to be visible above
|
||||
# the comment box.
|
||||
# The comment box.
|
||||
questionBoxEnable: Boolean
|
||||
|
||||
# questionBoxContent is the content of the Question Box.
|
||||
|
||||
@@ -9,4 +9,5 @@ module.exports = {
|
||||
VIEW_PROTECTED_SETTINGS: 'VIEW_PROTECTED_SETTINGS',
|
||||
LIST_OWN_TOKENS: 'LIST_OWN_TOKENS',
|
||||
VIEW_USER_ROLE: 'VIEW_USER_ROLE',
|
||||
VIEW_USER_EMAIL: 'VIEW_USER_EMAIL',
|
||||
};
|
||||
|
||||
@@ -8,11 +8,11 @@ module.exports = (user, perm) => {
|
||||
case types.SEARCH_ACTIONS:
|
||||
case types.SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS:
|
||||
case types.SEARCH_OTHERS_COMMENTS:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.SEARCH_COMMENT_STATUS_HISTORY:
|
||||
case types.VIEW_USER_STATUS:
|
||||
case types.VIEW_PROTECTED_SETTINGS:
|
||||
case types.VIEW_USER_ROLE:
|
||||
case types.VIEW_USER_EMAIL:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.LIST_OWN_TOKENS:
|
||||
return check(user, ['ADMIN']);
|
||||
|
||||
+2
-8
@@ -2,7 +2,6 @@ const SetupService = require('../services/setup');
|
||||
const apollo = require('apollo-server-express');
|
||||
const authentication = require('../middleware/authentication');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const debug = require('debug')('talk:routes');
|
||||
const enabled = require('debug').enabled;
|
||||
const errors = require('../errors');
|
||||
const express = require('express');
|
||||
@@ -159,13 +158,8 @@ if (process.env.NODE_ENV !== 'production') {
|
||||
});
|
||||
}
|
||||
|
||||
// Inject server route plugins.
|
||||
plugins.get('server', 'router').forEach(plugin => {
|
||||
debug(`added plugin '${plugin.plugin.name}'`);
|
||||
|
||||
// Pass the root router to the plugin to mount it's routes.
|
||||
plugin.router(router);
|
||||
});
|
||||
// Mount the plugin routes.
|
||||
router.use(require('./plugins'));
|
||||
|
||||
//==============================================================================
|
||||
// ERROR HANDLING
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
const express = require('express');
|
||||
const debug = require('debug')('talk:routes:plugins');
|
||||
const plugins = require('../services/plugins');
|
||||
const { connectors } = require('../graph');
|
||||
const { set } = require('lodash');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Mount the connectors on the router.
|
||||
router.use((req, res, next) => {
|
||||
set(req, 'talk.connectors', connectors);
|
||||
next();
|
||||
});
|
||||
|
||||
// Inject server route plugins.
|
||||
plugins.get('server', 'router').forEach(plugin => {
|
||||
debug(`added plugin '${plugin.plugin.name}'`);
|
||||
|
||||
// Pass the root router to the plugin to mount it's routes.
|
||||
plugin.router(router);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -22,6 +22,53 @@ describe('graph.queries.user', () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe('email', () => {
|
||||
const query = `
|
||||
query User($user_id: ID!) {
|
||||
user(id: $user_id) {
|
||||
id
|
||||
email
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
it('can query for your own email', async () => {
|
||||
const ctx = new Context({ user });
|
||||
|
||||
const { data, errors } = await graphql(schema, query, {}, ctx, {
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
expect(errors).to.be.undefined;
|
||||
expect(data.user).to.not.be.null;
|
||||
expect(data.user.email).to.be.equal(user.firstEmail);
|
||||
});
|
||||
|
||||
[
|
||||
{ role: 'COMMENTER', can: false },
|
||||
{ role: 'STAFF', can: false },
|
||||
{ role: 'MODERATOR', can: true },
|
||||
{ role: 'ADMIN', can: true },
|
||||
].forEach(({ role, can }) => {
|
||||
it(`${can ? 'can' : 'can not'} query with role = ${role}`, async () => {
|
||||
const actor = new UserModel({ role });
|
||||
const ctx = new Context({ user: actor });
|
||||
|
||||
const { data, errors } = await graphql(schema, query, {}, ctx, {
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
expect(errors).to.be.undefined;
|
||||
if (!can) {
|
||||
expect(data.user).to.be.null;
|
||||
} else {
|
||||
expect(data.user).to.not.be.null;
|
||||
expect(data.user.email).to.be.equal(user.firstEmail);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('state', () => {
|
||||
const meQuery = `
|
||||
query Me {
|
||||
|
||||
Reference in New Issue
Block a user