[BREAK] Remove GraphQL and grant packages (#15192)
parent
9632cb7ec5
commit
4e1d4f504b
@ -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(); |
@ -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); |
||||
}); |
||||
}); |
Loading…
Reference in new issue