[BREAK] Remove GraphQL and grant packages (#15192)

pull/15198/head
Diego Sampaio 6 years ago committed by GitHub
parent 9632cb7ec5
commit 4e1d4f504b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .meteor/packages
  2. 1
      .meteor/versions
  3. 3
      app/grant-facebook/README.md
  4. 1
      app/grant-facebook/index.js
  5. 57
      app/grant-facebook/server/index.js
  6. 3
      app/grant-github/README.md
  7. 1
      app/grant-github/index.js
  8. 47
      app/grant-github/server/index.js
  9. 3
      app/grant-google/README.md
  10. 1
      app/grant-google/index.js
  11. 39
      app/grant-google/server/index.js
  12. 101
      app/grant/README.md
  13. 1
      app/grant/index.js
  14. 108
      app/grant/server/authenticate.js
  15. 2
      app/grant/server/error.js
  16. 52
      app/grant/server/grant.js
  17. 58
      app/grant/server/index.js
  18. 40
      app/grant/server/providers.js
  19. 54
      app/grant/server/redirect.js
  20. 48
      app/grant/server/routes.js
  21. 43
      app/grant/server/settings.js
  22. 33
      app/grant/server/storage.js
  23. 3
      app/graphql/README.md
  24. 1
      app/graphql/index.js
  25. 79
      app/graphql/server/api.js
  26. 5
      app/graphql/server/helpers/authenticated.js
  27. 5
      app/graphql/server/helpers/dateToFloat.js
  28. 2
      app/graphql/server/index.js
  29. 21
      app/graphql/server/mocks/accounts/graphql-api.js
  30. 5
      app/graphql/server/resolvers/accounts/OauthProvider-type.js
  31. 22
      app/graphql/server/resolvers/accounts/index.js
  32. 38
      app/graphql/server/resolvers/accounts/oauthProviders.js
  33. 51
      app/graphql/server/resolvers/channels/Channel-type.js
  34. 5
      app/graphql/server/resolvers/channels/ChannelFilter-input.js
  35. 5
      app/graphql/server/resolvers/channels/ChannelNameAndDirect-input.js
  36. 5
      app/graphql/server/resolvers/channels/ChannelSort-enum.js
  37. 5
      app/graphql/server/resolvers/channels/Privacy-enum.js
  38. 24
      app/graphql/server/resolvers/channels/channelByName.js
  39. 55
      app/graphql/server/resolvers/channels/channels.js
  40. 31
      app/graphql/server/resolvers/channels/channelsByUser.js
  41. 39
      app/graphql/server/resolvers/channels/createChannel.js
  42. 41
      app/graphql/server/resolvers/channels/deleteChannel.js
  43. 36
      app/graphql/server/resolvers/channels/directChannel.js
  44. 41
      app/graphql/server/resolvers/channels/hideChannel.js
  45. 52
      app/graphql/server/resolvers/channels/index.js
  46. 31
      app/graphql/server/resolvers/channels/leaveChannel.js
  47. 12
      app/graphql/server/resolvers/channels/settings.js
  48. 74
      app/graphql/server/resolvers/messages/Message-type.js
  49. 5
      app/graphql/server/resolvers/messages/MessageIdentifier-input.js
  50. 5
      app/graphql/server/resolvers/messages/MessagesWithCursor-type.js
  51. 5
      app/graphql/server/resolvers/messages/Reaction-type.js
  52. 22
      app/graphql/server/resolvers/messages/addReactionToMessage.js
  53. 52
      app/graphql/server/resolvers/messages/chatMessageAdded.js
  54. 32
      app/graphql/server/resolvers/messages/deleteMessage.js
  55. 34
      app/graphql/server/resolvers/messages/editMessage.js
  56. 47
      app/graphql/server/resolvers/messages/index.js
  57. 90
      app/graphql/server/resolvers/messages/messages.js
  58. 27
      app/graphql/server/resolvers/messages/sendMessage.js
  59. 29
      app/graphql/server/resolvers/users/User-type.js
  60. 5
      app/graphql/server/resolvers/users/UserStatus-enum.js
  61. 22
      app/graphql/server/resolvers/users/index.js
  62. 22
      app/graphql/server/resolvers/users/setStatus.js
  63. 29
      app/graphql/server/schema.js
  64. 4
      app/graphql/server/schemas/accounts/LoginResult-type.graphqls
  65. 3
      app/graphql/server/schemas/accounts/OauthProvider-type.graphqls
  66. 3
      app/graphql/server/schemas/accounts/oauthProviders.graphqls
  67. 16
      app/graphql/server/schemas/channels/Channel-type.graphqls
  68. 6
      app/graphql/server/schemas/channels/ChannelFilter-input.graphqls
  69. 4
      app/graphql/server/schemas/channels/ChannelNameAndDirect-input.graphqls
  70. 4
      app/graphql/server/schemas/channels/ChannelSort-enum.graphqls
  71. 5
      app/graphql/server/schemas/channels/Privacy-enum.graphqls
  72. 3
      app/graphql/server/schemas/channels/channelByName.graphqls
  73. 7
      app/graphql/server/schemas/channels/channels.graphqls
  74. 3
      app/graphql/server/schemas/channels/channelsByUser.graphqls
  75. 8
      app/graphql/server/schemas/channels/createChannel.graphqls
  76. 3
      app/graphql/server/schemas/channels/deleteChannel.graphqls
  77. 3
      app/graphql/server/schemas/channels/directChannel.graphqls
  78. 3
      app/graphql/server/schemas/channels/hideChannel.graphqls
  79. 3
      app/graphql/server/schemas/channels/leaveChannel.graphqls
  80. 15
      app/graphql/server/schemas/messages/Message-type.graphqls
  81. 4
      app/graphql/server/schemas/messages/MessageIdentifier-input.graphqls
  82. 5
      app/graphql/server/schemas/messages/MessagesWithCursor-type.graphqls
  83. 4
      app/graphql/server/schemas/messages/Reaction-type.graphqls
  84. 3
      app/graphql/server/schemas/messages/addReactionToMessage.graphqls
  85. 3
      app/graphql/server/schemas/messages/chatMessageAdded.graphqls
  86. 3
      app/graphql/server/schemas/messages/deleteMessage.graphqls
  87. 3
      app/graphql/server/schemas/messages/editMessage.graphqls
  88. 11
      app/graphql/server/schemas/messages/messages.graphqls
  89. 3
      app/graphql/server/schemas/messages/sendMessage.graphqls
  90. 8
      app/graphql/server/schemas/users/User-type.graphqls
  91. 7
      app/graphql/server/schemas/users/UserStatus-enum.graphqls
  92. 3
      app/graphql/server/schemas/users/setStatus.graphqls
  93. 9
      app/graphql/server/settings.js
  94. 3
      app/graphql/server/subscriptions.js
  95. 5
      server/importPackages.js
  96. 1
      server/startup/migrations/index.js
  97. 18
      server/startup/migrations/v150.js
  98. 343
      tests/end-to-end/api/11-graphql.js
  99. 14
      tests/pageobjects/administration.page.js

@ -92,7 +92,6 @@ oauth2
raix:eventemitter
routepolicy
sha
swydo:graphql
templating
webapp
webapp-hashing

@ -144,7 +144,6 @@ spacebars@1.0.15
spacebars-compiler@1.1.3
srp@1.0.12
standard-minifier-js@2.4.0
swydo:graphql@0.4.0
templating@1.3.2
templating-compiler@1.3.3
templating-runtime@1.3.2

@ -1,3 +0,0 @@
# rocketchat:grant-facebook
An implementation of the Facebook OAuth flow.

@ -1 +0,0 @@
export * from './server/index';

@ -1,57 +0,0 @@
import { HTTP } from 'meteor/http';
import { Providers, GrantError } from '../../grant';
const userAgent = 'Meteor';
const version = 'v2.10';
function getIdentity(accessToken, fields) {
try {
return HTTP.get(
`https://graph.facebook.com/${ version }/me`, {
headers: { 'User-Agent': userAgent },
params: {
access_token: accessToken,
fields: fields.join(','),
},
}).data;
} catch (err) {
throw new GrantError(`Failed to fetch identity from Facebook. ${ err.message }`);
}
}
function getPicture(accessToken) {
try {
return HTTP.get(
`https://graph.facebook.com/${ version }/me/picture`, {
headers: { 'User-Agent': userAgent },
params: {
redirect: false,
height: 200,
width: 200,
type: 'normal',
access_token: accessToken,
},
}).data;
} catch (err) {
throw new GrantError(`Failed to fetch profile picture from Facebook. ${ err.message }`);
}
}
export function getUser(accessToken) {
const whitelisted = ['id', 'email', 'name', 'first_name', 'last_name'];
const identity = getIdentity(accessToken, whitelisted);
const avatar = getPicture(accessToken);
const username = identity.name.toLowerCase().replace(' ', '.');
return {
id: identity.id,
email: identity.email,
username,
name: `${ identity.first_name } ${ identity.last_name }`,
avatar: avatar.data.url,
};
}
// Register Facebook OAuth
Providers.register('facebook', { scope: ['public_profile', 'email'] }, getUser);

@ -1,3 +0,0 @@
# rocketchat:grant-github
An implementation of the GitHub OAuth flow.

@ -1 +0,0 @@
export * from './server/index';

@ -1,47 +0,0 @@
import { HTTP } from 'meteor/http';
import { Providers, GrantError } from '../../grant';
const userAgent = 'Meteor';
function getIdentity(accessToken) {
try {
return HTTP.get(
'https://api.github.com/user', {
headers: { 'User-Agent': userAgent }, // http://developer.github.com/v3/#user-agent-required
params: { access_token: accessToken },
}).data;
} catch (err) {
throw new GrantError(`Failed to fetch identity from Github. ${ err.message }`);
}
}
function getEmails(accessToken) {
try {
return HTTP.get(
'https://api.github.com/user/emails', {
headers: { 'User-Agent': userAgent }, // http://developer.github.com/v3/#user-agent-required
params: { access_token: accessToken },
}).data;
} catch (err) {
return [];
}
}
export function getUser(accessToken) {
const identity = getIdentity(accessToken);
const emails = getEmails(accessToken);
const primaryEmail = (emails || []).find((email) => email.primary === true);
return {
id: identity.id,
email: identity.email || (primaryEmail && primaryEmail.email) || '',
username: identity.login,
emails,
name: identity.name,
avatar: identity.avatar_url,
};
}
// Register GitHub OAuth
Providers.register('github', { scope: ['user', 'user:email'] }, getUser);

@ -1,3 +0,0 @@
# rocketchat:grant-google
An implementation of the Google OAuth flow.

@ -1 +0,0 @@
export * from './server/index';

@ -1,39 +0,0 @@
import { HTTP } from 'meteor/http';
import { Providers, GrantError } from '../../grant';
const userAgent = 'Meteor';
function getIdentity(accessToken) {
try {
return HTTP.get(
'https://www.googleapis.com/oauth2/v1/userinfo', {
headers: { 'User-Agent': userAgent },
params: {
access_token: accessToken,
},
}).data;
} catch (err) {
throw new GrantError(`Failed to fetch identity from Google. ${ err.message }`);
}
}
export function getUser(accessToken) {
const whitelisted = [
'id', 'email', 'verified_email', 'name',
'given_name', 'family_name', 'picture',
];
const identity = getIdentity(accessToken, whitelisted);
const username = `${ identity.given_name.toLowerCase() }.${ identity.family_name.toLowerCase() }`;
return {
id: identity.id,
email: identity.email,
username,
name: identity.name,
avatar: identity.picture,
};
}
// Register Google OAuth
Providers.register('google', { scope: ['openid', 'email'] }, getUser);

@ -1,101 +0,0 @@
# rocketchat:grant
The main idea behind creating this package was to allow external apps (i.e. PWA) to use OAuth smoothely with currently available accounts system.
## Usage
1. Define providers using `Settings.add()`
1. Add apps with `Settings.apps.add()`
1. Put the path that stars OAuth flow in your app
1. You app should be able to authenticate user with received tokens
## Paths
There are few paths you need to be familiar with.
### Start OAuth flow
> \<ROOT_PATH>/_oauth_apps/connect/\<PROVIDER>/\<APP>
### Authorization callback URL
> \<ROOT_PATH>/_oauth_apps/connect/\<PROVIDER>/callback
### List of available providers
> \<ROOT_PATH>/_oauth_apps/providers
## API
### Providers
#### Providers.register(name, options, getUser)
Allows to register an OAuth Provider.
- name - string that represents the name of an OAuth provider
- options - contains fields like _scope_
- getUser - a function that returns fields: _id, email, username, name and avatar_
### Settings
#### Settings.add(options)
Defines a provider that is able for being used in OAuth.
**options**:
- enabled - __boolean__ - tells to `rocketchat:grant` if provider could be used
- provider - __string__ - id of a provider
- key - __string__ - client ID provided for your OAuth access
- secret - __string__ - secret key
Example:
```js
Settings.add({
enabled: true,
provider: 'google',
key: 'CLIENT_ID',
secret: 'SECRET'
});
```
#### Settings.apps.add(name, options)
Defines an app that is able for using OAuth.
**options**:
- redirectUrl - __string__ - where to redirect if auth was succesful
- errorUrl - __string__ - place to redirect on failure
Example:
```js
const redirectUrl = 'http://localhost:4200/login?service={provider}&access_token={accessToken}&refresh_token={refreshToken}';
const errorUrl = 'http://localhost:4200/login?service={provider}&error={error}'
Settings.apps.add('PWA', {
redirectUrl,
errorUrl
});
```
About URLs:
We use a parser to produce a URL.
There are few available variables for each type of redirect.
- redirectUrl - provider, accessToken, refreshToken
- errorUrl - provider, error
Example:
```
http://localhost:4200/login?provider={provider}
// outputs: http://localhost:4200/login?provider=google
```

@ -1 +0,0 @@
export * from './server/index';

@ -1,108 +0,0 @@
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';
import { GrantError } from './error';
import Providers from './providers';
import { AccountsServer } from '../../accounts';
import { Users } from '../../models';
import { t } from '../../utils';
const setAvatarFromUrl = (userId, url) => new Promise((resolve, reject) => {
Meteor.runAsUser(userId, () => {
Meteor.call('setAvatarFromService', url, '', 'url', (err) => {
if (err) {
if (err.details && err.details.timeToReset) {
reject(t('error-too-many-requests', {
seconds: parseInt(err.details.timeToReset / 1000),
}));
} else {
reject(t('Avatar_url_invalid_or_error'));
}
} else {
resolve();
}
});
});
});
const findUserByOAuthId = (providerName, id) => Users.findOne({ [`settings.profile.oauth.${ providerName }`]: id });
const addOAuthIdToUserProfile = (user, providerName, providerId) => {
const profile = Object.assign({}, user.settings.profile, {
oauth: {
...user.settings.profile.oauth,
[providerName]: providerId,
},
});
Users.setProfile(user.id, profile);
};
function getAccessToken(req) {
const i = req.url.indexOf('?');
if (i === -1) {
return;
}
const barePath = req.url.substring(i + 1);
const splitPath = barePath.split('&');
const token = splitPath.find((p) => p.match(/access_token=[a-zA-Z0-9]+/));
if (token) {
return token.replace('access_token=', '');
}
}
export async function authenticate(providerName, req) {
let tokens;
const accessToken = getAccessToken(req);
const provider = Providers.get(providerName);
if (!provider) {
throw new GrantError(`Provider '${ providerName }' not found`);
}
const userData = provider.getUser(accessToken);
let user = findUserByOAuthId(providerName, userData.id);
if (user) {
user.id = user._id;
} else {
user = Users.findOneByEmailAddress(userData.email);
if (user) {
user.id = user._id;
}
}
if (user) {
addOAuthIdToUserProfile(user, providerName, userData.id);
const loginResult = await AccountsServer.loginWithUser({ id: user.id });
tokens = loginResult.tokens;
} else {
const id = Accounts.createUser({
email: userData.email,
username: userData.username,
});
Users.setProfile(id, {
avatar: userData.avatar,
oauth: {
[providerName]: userData.id,
},
});
Users.setName(id, userData.name);
Users.setEmailVerified(id, userData.email);
await setAvatarFromUrl(id, userData.avatar);
const loginResult = await AccountsServer.loginWithUser({ id });
tokens = loginResult.tokens;
}
return tokens;
}

@ -1,2 +0,0 @@
export class GrantError extends Error {
}

@ -1,52 +0,0 @@
import Providers from './providers';
import Settings from './settings';
import { path, generateCallback, generateAppCallback } from './routes';
import { hostname } from '../../lib';
function addProviders(config) {
Settings.forEach((settings, providerName) => {
if (settings.enabled === true) {
const registeredProvider = Providers.get(providerName);
if (!registeredProvider) {
console.error(`No configuration for '${ providerName }' provider`);
}
// basic settings
const data = {
key: settings.key,
secret: settings.secret,
scope: registeredProvider.scope,
callback: generateCallback(providerName),
};
// set each app
Settings.apps.forEach((_, appName) => {
data[appName] = {
callback: generateAppCallback(providerName, appName),
};
});
config[providerName] = data;
}
});
}
const config = {};
export function generateConfig() {
config.server = {
protocol: 'http',
host: hostname,
path,
state: true,
};
addProviders(config);
return config;
}
export function getConfig() {
return config;
}

@ -1,58 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { WebApp } from 'meteor/webapp';
import session from 'express-session';
import Grant from 'grant-express';
import fiber from 'fibers';
import { GrantError } from './error';
import { generateConfig } from './grant';
import { path, generateCallback, generateAppCallback } from './routes';
import { middleware as redirect } from './redirect';
import Providers, { middleware as providers } from './providers';
import Settings from './settings';
let grant;
WebApp.connectHandlers.use(session({
secret: 'grant',
resave: true,
saveUninitialized: true,
}));
// grant
WebApp.connectHandlers.use(path, (req, res, next) => {
if (grant) {
grant(req, res, next);
} else {
next();
}
});
// callbacks
WebApp.connectHandlers.use((req, res, next) => {
fiber(() => {
redirect(req, res, next);
}).run();
});
// providers
WebApp.connectHandlers.use((req, res, next) => {
fiber(() => {
providers(req, res, next);
}).run();
});
Meteor.startup(() => {
const config = generateConfig();
grant = new Grant(config);
});
export {
path,
generateCallback,
generateAppCallback,
Providers,
Settings,
GrantError,
};

@ -1,40 +0,0 @@
import { Match, check } from 'meteor/check';
import { Storage } from './storage';
import { routes } from './routes';
class Providers extends Storage {
register(name, options, getUser) {
check(name, String);
check(options, {
scope: Match.OneOf(String, [String]),
});
check(getUser, Function);
this._add(name.toLowerCase(), {
scope: options.scope,
getUser,
});
}
}
const providers = new Providers();
export default providers;
export function middleware(req, res, next) {
const route = routes.providers(req);
if (route) {
const list = [];
providers.forEach((_, name) => list.push(name));
res.end(JSON.stringify({
data: list,
}));
return;
}
next();
}

@ -1,54 +0,0 @@
import { authenticate } from './authenticate';
import Settings from './settings';
import { routes } from './routes';
import { GrantError } from './error';
function parseUrl(url, config) {
return url.replace(/\{[\ ]*(provider|accessToken|refreshToken|error)[\ ]*\}/g, (_, key) => config[key]);
}
function getAppConfig(providerName, appName) {
const providerConfig = Settings.get(providerName);
if (providerConfig) {
return Settings.apps.get(appName);
}
}
export async function middleware(req, res, next) {
const route = routes.appCallback(req);
// handle app callback
if (route) {
const config = {
provider: route.provider,
};
const appConfig = getAppConfig(route.provider, route.app);
if (appConfig) {
const {
redirectUrl,
errorUrl,
} = appConfig;
try {
const tokens = await authenticate(route.provider, req);
config.accessToken = tokens.accessToken;
config.refreshToken = tokens.refreshToken;
res.redirect(parseUrl(redirectUrl, config));
return;
} catch (error) {
config.error = error instanceof GrantError ? error.message : 'Something went wrong';
console.error(error);
res.redirect(parseUrl(errorUrl, config));
return;
}
}
}
next();
}

@ -1,48 +0,0 @@
export const path = '/_oauth_apps';
export function generateCallback(providerName) {
return `${ path }/${ providerName }/callback`;
}
export function generateAppCallback(providerName, appName) {
return generateCallback(`${ providerName }/${ appName }`);
}
export function getPaths(req) {
const i = req.url.indexOf('?');
let barePath;
if (i === -1) {
barePath = req.url;
} else {
barePath = req.url.substring(0, i);
}
const splitPath = barePath.split('/');
// Any non-oauth request will continue down the default
// middlewares.
if (splitPath[1] === '_oauth_apps') {
return splitPath.slice(2);
}
}
export const routes = {
// :path/:provider/:app/callback
appCallback: (req) => {
const paths = getPaths(req);
if (paths && paths[2] === 'callback') {
return {
provider: paths[0],
app: paths[1],
};
}
},
// :path/providers
providers: (req) => {
const paths = getPaths(req);
return paths && paths[0] === 'providers';
},
};

@ -1,43 +0,0 @@
import { Match, check } from 'meteor/check';
import { Storage } from './storage';
class Apps extends Storage {
add(name, body) {
check(name, String);
check(body, {
redirectUrl: String,
errorUrl: String,
});
this._add(name, body);
}
}
class Settings extends Storage {
constructor() {
super();
this.apps = new Apps();
}
add(settings) {
check(settings, {
enabled: Match.Optional(Boolean),
provider: String,
key: String,
secret: String,
});
this._add(settings.provider, {
enabled: settings.enabled === true,
provider: settings.provider,
key: settings.key,
secret: settings.secret,
});
}
}
const settings = new Settings();
export default settings;

@ -1,33 +0,0 @@
export class Storage {
constructor() {
this._data = {};
}
all() {
return this._data;
}
forEach(fn) {
Object.keys(this.all())
.forEach((name) => {
fn(this.get(name), name);
});
}
get(name) {
return this.all()[name.toLowerCase()];
}
has(name) {
return !!this._data[name];
}
_add(name, body) {
if (this.has(name)) {
console.error(`'${ name }' have been already defined`);
return;
}
this._data[name] = body;
}
}

@ -1,3 +0,0 @@
# rocketchat:graphql
GraphQL API

@ -1 +0,0 @@
export * from './server/index';

@ -1,79 +0,0 @@
import { graphqlExpress, graphiqlExpress } from 'apollo-server-express';
import { JSAccountsContext as jsAccountsContext } from '@accounts/graphql-api';
import { SubscriptionServer } from 'subscriptions-transport-ws';
import { execute, subscribe } from 'graphql';
import { Meteor } from 'meteor/meteor';
import { WebApp } from 'meteor/webapp';
import bodyParser from 'body-parser';
import express from 'express';
import cors from 'cors';
import { executableSchema } from './schema';
import { settings } from '../../settings';
const subscriptionPort = settings.get('Graphql_Subscription_Port') || 3100;
// the Meteor GraphQL server is an Express server
const graphQLServer = express();
graphQLServer.disable('x-powered-by');
if (settings.get('Graphql_CORS')) {
graphQLServer.use(cors());
}
graphQLServer.use('/api/graphql', (req, res, next) => {
if (settings.get('Graphql_Enabled')) {
next();
} else {
res.status(400).send('Graphql is not enabled in this server');
}
});
graphQLServer.use(
'/api/graphql',
bodyParser.json(),
graphqlExpress((request) => ({
schema: executableSchema,
context: jsAccountsContext(request),
formatError: (e) => ({
message: e.message,
locations: e.locations,
path: e.path,
}),
debug: Meteor.isDevelopment,
}))
);
graphQLServer.use(
'/graphiql',
graphiqlExpress({
endpointURL: '/api/graphql',
subscriptionsEndpoint: `ws://localhost:${ subscriptionPort }`,
})
);
const startSubscriptionServer = () => {
if (settings.get('Graphql_Enabled')) {
SubscriptionServer.create({
schema: executableSchema,
execute,
subscribe,
onConnect: (connectionParams) => ({ authToken: connectionParams.Authorization }),
},
{
port: subscriptionPort,
host: process.env.BIND_IP || '0.0.0.0',
});
console.log('GraphQL Subscription server runs on port:', subscriptionPort);
}
};
WebApp.onListening(() => {
startSubscriptionServer();
});
// this binds the specified paths to the Express server running Apollo + GraphiQL
WebApp.connectHandlers.use(graphQLServer);

@ -1,5 +0,0 @@
import { AccountsServer } from '../../../accounts';
// import { authenticated as _authenticated } from '@accounts/graphql-api';
import { authenticated as _authenticated } from '../mocks/accounts/graphql-api';
export const authenticated = (resolver) => _authenticated(AccountsServer, resolver);

@ -1,5 +0,0 @@
export function dateToFloat(date) {
if (date) {
return new Date(date).getTime();
}
}

@ -1,2 +0,0 @@
import './settings';
import './api';

@ -1,21 +0,0 @@
// Same as here: https://github.com/js-accounts/graphql/blob/master/packages/graphql-api/src/utils/authenticated-resolver.js
// except code below works
// It might be like that because of async/await,
// maybe Promise is not wrapped with Fiber
// See: https://github.com/meteor/meteor/blob/a362e20a37547362b581fed52f7171d022e83b62/packages/promise/server.js
// Opened issue: https://github.com/js-accounts/graphql/issues/16
export const authenticated = (Accounts, func) => async (root, args, context, info) => {
const { authToken } = context;
if (!authToken || authToken === '' || authToken === null) {
throw new Error('Unable to find authorization token in request');
}
const userObject = await Accounts.resumeSession(authToken);
if (userObject === null) {
throw new Error('Invalid or expired token!');
}
return func(root, args, Object.assign(context, { user: userObject }), info);
};

@ -1,5 +0,0 @@
import schema from '../../schemas/accounts/OauthProvider-type.graphqls';
export {
schema,
};

@ -1,22 +0,0 @@
import { createJSAccountsGraphQL } from '@accounts/graphql-api';
import { mergeTypes, mergeResolvers } from 'merge-graphql-schemas';
// queries
import * as oauthProviders from './oauthProviders';
// types
import * as OauthProviderType from './OauthProvider-type';
import { AccountsServer } from '../../../../accounts';
const accountsGraphQL = createJSAccountsGraphQL(AccountsServer);
export const schema = mergeTypes([
accountsGraphQL.schema,
oauthProviders.schema,
OauthProviderType.schema,
]);
export const resolvers = mergeResolvers([
accountsGraphQL.extendWithResolvers({}),
oauthProviders.resolver,
]);

@ -1,38 +0,0 @@
import { HTTP } from 'meteor/http';
import { Meteor } from 'meteor/meteor';
import schema from '../../schemas/accounts/oauthProviders.graphqls';
function isJSON(obj) {
try {
JSON.parse(obj);
return true;
} catch (e) {
return false;
}
}
const resolver = {
Query: {
oauthProviders: async () => {
// depends on rocketchat:grant package
try {
const result = HTTP.get(Meteor.absoluteUrl('_oauth_apps/providers')).content;
if (isJSON(result)) {
const providers = JSON.parse(result).data;
return providers.map((name) => ({ name }));
}
throw new Error('Could not parse the result');
} catch (e) {
throw new Error('rocketchat:grant not installed');
}
},
},
};
export {
schema,
resolver,
};

@ -1,51 +0,0 @@
import property from 'lodash.property';
import { Subscriptions, Users } from '../../../../models';
import schema from '../../schemas/channels/Channel-type.graphqls';
const resolver = {
Channel: {
id: property('_id'),
name: (root, args, { user }) => {
if (root.t === 'd') {
return root.usernames.find((u) => u !== user.username);
}
return root.name;
},
members: (root) => {
const ids = Subscriptions.findByRoomIdWhenUserIdExists(root._id, { fields: { 'u._id': 1 } })
.fetch()
.map((sub) => sub.u._id);
return Users.findByIds(ids).fetch();
},
owners: (root) => {
// there might be no owner
if (!root.u) {
return;
}
return [Users.findOneByUsername(root.u.username)];
},
numberOfMembers: (root) => Subscriptions.findByRoomId(root._id).count(),
numberOfMessages: property('msgs'),
readOnly: (root) => root.ro === true,
direct: (root) => root.t === 'd',
privateChannel: (root) => root.t === 'p',
favourite: (root, args, { user }) => {
const room = Subscriptions.findOneByRoomIdAndUserId(root._id, user._id);
return room && room.f === true;
},
unseenMessages: (root, args, { user }) => {
const room = Subscriptions.findOneByRoomIdAndUserId(root._id, user._id);
return (room || {}).unread;
},
},
};
export {
schema,
resolver,
};

@ -1,5 +0,0 @@
import schema from '../../schemas/channels/ChannelFilter-input.graphqls';
export {
schema,
};

@ -1,5 +0,0 @@
import schema from '../../schemas/channels/ChannelNameAndDirect-input.graphqls';
export {
schema,
};

@ -1,5 +0,0 @@
import schema from '../../schemas/channels/ChannelSort-enum.graphqls';
export {
schema,
};

@ -1,5 +0,0 @@
import schema from '../../schemas/channels/Privacy-enum.graphqls';
export {
schema,
};

@ -1,24 +0,0 @@
import { roomPublicFields } from './settings';
import { Rooms } from '../../../../models';
import { authenticated } from '../../helpers/authenticated';
import schema from '../../schemas/channels/channelByName.graphqls';
const resolver = {
Query: {
channelByName: authenticated((root, { name }) => {
const query = {
name,
t: 'c',
};
return Rooms.findOne(query, {
fields: roomPublicFields,
});
}),
},
};
export {
schema,
resolver,
};

@ -1,55 +0,0 @@
import { roomPublicFields } from './settings';
import { Rooms } from '../../../../models';
import { authenticated } from '../../helpers/authenticated';
import schema from '../../schemas/channels/channels.graphqls';
const resolver = {
Query: {
channels: authenticated((root, args) => {
const query = {};
const options = {
sort: {
name: 1,
},
fields: roomPublicFields,
};
// Filter
if (typeof args.filter !== 'undefined') {
// nameFilter
if (typeof args.filter.nameFilter !== 'undefined') {
query.name = {
$regex: new RegExp(args.filter.nameFilter, 'i'),
};
}
// sortBy
if (args.filter.sortBy === 'NUMBER_OF_MESSAGES') {
options.sort = {
msgs: -1,
};
}
// privacy
switch (args.filter.privacy) {
case 'PRIVATE':
query.t = 'p';
break;
case 'PUBLIC':
query.t = {
$ne: 'p',
};
break;
}
}
return Rooms.find(query, options).fetch();
}),
},
};
export {
schema,
resolver,
};

@ -1,31 +0,0 @@
import { roomPublicFields } from './settings';
import { Users, Subscriptions, Rooms } from '../../../../models';
import { authenticated } from '../../helpers/authenticated';
import schema from '../../schemas/channels/channelsByUser.graphqls';
const resolver = {
Query: {
channelsByUser: authenticated((root, { userId }) => {
const user = Users.findOneById(userId);
if (!user) {
throw new Error('No user');
}
const roomIds = Subscriptions.findByUserId(userId, { fields: { rid: 1 } }).fetch().map((s) => s.rid);
const rooms = Rooms.findByIds(roomIds, {
sort: {
name: 1,
},
fields: roomPublicFields,
}).fetch();
return rooms;
}),
},
};
export {
schema,
resolver,
};

@ -1,39 +0,0 @@
import { API } from '../../../../api';
import { authenticated } from '../../helpers/authenticated';
import schema from '../../schemas/channels/createChannel.graphqls';
const resolver = {
Mutation: {
createChannel: authenticated((root, args, { user }) => {
try {
API.channels.create.validate({
user: {
value: user._id,
},
name: {
value: args.name,
key: 'name',
},
members: {
value: args.membersId,
key: 'membersId',
},
});
} catch (e) {
throw e;
}
const { channel } = API.channels.create.execute(user._id, {
name: args.name,
members: args.membersId,
});
return channel;
}),
},
};
export {
schema,
resolver,
};

@ -1,41 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Rooms, Subscriptions } from '../../../../models';
import { authenticated } from '../../helpers/authenticated';
import schema from '../../schemas/channels/deleteChannel.graphqls';
const resolver = {
Mutation: {
deleteChannel: authenticated((root, { channelId }, { user }) => {
const channel = Rooms.findOne({
_id: channelId,
t: 'c',
});
if (!channel) {
throw new Error('error-room-not-found', 'The required "channelId" param provided does not match any channel');
}
const sub = Subscriptions.findOneByRoomIdAndUserId(channel._id, user._id);
if (!sub) {
throw new Error(`The user/callee is not in the channel "${ channel.name }.`);
}
if (!sub.open) {
throw new Error(`The channel, ${ channel.name }, is already closed to the sender`);
}
Meteor.runAsUser(user._id, () => {
Meteor.call('eraseRoom', channel._id);
});
return true;
}),
},
};
export {
schema,
resolver,
};

@ -1,36 +0,0 @@
import { roomPublicFields } from './settings';
import { Rooms } from '../../../../models';
import { authenticated } from '../../helpers/authenticated';
import schema from '../../schemas/channels/directChannel.graphqls';
const resolver = {
Query: {
directChannel: authenticated((root, { username, channelId }, { user }) => {
const query = {
t: 'd',
usernames: user.username,
};
if (typeof username !== 'undefined') {
if (username === user.username) {
throw new Error('You cannot specify your username');
}
query.usernames = { $all: [user.username, username] };
} else if (typeof channelId !== 'undefined') {
query.id = channelId;
} else {
throw new Error('Use one of those fields: username, channelId');
}
return Rooms.findOne(query, {
fields: roomPublicFields,
});
}),
},
};
export {
schema,
resolver,
};

@ -1,41 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Rooms, Subscriptions } from '../../../../models';
import { authenticated } from '../../helpers/authenticated';
import schema from '../../schemas/channels/hideChannel.graphqls';
const resolver = {
Mutation: {
hideChannel: authenticated((root, args, { user }) => {
const channel = Rooms.findOne({
_id: args.channelId,
t: 'c',
});
if (!channel) {
throw new Error('error-room-not-found', 'The required "channelId" param provided does not match any channel');
}
const sub = Subscriptions.findOneByRoomIdAndUserId(channel._id, user._id);
if (!sub) {
throw new Error(`The user/callee is not in the channel "${ channel.name }.`);
}
if (!sub.open) {
throw new Error(`The channel, ${ channel.name }, is already closed to the sender`);
}
Meteor.runAsUser(user._id, () => {
Meteor.call('hideRoom', channel._id);
});
return true;
}),
},
};
export {
schema,
resolver,
};

@ -1,52 +0,0 @@
import { mergeTypes, mergeResolvers } from 'merge-graphql-schemas';
// queries
import * as channels from './channels';
import * as channelByName from './channelByName';
import * as directChannel from './directChannel';
import * as channelsByUser from './channelsByUser';
// mutations
import * as createChannel from './createChannel';
import * as leaveChannel from './leaveChannel';
import * as hideChannel from './hideChannel';
import * as deleteChannel from './deleteChannel';
// types
import * as ChannelType from './Channel-type';
import * as ChannelSort from './ChannelSort-enum';
import * as ChannelFilter from './ChannelFilter-input';
import * as Privacy from './Privacy-enum';
import * as ChannelNameAndDirect from './ChannelNameAndDirect-input';
export const schema = mergeTypes([
// queries
channels.schema,
channelByName.schema,
directChannel.schema,
channelsByUser.schema,
// mutations
createChannel.schema,
leaveChannel.schema,
hideChannel.schema,
deleteChannel.schema,
// types
ChannelType.schema,
ChannelSort.schema,
ChannelFilter.schema,
Privacy.schema,
ChannelNameAndDirect.schema,
]);
export const resolvers = mergeResolvers([
// queries
channels.resolver,
channelByName.resolver,
directChannel.resolver,
channelsByUser.resolver,
// mutations
createChannel.resolver,
leaveChannel.resolver,
hideChannel.resolver,
deleteChannel.resolver,
// types
ChannelType.resolver,
]);

@ -1,31 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Rooms } from '../../../../models';
import { authenticated } from '../../helpers/authenticated';
import schema from '../../schemas/channels/leaveChannel.graphqls';
const resolver = {
Mutation: {
leaveChannel: authenticated((root, args, { user }) => {
const channel = Rooms.findOne({
_id: args.channelId,
t: 'c',
});
if (!channel) {
throw new Error('error-room-not-found', 'The required "channelId" param provided does not match any channel');
}
Meteor.runAsUser(user._id, () => {
Meteor.call('leaveRoom', channel._id);
});
return true;
}),
},
};
export {
schema,
resolver,
};

@ -1,12 +0,0 @@
export const roomPublicFields = {
t: 1,
name: 1,
description: 1,
announcement: 1,
topic: 1,
usernames: 1,
msgs: 1,
ro: 1,
u: 1,
archived: 1,
};

@ -1,74 +0,0 @@
import property from 'lodash.property';
import { Rooms, Users } from '../../../../models';
import { dateToFloat } from '../../helpers/dateToFloat';
import schema from '../../schemas/messages/Message-type.graphqls';
const resolver = {
Message: {
id: property('_id'),
content: property('msg'),
creationTime: (root) => dateToFloat(root.ts),
author: (root) => {
const user = Users.findOne(root.u._id);
return user || root.u;
},
channel: (root) => Rooms.findOne(root.rid),
fromServer: (root) => typeof root.t !== 'undefined', // on a message sent by user `true` otherwise `false`
type: property('t'),
channelRef: (root) => {
if (!root.channels) {
return;
}
return Rooms.find({
_id: {
$in: root.channels.map((c) => c._id),
},
}, {
sort: {
name: 1,
},
}).fetch();
},
userRef: (root) => {
if (!root.mentions) {
return;
}
return Users.find({
_id: {
$in: root.mentions.map((c) => c._id),
},
}, {
sort: {
username: 1,
},
}).fetch();
},
reactions: (root) => {
if (!root.reactions || Object.keys(root.reactions).length === 0) {
return;
}
const reactions = [];
Object.keys(root.reactions).forEach((icon) => {
root.reactions[icon].usernames.forEach((username) => {
reactions.push({
icon,
username,
});
});
});
return reactions;
},
},
};
export {
schema,
resolver,
};

@ -1,5 +0,0 @@
import schema from '../../schemas/messages/MessageIdentifier-input.graphqls';
export {
schema,
};

@ -1,5 +0,0 @@
import schema from '../../schemas/messages/MessagesWithCursor-type.graphqls';
export {
schema,
};

@ -1,5 +0,0 @@
import schema from '../../schemas/messages/Reaction-type.graphqls';
export {
schema,
};

@ -1,22 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Messages } from '../../../../models';
import { authenticated } from '../../helpers/authenticated';
import schema from '../../schemas/messages/addReactionToMessage.graphqls';
const resolver = {
Mutation: {
addReactionToMessage: authenticated((root, { id, icon, shouldReact }, { user }) => new Promise((resolve) => {
Meteor.runAsUser(user._id, () => {
Meteor.call('setReaction', icon, id.messageId, shouldReact, () => {
resolve(Messages.findOne(id.messageId));
});
});
})),
},
};
export {
schema,
resolver,
};

@ -1,52 +0,0 @@
import { withFilter } from 'graphql-subscriptions';
import { Rooms } from '../../../../models';
import { callbacks } from '../../../../callbacks';
import { pubsub } from '../../subscriptions';
import { authenticated } from '../../helpers/authenticated';
import schema from '../../schemas/messages/chatMessageAdded.graphqls';
export const CHAT_MESSAGE_SUBSCRIPTION_TOPIC = 'CHAT_MESSAGE_ADDED';
export function publishMessage(message) {
pubsub.publish(CHAT_MESSAGE_SUBSCRIPTION_TOPIC, { chatMessageAdded: message });
}
function shouldPublish(message, { id, directTo }, username) {
if (id) {
return message.rid === id;
} if (directTo) {
const room = Rooms.findOne({
usernames: { $all: [directTo, username] },
t: 'd',
});
return room && room._id === message.rid;
}
return false;
}
const resolver = {
Subscription: {
chatMessageAdded: {
subscribe: withFilter(() => pubsub.asyncIterator(CHAT_MESSAGE_SUBSCRIPTION_TOPIC), authenticated((payload, args, { user }) => {
const channel = {
id: args.channelId,
directTo: args.directTo,
};
return shouldPublish(payload.chatMessageAdded, channel, user.username);
})),
},
},
};
callbacks.add('afterSaveMessage', (message) => {
publishMessage(message);
}, callbacks.priority.MEDIUM, 'chatMessageAddedSubscription');
export {
schema,
resolver,
};

@ -1,32 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Messages } from '../../../../models';
import { authenticated } from '../../helpers/authenticated';
import schema from '../../schemas/messages/deleteMessage.graphqls';
const resolver = {
Mutation: {
deleteMessage: authenticated((root, { id }, { user }) => {
const msg = Messages.findOneById(id.messageId, { fields: { u: 1, rid: 1 } });
if (!msg) {
throw new Error(`No message found with the id of "${ id.messageId }".`);
}
if (id.channelId !== msg.rid) {
throw new Error('The room id provided does not match where the message is from.');
}
Meteor.runAsUser(user._id, () => {
Meteor.call('deleteMessage', { _id: msg._id });
});
return msg;
}),
},
};
export {
schema,
resolver,
};

@ -1,34 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Messages } from '../../../../models';
import { authenticated } from '../../helpers/authenticated';
import schema from '../../schemas/messages/editMessage.graphqls';
const resolver = {
Mutation: {
editMessage: authenticated((root, { id, content }, { user }) => {
const msg = Messages.findOneById(id.messageId);
// Ensure the message exists
if (!msg) {
throw new Error(`No message found with the id of "${ id.messageId }".`);
}
if (id.channelId !== msg.rid) {
throw new Error('The channel id provided does not match where the message is from.');
}
// Permission checks are already done in the updateMessage method, so no need to duplicate them
Meteor.runAsUser(user._id, () => {
Meteor.call('updateMessage', { _id: msg._id, msg: content, rid: msg.rid });
});
return Messages.findOneById(msg._id);
}),
},
};
export {
schema,
resolver,
};

@ -1,47 +0,0 @@
import { mergeTypes, mergeResolvers } from 'merge-graphql-schemas';
// queries
import * as messages from './messages';
// mutations
import * as sendMessage from './sendMessage';
import * as editMessage from './editMessage';
import * as deleteMessage from './deleteMessage';
import * as addReactionToMessage from './addReactionToMessage';
// subscriptions
import * as chatMessageAdded from './chatMessageAdded';
// types
import * as MessageType from './Message-type';
import * as MessagesWithCursorType from './MessagesWithCursor-type';
import * as MessageIdentifier from './MessageIdentifier-input';
import * as ReactionType from './Reaction-type';
export const schema = mergeTypes([
// queries
messages.schema,
// mutations
sendMessage.schema,
editMessage.schema,
deleteMessage.schema,
addReactionToMessage.schema,
// subscriptions
chatMessageAdded.schema,
// types
MessageType.schema,
MessagesWithCursorType.schema,
MessageIdentifier.schema,
ReactionType.schema,
]);
export const resolvers = mergeResolvers([
// queries
messages.resolver,
// mutations
sendMessage.resolver,
editMessage.resolver,
deleteMessage.resolver,
addReactionToMessage.resolver,
// subscriptions
chatMessageAdded.resolver,
// types
MessageType.resolver,
]);

@ -1,90 +0,0 @@
import { Rooms, Messages } from '../../../../models';
import { authenticated } from '../../helpers/authenticated';
import schema from '../../schemas/messages/messages.graphqls';
const resolver = {
Query: {
messages: authenticated((root, args, { user }) => {
const messagesQuery = {};
const messagesOptions = {
sort: { ts: -1 },
};
const channelQuery = {};
const isPagination = !!args.cursor || args.count > 0;
let cursor;
if (args.channelId) {
// channelId
channelQuery._id = args.channelId;
} else if (args.directTo) {
// direct message where directTo is a user id
channelQuery.t = 'd';
channelQuery.usernames = { $all: [args.directTo, user.username] };
} else if (args.channelName) {
// non-direct channel
channelQuery.t = { $ne: 'd' };
channelQuery.name = args.channelName;
} else {
console.error('messages query must be called with channelId or directTo');
return null;
}
const channel = Rooms.findOne(channelQuery);
let messagesArray = [];
if (channel) {
// cursor
if (isPagination && args.cursor) {
const cursorMsg = Messages.findOne(args.cursor, { fields: { ts: 1 } });
messagesQuery.ts = { $lt: cursorMsg.ts };
}
// search
if (typeof args.searchRegex === 'string') {
messagesQuery.msg = {
$regex: new RegExp(args.searchRegex, 'i'),
};
}
// count
if (isPagination && args.count) {
messagesOptions.limit = args.count;
}
// exclude messages generated by server
if (args.excludeServer === true) {
messagesQuery.t = { $exists: false };
}
// look for messages that belongs to specific channel
messagesQuery.rid = channel._id;
const messages = Messages.find(messagesQuery, messagesOptions);
messagesArray = messages.fetch();
if (isPagination) {
// oldest first (because of findOne)
messagesOptions.sort.ts = 1;
const firstMessage = Messages.findOne(messagesQuery, messagesOptions);
const lastId = (messagesArray[messagesArray.length - 1] || {})._id;
cursor = !lastId || lastId === firstMessage._id ? null : lastId;
}
}
return {
cursor,
channel,
messagesArray,
};
}),
},
};
export {
schema,
resolver,
};

@ -1,27 +0,0 @@
import { processWebhookMessage } from '../../../../lib';
import { authenticated } from '../../helpers/authenticated';
import schema from '../../schemas/messages/sendMessage.graphqls';
const resolver = {
Mutation: {
sendMessage: authenticated((root, { channelId, directTo, content }, { user }) => {
const options = {
text: content,
channel: channelId || directTo,
};
const messageReturn = processWebhookMessage(options, user)[0];
if (!messageReturn) {
throw new Error('Unknown error');
}
return messageReturn.message;
}),
},
};
export {
schema,
resolver,
};

@ -1,29 +0,0 @@
import { Meteor } from 'meteor/meteor';
import property from 'lodash.property';
import { Avatars, Rooms } from '../../../../models';
import schema from '../../schemas/users/User-type.graphqls';
const resolver = {
User: {
id: property('_id'),
status: ({ status }) => status.toUpperCase(),
avatar: async ({ _id }) => {
// XXX js-accounts/graphql#16
const avatar = await Avatars.model.rawCollection().findOne({
userId: _id,
}, { fields: { url: 1 } });
if (avatar) {
return avatar.url;
}
},
channels: Meteor.bindEnvironment(({ _id }) => Rooms.findBySubscriptionUserId(_id).fetch()),
directMessages: ({ username }) => Rooms.findDirectRoomContainingUsername(username).fetch(),
},
};
export {
schema,
resolver,
};

@ -1,5 +0,0 @@
import schema from '../../schemas/users/UserStatus-enum.graphqls';
export {
schema,
};

@ -1,22 +0,0 @@
import { mergeTypes, mergeResolvers } from 'merge-graphql-schemas';
// mutations
import * as setStatus from './setStatus';
// types
import * as UserType from './User-type';
import * as UserStatus from './UserStatus-enum';
export const schema = mergeTypes([
// mutations
setStatus.schema,
// types
UserType.schema,
UserStatus.schema,
]);
export const resolvers = mergeResolvers([
// mutations
setStatus.resolver,
// types
UserType.resolver,
]);

@ -1,22 +0,0 @@
import { Users } from '../../../../models';
import { authenticated } from '../../helpers/authenticated';
import schema from '../../schemas/users/setStatus.graphqls';
const resolver = {
Mutation: {
setStatus: authenticated((root, { status }, { user }) => {
Users.update(user._id, {
$set: {
status: status.toLowerCase(),
},
});
return Users.findOne(user._id);
}),
},
};
export {
schema,
resolver,
};

@ -1,29 +0,0 @@
import { makeExecutableSchema } from 'graphql-tools';
import { mergeTypes, mergeResolvers } from 'merge-graphql-schemas';
import * as channels from './resolvers/channels';
import * as messages from './resolvers/messages';
import * as accounts from './resolvers/accounts';
import * as users from './resolvers/users';
const schema = mergeTypes([
channels.schema,
messages.schema,
accounts.schema,
users.schema,
]);
const resolvers = mergeResolvers([
channels.resolvers,
messages.resolvers,
accounts.resolvers,
users.resolvers,
]);
export const executableSchema = makeExecutableSchema({
typeDefs: [schema],
resolvers,
logger: {
log: (e) => console.log(e),
},
});

@ -1,4 +0,0 @@
type LoginResult {
accessToken: String!
refreshToken: String!
}

@ -1,3 +0,0 @@
type OauthProvider {
name: String!
}

@ -1,3 +0,0 @@
type Query {
oauthProviders: [OauthProvider]
}

@ -1,16 +0,0 @@
type Channel {
id: String!
name: String
description: String
announcement: String
topic: String
members: [User]
owners: [User]
numberOfMembers: Int
numberOfMessages: Int
readOnly: Boolean
direct: Boolean
privateChannel: Boolean
favourite: Boolean
unseenMessages: Int
}

@ -1,6 +0,0 @@
input ChannelFilter {
nameFilter: String
privacy: Privacy
joinedChannels: Boolean
sortBy: ChannelSort
}

@ -1,4 +0,0 @@
input ChannelNameAndDirect {
name: String!
direct: Boolean!
}

@ -1,4 +0,0 @@
enum ChannelSort {
NAME
NUMBER_OF_MESSAGES
}

@ -1,5 +0,0 @@
enum Privacy {
PRIVATE
PUBLIC
ALL
}

@ -1,3 +0,0 @@
type Query {
channelByName(name: String!): Channel
}

@ -1,7 +0,0 @@
type Query {
channels(filter: ChannelFilter = {
privacy: ALL,
joinedChannels: false,
sortBy: NAME
}): [Channel]
}

@ -1,3 +0,0 @@
type Query {
channelsByUser(userId: String!): [Channel]
}

@ -1,8 +0,0 @@
type Mutation {
createChannel(
name: String!,
private: Boolean = false,
readOnly: Boolean = false,
membersId: [String!]
): Channel
}

@ -1,3 +0,0 @@
type Mutation {
deleteChannel(channelId: String!): Boolean
}

@ -1,3 +0,0 @@
type Query {
directChannel(username: String, channelId: String): Channel
}

@ -1,3 +0,0 @@
type Mutation {
hideChannel(channelId: String!): Boolean
}

@ -1,3 +0,0 @@
type Mutation {
leaveChannel(channelId: String!): Boolean
}

@ -1,15 +0,0 @@
type Message {
id: String
author: User
content: String
channel: Channel
creationTime: Float
# Message sent by server e.g. User joined channel
fromServer: Boolean
type: String
# List of mentioned users
userRef: [User]
# list of mentioned channels
channelRef: [Channel]
reactions: [Reaction]
}

@ -1,4 +0,0 @@
input MessageIdentifier {
channelId: String!
messageId: String!
}

@ -1,5 +0,0 @@
type MessagesWithCursor {
cursor: String
channel: Channel
messagesArray: [Message]
}

@ -1,4 +0,0 @@
type Reaction {
username: String
icon: String
}

@ -1,3 +0,0 @@
type Mutation {
addReactionToMessage(id: MessageIdentifier!, icon: String!, shouldReact: Boolean): Message
}

@ -1,3 +0,0 @@
type Subscription {
chatMessageAdded(channelId: String, directTo: String): Message
}

@ -1,3 +0,0 @@
type Mutation {
deleteMessage(id: MessageIdentifier!): Message
}

@ -1,3 +0,0 @@
type Mutation {
editMessage(id: MessageIdentifier!, content: String!): Message
}

@ -1,11 +0,0 @@
type Query {
messages(
channelId: String,
channelName: String,
directTo: String,
cursor: String,
count: Int,
searchRegex: String,
excludeServer: Boolean
): MessagesWithCursor
}

@ -1,3 +0,0 @@
type Mutation {
sendMessage(channelId: String, directTo: String, content: String!): Message
}

@ -1,8 +0,0 @@
extend type User {
status: UserStatus
avatar: String
name: String
lastLogin: String
channels: [Channel]
directMessages: [Channel]
}

@ -1,7 +0,0 @@
enum UserStatus {
ONLINE
AWAY
BUSY
INVISIBLE
OFFLINE
}

@ -1,3 +0,0 @@
type Mutation {
setStatus(status: UserStatus!): User
}

@ -1,9 +0,0 @@
import { settings } from '../../settings';
settings.addGroup('General', function() {
this.section('GraphQL API', function() {
this.add('Graphql_Enabled', false, { type: 'boolean', public: false });
this.add('Graphql_CORS', true, { type: 'boolean', public: false, enableQuery: { _id: 'Graphql_Enabled', value: true } });
this.add('Graphql_Subscription_Port', 3100, { type: 'int', public: false, enableQuery: { _id: 'Graphql_Enabled', value: true } });
});
});

@ -1,3 +0,0 @@
import { PubSub } from 'graphql-subscriptions';
export const pubsub = new PubSub();

@ -29,11 +29,6 @@ import '../app/file-upload';
import '../app/github-enterprise/server';
import '../app/gitlab/server';
import '../app/google-vision/server';
import '../app/grant';
import '../app/grant-facebook';
import '../app/grant-github';
import '../app/grant-google';
import '../app/graphql';
import '../app/iframe-login/server';
import '../app/importer/server';
import '../app/importer-csv/server';

@ -147,4 +147,5 @@ import './v146';
import './v147';
import './v148';
import './v149';
import './v150';
import './xrun';

@ -0,0 +1,18 @@
import { Migrations } from '../../../app/migrations/server';
import { Settings } from '../../../app/models/server';
Migrations.add({
version: 150,
up() {
const settings = [
'Graphql_CORS',
'Graphql_Enabled',
'Graphql_Subscription_Port',
];
Settings.remove({ _id: { $in: settings } });
},
down() {
// Down migration does not apply in this case
},
});

@ -1,343 +0,0 @@
import supertest from 'supertest';
import { adminUsername, adminPassword, adminEmail } from '../../data/user.js';
const request = supertest('http://localhost:3000');
const user = { username: adminUsername, password: adminPassword, email: adminEmail, accessToken: null };
const channel = {};
const message = { content: 'Test Message GraphQL', modifiedContent: 'Test Message GraphQL Modified' };
const { expect } = require('chai');
const credentials = {
'X-Auth-Token': undefined,
'X-User-Id': undefined,
};
const login = {
user: adminUsername,
password: adminPassword,
};
describe('GraphQL Tests', function() {
this.retries(0);
before((done) => {
request.post('/api/v1/login')
.send(login)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
credentials['X-Auth-Token'] = res.body.data.authToken;
credentials['X-User-Id'] = res.body.data.userId;
})
.end(done);
});
before((done) => {
request.get('/api/graphql')
.expect(400)
.end(done);
});
before((done) => {
request.post('/api/v1/settings/Graphql_Enabled')
.set(credentials)
.send({ value: true })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
})
.end(done);
});
after((done) => {
request.post('/api/v1/settings/Graphql_Enabled')
.set(credentials)
.send({ value: false })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
})
.end(done);
});
it('Is able to login with username and password', (done) => {
browser.pause(500);
const query = `
mutation login {
loginWithPassword(user: "${ user.username }", password: "${ user.password }") {
user {
username,
email
},
tokens {
accessToken
}
}
}`;
request.post('/api/graphql')
.send({
query,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('data');
expect(res.body).to.not.have.property('errors');
const data = res.body.data.loginWithPassword;
expect(data).to.have.property('user');
expect(data).to.have.property('tokens');
user.accessToken = data.tokens.accessToken;
expect(data.user).to.have.property('username', user.username);
expect(data.user).to.have.property('email', user.email);
})
.end(done);
});
it('Is able to login with email and password', (done) => {
const query = `
mutation login {
loginWithPassword(user: "", userFields: {email: "${ user.email }"}, password: "${ user.password }") {
user {
username,
email,
id
},
tokens {
accessToken
}
}
}`;
request.post('/api/graphql')
.send({
query,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('data');
expect(res.body).to.not.have.property('errors');
const data = res.body.data.loginWithPassword;
expect(data).to.have.property('user');
expect(data).to.have.property('tokens');
user.accessToken = data.tokens.accessToken;
expect(data.user).to.have.property('username', user.username);
expect(data.user).to.have.property('email', user.email);
})
.end(done);
});
it('Fails when trying to login with wrong password', (done) => {
const query = `
mutation login {
loginWithPassword(user: "${ user.username }", password: "not!${ user.password }") {
user {
username
},
tokens {
accessToken
}
}
}`;
request.post('/api/graphql')
.send({
query,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('data');
expect(res.body).to.have.property('errors');
expect(res.body.data).to.have.property('loginWithPassword', null);
expect(res.body.errors[0]).to.have.property('message', 'Incorrect password');
})
.end(done);
});
it('Is able to get user data (/me)', (done) => {
const query = `
{
me {
username,
name,
email,
channels {
id,
name
},
directMessages {
id,
name
}
}
}`;
request.post('/api/graphql')
.set('Authorization', user.accessToken)
.send({
query,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('data');
expect(res.body).to.not.have.property('errors');
const { me } = res.body.data;
expect(me).to.have.property('username', user.username);
expect(me).to.have.property('email', user.email);
expect(me.channels).to.be.an('array');
expect(me.channels[0]).to.have.property('id');
channel.id = me.channels[0].id;
})
.end(done);
});
it('Is able to send messages to channel', (done) => {
const query = `
mutation sendMessage{
sendMessage(channelId: "${ channel.id }", content: "${ message.content }") {
id,
author {
username,
name
},
content,
channel {
name,
id
},
reactions {
username,
icon
}
}
}`;
request.post('/api/graphql')
.set('Authorization', user.accessToken)
.send({
query,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('data');
expect(res.body).to.not.have.property('errors');
const data = res.body.data.sendMessage;
expect(data).to.have.property('id');
message.id = data.id;
expect(data).to.have.property('author');
expect(data.author).to.have.property('username', user.username);
expect(data).to.have.property('content', message.content);
expect(data).to.have.property('channel');
expect(data.channel).to.have.property('id', channel.id);
expect(data).to.have.property('reactions', null);
})
.end(done);
});
it('Is able to edit messages', (done) => {
const query = `
mutation editMessage {
editMessage(id: {messageId: "${ message.id }", channelId: "${ channel.id }"}, content: "${ message.modifiedContent }") {
id,
content,
author {
username
},
channel {
id,
name
}
}
}`;
request.post('/api/graphql')
.set('Authorization', user.accessToken)
.send({
query,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('data');
expect(res.body).to.not.have.property('errors');
const data = res.body.data.editMessage;
expect(data).to.have.property('id');
expect(data).to.have.property('author');
expect(data.author).to.have.property('username', user.username);
expect(data).to.have.property('content', message.modifiedContent);
expect(data).to.have.property('channel');
expect(data.channel).to.have.property('id', channel.id);
})
.end(done);
});
it('Can read messages from channel', (done) => {
const query = `
{
messages (channelId: "${ channel.id }") {
channel {
id,
name
},
messagesArray {
id,
author {
username
},
content
}
}
}`;
request.post('/api/graphql')
.set('Authorization', user.accessToken)
.send({
query,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('data');
expect(res.body).to.not.have.property('errors');
const data = res.body.data.messages;
expect(data).to.have.property('channel');
expect(data.channel).to.have.property('id', channel.id);
expect(data).to.have.property('messagesArray');
expect(data.messagesArray[0]).to.have.property('id', message.id);
expect(data.messagesArray[0]).to.have.property('author');
expect(data.messagesArray[0].author).to.have.property('username', user.username);
expect(data.messagesArray[0]).to.have.property('content', message.modifiedContent);
})
.end(done);
});
it('Is able to delete messages', (done) => {
const query = `
mutation deleteMessage {
deleteMessage(id: {messageId: "${ message.id }", channelId: "${ channel.id }"}) {
id,
author {
username
}
}
}`;
request.post('/api/graphql')
.set('Authorization', user.accessToken)
.send({
query,
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('data');
expect(res.body).to.not.have.property('errors');
const data = res.body.data.deleteMessage;
expect(data).to.have.property('id', message.id);
expect(data).to.have.property('author');
expect(data.author).to.have.property('username', user.username);
})
.end(done);
});
});

@ -117,19 +117,19 @@ class Administration extends Page {
// settings
get buttonSave() { return browser.element('button.save'); }
get generalButtonExpandIframe() { return browser.element('.section:nth-of-type(4) .expand'); }
get generalButtonExpandIframe() { return browser.element('.section:nth-of-type(3) .expand'); }
get generalButtonExpandNotifications() { return browser.element('.section:nth-of-type(5) .expand'); }
get generalButtonExpandNotifications() { return browser.element('.section:nth-of-type(4) .expand'); }
get generalButtonExpandRest() { return browser.element('.section:nth-of-type(6) .expand'); }
get generalButtonExpandRest() { return browser.element('.section:nth-of-type(5) .expand'); }
get generalButtonExpandReporting() { return browser.element('.section:nth-of-type(7) .expand'); }
get generalButtonExpandReporting() { return browser.element('.section:nth-of-type(6) .expand'); }
get generalButtonExpandStreamCast() { return browser.element('.section:nth-of-type(8) .expand'); }
get generalButtonExpandStreamCast() { return browser.element('.section:nth-of-type(7) .expand'); }
get generalButtonExpandTranslations() { return browser.element('.section:nth-of-type(9) .expand'); }
get generalButtonExpandTranslations() { return browser.element('.section:nth-of-type(8) .expand'); }
get generalButtonExpandUTF8() { return browser.element('.section:nth-of-type(10) .expand'); }
get generalButtonExpandUTF8() { return browser.element('.section:nth-of-type(9) .expand'); }
get generalSiteUrl() { return browser.element('[name="Site_Url"]'); }

Loading…
Cancel
Save