mirror of
https://github.com/wassname/talk.git
synced 2026-07-05 07:05:45 +08:00
Merge branch 'master' into docs-update
This commit is contained in:
+2
-2
@@ -90,7 +90,7 @@ async function onListening() {
|
||||
let bind = typeof addr === 'string'
|
||||
? `pipe ${addr}`
|
||||
: `port ${addr.port}`;
|
||||
console.log(`API Server Listening on ${bind}`);
|
||||
debug(`API Server Listening on ${bind}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,7 +149,7 @@ async function startApp(program) {
|
||||
|
||||
// Mount the websocket server if requested.
|
||||
if (program.websockets) {
|
||||
console.log(`Websocket Server Listening on ${port}`);
|
||||
debug(`Websocket Server Listening on ${port}`);
|
||||
|
||||
// Mount the subscriptions server on the application server.
|
||||
createSubscriptionManager(server);
|
||||
|
||||
@@ -14,7 +14,7 @@ require('env-rewrite').rewrite();
|
||||
const CONFIG = {
|
||||
|
||||
// WEBPACK indicates when webpack is currently building.
|
||||
WEBPACK: process.env.WEBPACK === 'true',
|
||||
WEBPACK: process.env.WEBPACK === 'TRUE',
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// JWT based configuration
|
||||
@@ -24,6 +24,17 @@ const CONFIG = {
|
||||
// application.
|
||||
JWT_SECRET: process.env.TALK_JWT_SECRET || null,
|
||||
|
||||
// JWT_SECRETS is used when key rotation is available.
|
||||
JWT_SECRETS: process.env.TALK_JWT_SECRETS || null,
|
||||
|
||||
// JWT_COOKIE_NAME is the name of the cookie optionally containing the JWT
|
||||
// token.
|
||||
JWT_COOKIE_NAME: process.env.TALK_JWT_COOKIE_NAME || 'authorization',
|
||||
|
||||
// JWT_CLEAR_COOKIE_LOGOUT specifies whether the named cookie should be
|
||||
// cleared when the user is logged out.
|
||||
JWT_CLEAR_COOKIE_LOGOUT: process.env.TALK_JWT_CLEAR_COOKIE_LOGOUT ? process.env.TALK_JWT_CLEAR_COOKIE_LOGOUT !== 'FALSE' : true,
|
||||
|
||||
// JWT_AUDIENCE is the value for the audience claim for the tokens that will be
|
||||
// verified when decoding. If `JWT_AUDIENCE` is not in the environment, then it
|
||||
// will default to `talk`.
|
||||
@@ -102,6 +113,10 @@ const CONFIG = {
|
||||
// JWT based configuration
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
if (CONFIG.JWT_SECRETS) {
|
||||
CONFIG.JWT_SECRETS = JSON.parse(CONFIG.JWT_SECRETS);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'test' && !CONFIG.JWT_SECRET) {
|
||||
CONFIG.JWT_SECRET = 'keyboard cat';
|
||||
} else if (!CONFIG.JWT_SECRET) {
|
||||
|
||||
+4
-4
@@ -6,11 +6,11 @@
|
||||
"scripts": {
|
||||
"postinstall": "./bin/cli plugins reconcile --skip-remote",
|
||||
"start": "./bin/cli serve -j -w",
|
||||
"dev-start": "nodemon -w . -w bin/cli -w bin/cli-serve --config .nodemon.json --exec \"yarn generate-introspection && ./bin/cli -c .env serve -j -w\"",
|
||||
"dev-start": "nodemon -w . -w bin/cli -w bin/cli-serve --config .nodemon.json --exec \"WEBPACK=TRUE NODE_ENV=test ./scripts/generateIntrospectionResult.js && ./bin/cli -c .env serve -j -w\"",
|
||||
"prebuild": "yarn generate-introspection",
|
||||
"build": "WEBPACK=true NODE_ENV=production webpack -p --config webpack.config.js --bail",
|
||||
"build": "WEBPACK=TRUE NODE_ENV=production webpack -p --config webpack.config.js --bail",
|
||||
"prebuild-watch": "yarn generate-introspection",
|
||||
"build-watch": "WEBPACK=true NODE_ENV=development webpack --progress --config webpack.config.js --watch",
|
||||
"build-watch": "WEBPACK=TRUE NODE_ENV=development webpack --progress --config webpack.config.js --watch",
|
||||
"lint": "eslint bin/* .",
|
||||
"lint-fix": "eslint bin/* . --fix",
|
||||
"test": "TEST_MODE=unit NODE_ENV=test mocha -R ${MOCHA_REPORTER:-spec}",
|
||||
@@ -20,7 +20,7 @@
|
||||
"poste2e": "NODE_ENV=test scripts/poste2e.sh",
|
||||
"embed-start": "NODE_ENV=development yarn build && ./bin/cli serve --jobs",
|
||||
"heroku-postbuild": "./bin/cli plugins reconcile && yarn build",
|
||||
"generate-introspection": "WEBPACK=true NODE_ENV=test ./scripts/generateIntrospectionResult.js"
|
||||
"generate-introspection": "WEBPACK=TRUE NODE_ENV=test ./scripts/generateIntrospectionResult.js"
|
||||
},
|
||||
"talk": {
|
||||
"migration": {
|
||||
|
||||
@@ -54,6 +54,13 @@ export default (reaction) => (WrappedComponent) => {
|
||||
id: fragmentId
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
if (self) {
|
||||
throw new Error(`Comment ${action.item_id} was not found`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Add our comment from the mutation to the end.
|
||||
let idx = data.action_summaries.findIndex(isReaction);
|
||||
|
||||
@@ -96,6 +103,13 @@ export default (reaction) => (WrappedComponent) => {
|
||||
id: fragmentId
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
if (self) {
|
||||
throw new Error(`Comment ${action.item_id} was not found`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check whether we liked this comment.
|
||||
const idx = data.action_summaries.findIndex(isReaction);
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: #3D73D5;
|
||||
background: #D2D7D3;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
color: #4A4A4A;
|
||||
display: inline-block;
|
||||
margin: 0px 5px;
|
||||
padding: 5px 5px;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {VIEWING_OPTIONS_OPEN, VIEWING_OPTIONS_CLOSE} from './constants';
|
||||
import {OPEN_MENU, CLOSE_MENU} from './constants';
|
||||
|
||||
export const openViewingOptions = () => ({
|
||||
type: VIEWING_OPTIONS_OPEN
|
||||
export const openMenu = () => ({
|
||||
type: OPEN_MENU,
|
||||
});
|
||||
|
||||
export const closeViewingOptions = () => ({
|
||||
type: VIEWING_OPTIONS_CLOSE
|
||||
export const closeMenu = () => ({
|
||||
type: CLOSE_MENU,
|
||||
});
|
||||
|
||||
@@ -8,15 +8,15 @@ import {Slot, ClickOutside} from 'plugin-api/beta/client/components';
|
||||
const ViewingOptions = (props) => {
|
||||
const toggleOpen = () => {
|
||||
if (!props.open) {
|
||||
props.openViewingOptions();
|
||||
props.openMenu();
|
||||
} else {
|
||||
props.closeViewingOptions();
|
||||
props.closeMenu();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickOutside = () => {
|
||||
if (props.open) {
|
||||
props.closeViewingOptions();
|
||||
props.closeMenu();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
export const VIEWING_OPTIONS_OPEN = 'VIEWING_OPTIONS_OPEN';
|
||||
export const VIEWING_OPTIONS_CLOSE = 'VIEWING_OPTIONS_CLOSE';
|
||||
const prefix = 'TALK_VIEWING_OPTIONS';
|
||||
|
||||
export const OPEN_MENU = `${prefix}_OPEN_MENU`;
|
||||
export const CLOSE_MENU = `${prefix}_CLOSE_MENU`;
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {connect} from 'plugin-api/beta/client/hocs';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import ViewingOptions from '../components/ViewingOptions';
|
||||
import {openViewingOptions, closeViewingOptions} from '../actions';
|
||||
import {openMenu, closeMenu} from '../actions';
|
||||
|
||||
const mapStateToProps = ({coralPluginViewingOptions: state}) => ({
|
||||
open: state.open
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({openViewingOptions, closeViewingOptions}, dispatch);
|
||||
bindActionCreators({openMenu, closeMenu}, dispatch);
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ViewingOptions);
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import {VIEWING_OPTIONS_OPEN, VIEWING_OPTIONS_CLOSE} from './constants';
|
||||
import {OPEN_MENU, CLOSE_MENU} from './constants';
|
||||
|
||||
const initialState = {
|
||||
open: false
|
||||
};
|
||||
|
||||
export default function offTopic (state = initialState, action) {
|
||||
export default function reducer(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case VIEWING_OPTIONS_OPEN:
|
||||
case OPEN_MENU:
|
||||
return {
|
||||
...state,
|
||||
open: true
|
||||
};
|
||||
case VIEWING_OPTIONS_CLOSE:
|
||||
case CLOSE_MENU:
|
||||
return {
|
||||
...state,
|
||||
open: false
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export const SHOW_TOOLTIP = 'SHOW_TOOLTIP';
|
||||
export const HIDE_TOOLTIP = 'HIDE_TOOLTIP';
|
||||
const prefix = 'TALK_FEATURED_COMMENTS';
|
||||
|
||||
export const SHOW_TOOLTIP = `${prefix}_SHOW_TOOLTIP`;
|
||||
export const HIDE_TOOLTIP = `${prefix}_HIDE_TOOLTIP`;
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
const {
|
||||
JWT_SECRETS,
|
||||
JWT_SECRET,
|
||||
JWT_ALG
|
||||
} = require('./config');
|
||||
|
||||
const debug = require('debug')('talk:secrets');
|
||||
const jwt = require('./services/jwt');
|
||||
|
||||
if (JWT_SECRETS) {
|
||||
if (!Array.isArray(JWT_SECRETS)) {
|
||||
throw new Error('TALK_JWT_SECRETS must be a JSON array in the form [{"kid": kid, ["secret": secret | "private": private, "public": public]}, ...]');
|
||||
}
|
||||
|
||||
if (JWT_SECRETS.length === 0) {
|
||||
throw new Error('TALK_JWT_SECRETS must be a JSON array with non zero length');
|
||||
}
|
||||
|
||||
// Wrap a multi-secret around the available secrets.
|
||||
module.exports.jwt = new jwt.MultiSecret(JWT_SECRETS.map((secret) => {
|
||||
if (!('kid' in secret)) {
|
||||
throw new Error('when multiple keys are specified, kid\'s must be specified');
|
||||
}
|
||||
|
||||
// HMAC secrets do not have public/private keys.
|
||||
if (JWT_ALG.startsWith('HS')) {
|
||||
return new jwt.SharedSecret(secret, JWT_ALG);
|
||||
}
|
||||
|
||||
if (!('public' in secret)) {
|
||||
throw new Error('all symetric keys must provide a PEM encoded public key');
|
||||
}
|
||||
|
||||
return new jwt.AsymmetricSecret(secret, JWT_ALG);
|
||||
}));
|
||||
|
||||
debug(`loaded ${JWT_SECRET.length} secrets`);
|
||||
} else if (JWT_SECRET) {
|
||||
if (JWT_ALG.startsWith('HS')) {
|
||||
module.exports.jwt = new jwt.SharedSecret({
|
||||
secret: JWT_SECRET
|
||||
}, JWT_ALG);
|
||||
} else {
|
||||
module.exports.jwt = new jwt.AsymmetricSecret(JSON.parse(JWT_SECRET), JWT_ALG);
|
||||
}
|
||||
|
||||
debug('loaded 1 secret');
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
/**
|
||||
* MultiSecret will take many secrets and provide a unified interface for
|
||||
* handling verifying and signing.
|
||||
*/
|
||||
class MultiSecret {
|
||||
constructor(secrets) {
|
||||
this.secrets = secrets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign will sign with the first secret.
|
||||
*/
|
||||
sign(payload, options) {
|
||||
return this.secrets[0].sign(payload, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify will parse the token and determine the kid, then match it to the
|
||||
* available secrets, using that to perform the verification.
|
||||
*/
|
||||
verify(token, options, callback) {
|
||||
let header = null;
|
||||
try {
|
||||
header = JSON.parse(Buffer(token.split('.')[0], 'base64').toString());
|
||||
} catch(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!('kid' in header)) {
|
||||
return callback(new Error('expected kid to exist in the token header, it did not.'));
|
||||
}
|
||||
|
||||
let kid = header.kid;
|
||||
let verifier = this.secrets.find((secret) => secret.kid === kid);
|
||||
if (!verifier) {
|
||||
return callback(new Error(`expected kid ${kid} was not available.`));
|
||||
}
|
||||
|
||||
return verifier.verify(token, options, callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Secret wraps the capabilities expected of a Secret, signing and verifying.
|
||||
*/
|
||||
class Secret {
|
||||
constructor({kid, signingKey, verifiyingKey, algorithm}) {
|
||||
this.kid = kid;
|
||||
this.signingKey = signingKey;
|
||||
this.verifiyingKey = verifiyingKey;
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign will sign the payload with the secret.
|
||||
*
|
||||
* @param {Object} payload the object to sign
|
||||
* @param {Object} options the signing options
|
||||
*/
|
||||
sign(payload, options) {
|
||||
if (!this.signingKey) {
|
||||
throw new Error('no signing key on secret, cannot sign');
|
||||
}
|
||||
|
||||
return jwt.sign(payload, this.signingKey, Object.assign({}, options, {
|
||||
keyid: this.kid,
|
||||
algorithm: this.algorithm
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify will ensure that the given token was indeed signed with this secret.
|
||||
* @param {String} token the token to verify
|
||||
* @param {Object} options the verification options
|
||||
* @param {Function} callback the function to call with the verification results
|
||||
*/
|
||||
verify(token, options, callback) {
|
||||
jwt.verify(token, this.verifiyingKey, Object.assign({}, options, {
|
||||
algorithms: [this.algorithm]
|
||||
}), callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SharedSecret is the HMAC based secret that's used for signing/verifying.
|
||||
*/
|
||||
function SharedSecret({kid = undefined, secret}, algorithm) {
|
||||
return new Secret({
|
||||
kid,
|
||||
signingKey: secret,
|
||||
verifiyingKey: secret,
|
||||
algorithm
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* AsymmetricSecret is the Asymmetric based key, where a private key is optional
|
||||
* and the public key is required.
|
||||
*/
|
||||
function AsymmetricSecret({kid = undefined, private: privateKey, public: publicKey}, algorithm) {
|
||||
publicKey = Buffer.from(publicKey.replace(/\\n/g, '\n'));
|
||||
privateKey = privateKey ? Buffer.from(privateKey.replace(/\\n/g, '\n')) : null;
|
||||
|
||||
return new Secret({
|
||||
kid,
|
||||
signingKey: privateKey,
|
||||
verifiyingKey: publicKey,
|
||||
algorithm
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
AsymmetricSecret,
|
||||
SharedSecret,
|
||||
MultiSecret
|
||||
};
|
||||
@@ -43,7 +43,7 @@ if (enabled('talk:db')) {
|
||||
|
||||
if (WEBPACK) {
|
||||
|
||||
console.warn('Not connecting to mongodb during webpack build');
|
||||
debug('Not connecting to mongodb during webpack build');
|
||||
|
||||
// @wyattjoh: We didn't call connect, but because we include mongoose, it will hold the socket ready,
|
||||
// preventing node from exiting. Calling disconnect here just ensures that the application
|
||||
|
||||
+25
-15
@@ -4,7 +4,6 @@ const SettingsService = require('./settings');
|
||||
const TokensService = require('./tokens');
|
||||
const fetch = require('node-fetch');
|
||||
const FormData = require('form-data');
|
||||
const JWT = require('jsonwebtoken');
|
||||
const LocalStrategy = require('passport-local').Strategy;
|
||||
const errors = require('../errors');
|
||||
const uuid = require('uuid');
|
||||
@@ -17,30 +16,38 @@ const {createClientFactory} = require('./redis');
|
||||
const client = createClientFactory();
|
||||
|
||||
const {
|
||||
JWT_SECRET,
|
||||
JWT_ISSUER,
|
||||
JWT_EXPIRY,
|
||||
JWT_AUDIENCE,
|
||||
JWT_ALG,
|
||||
RECAPTCHA_SECRET,
|
||||
RECAPTCHA_ENABLED
|
||||
RECAPTCHA_ENABLED,
|
||||
JWT_COOKIE_NAME,
|
||||
JWT_CLEAR_COOKIE_LOGOUT
|
||||
} = require('../config');
|
||||
|
||||
const {
|
||||
jwt: JWT_SECRET
|
||||
} = require('../secrets');
|
||||
|
||||
// GenerateToken will sign a token to include all the authorization information
|
||||
// needed for the front end.
|
||||
const GenerateToken = (user) => JWT.sign({}, JWT_SECRET, {
|
||||
jwtid: uuid.v4(),
|
||||
expiresIn: JWT_EXPIRY,
|
||||
issuer: JWT_ISSUER,
|
||||
subject: user.id,
|
||||
audience: JWT_AUDIENCE
|
||||
});
|
||||
const GenerateToken = (user) => {
|
||||
return JWT_SECRET.sign({}, {
|
||||
jwtid: uuid.v4(),
|
||||
expiresIn: JWT_EXPIRY,
|
||||
issuer: JWT_ISSUER,
|
||||
subject: user.id,
|
||||
audience: JWT_AUDIENCE,
|
||||
algorithm: JWT_ALG
|
||||
});
|
||||
};
|
||||
|
||||
// SetTokenForSafari sends the token in a cookie for Safari clients.
|
||||
const SetTokenForSafari = (req, res, token) => {
|
||||
const browser = bowser._detect(req.headers['user-agent']);
|
||||
if (browser.ios || browser.safari) {
|
||||
res.cookie('authorization', token, {
|
||||
res.cookie(JWT_COOKIE_NAME, token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
expires: new Date(Date.now() + ms(JWT_EXPIRY))
|
||||
@@ -154,7 +161,11 @@ const HandleLogout = (req, res, next) => {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.clearCookie('authorization');
|
||||
// Only clear the cookie on logout if enabled.
|
||||
if (JWT_CLEAR_COOKIE_LOGOUT) {
|
||||
res.clearCookie(JWT_COOKIE_NAME);
|
||||
}
|
||||
|
||||
res.status(204).end();
|
||||
});
|
||||
};
|
||||
@@ -187,7 +198,6 @@ const CheckBlacklisted = async (jwt) => {
|
||||
return checkGeneralTokenBlacklist(jwt);
|
||||
};
|
||||
|
||||
const jwt = require('jsonwebtoken');
|
||||
const JwtStrategy = require('passport-jwt').Strategy;
|
||||
const ExtractJwt = require('passport-jwt').ExtractJwt;
|
||||
|
||||
@@ -195,7 +205,7 @@ let cookieExtractor = function(req) {
|
||||
let token = null;
|
||||
|
||||
if (req && req.cookies) {
|
||||
token = req.cookies['authorization'];
|
||||
token = req.cookies[JWT_COOKIE_NAME];
|
||||
}
|
||||
|
||||
return token;
|
||||
@@ -204,7 +214,7 @@ let cookieExtractor = function(req) {
|
||||
// Override the JwtVerifier method on the JwtStrategy so we can pack the
|
||||
// original token into the payload.
|
||||
JwtStrategy.JwtVerifier = (token, secretOrKey, options, callback) => {
|
||||
return jwt.verify(token, secretOrKey, options, (err, jwt) => {
|
||||
return JWT_SECRET.verify(token, options, (err, jwt) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
+5
-3
@@ -1,14 +1,16 @@
|
||||
const errors = require('../errors');
|
||||
const UserModel = require('../models/user');
|
||||
const JWT = require('jsonwebtoken');
|
||||
const uuid = require('uuid');
|
||||
|
||||
const {
|
||||
JWT_SECRET,
|
||||
JWT_ISSUER,
|
||||
JWT_AUDIENCE
|
||||
} = require('../config');
|
||||
|
||||
const {
|
||||
jwt: JWT_SECRET
|
||||
} = require('../secrets');
|
||||
|
||||
/**
|
||||
* TokenService manages Personal Access Tokens for users. These tokens are
|
||||
* persisted in the database and attached to the user.
|
||||
@@ -33,7 +35,7 @@ module.exports = class TokenService {
|
||||
};
|
||||
|
||||
// Sign the payload.
|
||||
const jwt = JWT.sign(payload, JWT_SECRET, {});
|
||||
const jwt = JWT_SECRET.sign(payload, {});
|
||||
|
||||
// Create the PAT.
|
||||
let pat = {
|
||||
|
||||
+66
-82
@@ -2,13 +2,17 @@ const assert = require('assert');
|
||||
const uuid = require('uuid');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const url = require('url');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const Wordlist = require('./wordlist');
|
||||
const errors = require('../errors');
|
||||
|
||||
const {
|
||||
JWT_SECRET,
|
||||
ROOT_URL
|
||||
} = require('../config');
|
||||
|
||||
const {
|
||||
jwt: JWT_SECRET
|
||||
} = require('../secrets');
|
||||
|
||||
const debug = require('debug')('talk:services:users');
|
||||
|
||||
const UserModel = require('../models/user');
|
||||
@@ -354,7 +358,7 @@ module.exports = class UsersService {
|
||||
*/
|
||||
static disableUser(id) {
|
||||
return UserModel.update({
|
||||
id: id
|
||||
id
|
||||
}, {
|
||||
$set: {
|
||||
disabled: true
|
||||
@@ -369,7 +373,7 @@ module.exports = class UsersService {
|
||||
*/
|
||||
static enableUser(id) {
|
||||
return UserModel.update({
|
||||
id: id
|
||||
id
|
||||
}, {
|
||||
$set: {
|
||||
disabled: false
|
||||
@@ -413,9 +417,7 @@ module.exports = class UsersService {
|
||||
return Promise.reject(new Error(`role ${role} is not supported`));
|
||||
}
|
||||
|
||||
return UserModel.update({
|
||||
id: id
|
||||
}, {
|
||||
return UserModel.update({id}, {
|
||||
$pull: {
|
||||
roles: role
|
||||
}
|
||||
@@ -461,16 +463,15 @@ module.exports = class UsersService {
|
||||
* @param {Date} until date until the suspension is valid.
|
||||
*/
|
||||
static async suspendUser(id, message, until) {
|
||||
const user = await UserModel.findOneAndUpdate(
|
||||
{id}, {
|
||||
$set: {
|
||||
suspension: {
|
||||
until,
|
||||
},
|
||||
}
|
||||
}, {
|
||||
new: true,
|
||||
});
|
||||
const user = await UserModel.findOneAndUpdate({id}, {
|
||||
$set: {
|
||||
suspension: {
|
||||
until,
|
||||
},
|
||||
}
|
||||
}, {
|
||||
new: true,
|
||||
});
|
||||
|
||||
if (message) {
|
||||
let localProfile = user.profiles.find((profile) => profile.provider === 'local');
|
||||
@@ -500,9 +501,7 @@ module.exports = class UsersService {
|
||||
* @param {Date} until date until the suspension is valid.
|
||||
*/
|
||||
static async rejectUsername(id, message) {
|
||||
const user = await UserModel.findOneAndUpdate({
|
||||
id
|
||||
}, {
|
||||
const user = await UserModel.findOneAndUpdate({id}, {
|
||||
$set: {
|
||||
status: 'BANNED',
|
||||
canEditName: true,
|
||||
@@ -512,18 +511,17 @@ module.exports = class UsersService {
|
||||
});
|
||||
|
||||
if (message) {
|
||||
let localProfile = user.profiles.find((profile) => profile.provider === 'local');
|
||||
let localProfile = user.profiles.find(({provider}) => provider === 'local');
|
||||
if (localProfile) {
|
||||
const options =
|
||||
{
|
||||
template: 'suspension', // needed to know which template to render!
|
||||
locals: { // specifies the template locals.
|
||||
body: message
|
||||
},
|
||||
subject: 'Email Suspension',
|
||||
to: localProfile.id // This only works if the user has registered via e-mail.
|
||||
// We may want a standard way to access a user's e-mail address in the future
|
||||
};
|
||||
const options = {
|
||||
template: 'suspension', // needed to know which template to render!
|
||||
locals: { // specifies the template locals.
|
||||
body: message
|
||||
},
|
||||
subject: 'Email Suspension',
|
||||
to: localProfile.id // This only works if the user has registered via e-mail.
|
||||
// We may want a standard way to access a user's e-mail address in the future
|
||||
};
|
||||
|
||||
await MailerService.sendSimple(options);
|
||||
}
|
||||
@@ -548,9 +546,7 @@ module.exports = class UsersService {
|
||||
static async findOrCreateByIDToken(id, token) {
|
||||
|
||||
// Try to get the user.
|
||||
let user = await UserModel.findOne({
|
||||
id
|
||||
});
|
||||
let user = await UserModel.findOne({id});
|
||||
|
||||
// If the user was not found, try to look it up.
|
||||
if (user === null) {
|
||||
@@ -629,8 +625,7 @@ module.exports = class UsersService {
|
||||
version: user.__v
|
||||
};
|
||||
|
||||
return jwt.sign(payload, JWT_SECRET, {
|
||||
algorithm: 'HS256',
|
||||
return JWT_SECRET.sign(payload, {
|
||||
expiresIn: '1d',
|
||||
subject: PASSWORD_RESET_JWT_SUBJECT
|
||||
});
|
||||
@@ -644,11 +639,7 @@ module.exports = class UsersService {
|
||||
*/
|
||||
static verifyToken(token, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
// Set the allowed algorithms.
|
||||
options.algorithms = ['HS256'];
|
||||
|
||||
jwt.verify(token, JWT_SECRET, options, (err, decoded) => {
|
||||
JWT_SECRET.verify(token, options, (err, decoded) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
@@ -762,7 +753,7 @@ module.exports = class UsersService {
|
||||
* @param {String} email The email that we are needing to get confirmed.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static createEmailConfirmToken(userID = null, email, referer = ROOT_URL) {
|
||||
static async createEmailConfirmToken(userID = null, email, referer = ROOT_URL) {
|
||||
if (!email || typeof email !== 'string') {
|
||||
return Promise.reject('email is required when creating a JWT for resetting passord');
|
||||
}
|
||||
@@ -772,41 +763,37 @@ module.exports = class UsersService {
|
||||
|
||||
const tokenOptions = {
|
||||
jwtid: uuid.v4(),
|
||||
algorithm: 'HS256',
|
||||
expiresIn: '1d',
|
||||
subject: EMAIL_CONFIRM_JWT_SUBJECT
|
||||
};
|
||||
|
||||
let userPromise;
|
||||
|
||||
let user;
|
||||
if (!userID) {
|
||||
|
||||
// If there is no userID, we're coming from the endpoint where a new user
|
||||
// is re-requesting a confirmation email and we don't know the userID.
|
||||
userPromise = UserModel.findOne({profiles: {$elemMatch: {id: email, provider: 'local'}}});
|
||||
user = await UserModel.findOne({profiles: {$elemMatch: {id: email, provider: 'local'}}});
|
||||
} else {
|
||||
userPromise = UsersService.findById(userID);
|
||||
user = await UsersService.findById(userID);
|
||||
}
|
||||
|
||||
return userPromise.then((user) => {
|
||||
if (!user) {
|
||||
return Promise.reject(errors.ErrNotFound);
|
||||
}
|
||||
if (!user) {
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
// Get the profile representing the local account.
|
||||
let profile = user.profiles.find((profile) => profile.id === email && profile.provider === 'local');
|
||||
// Get the profile representing the local account.
|
||||
let profile = user.profiles.find((profile) => profile.id === email && profile.provider === 'local');
|
||||
|
||||
// Ensure that the user email hasn't already been verified.
|
||||
if (profile && profile.metadata && profile.metadata.confirmed_at) {
|
||||
return Promise.reject(new Error('email address already confirmed'));
|
||||
}
|
||||
// Ensure that the user email hasn't already been verified.
|
||||
if (profile && profile.metadata && profile.metadata.confirmed_at) {
|
||||
throw new Error('email address already confirmed');
|
||||
}
|
||||
|
||||
return jwt.sign({
|
||||
email,
|
||||
referer,
|
||||
userID: user.id
|
||||
}, JWT_SECRET, tokenOptions);
|
||||
});
|
||||
return JWT_SECRET.sign({
|
||||
email,
|
||||
referer,
|
||||
userID: user.id
|
||||
}, tokenOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -816,17 +803,14 @@ module.exports = class UsersService {
|
||||
* signed with our secret.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static verifyEmailConfirmation(token) {
|
||||
return UsersService
|
||||
.verifyToken(token, {
|
||||
subject: EMAIL_CONFIRM_JWT_SUBJECT
|
||||
})
|
||||
.then(({userID, email, referer}) => {
|
||||
return UsersService
|
||||
.confirmEmail(userID, email)
|
||||
.then(() => ({userID, email, referer}));
|
||||
});
|
||||
static async verifyEmailConfirmation(token) {
|
||||
let {userID, email, referer} = await UsersService.verifyToken(token, {
|
||||
subject: EMAIL_CONFIRM_JWT_SUBJECT
|
||||
});
|
||||
|
||||
await UsersService.confirmEmail(userID, email);
|
||||
|
||||
return {userID, email, referer};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -835,7 +819,7 @@ module.exports = class UsersService {
|
||||
static confirmEmail(id, email) {
|
||||
return UserModel
|
||||
.update({
|
||||
id: id,
|
||||
id,
|
||||
profiles: {
|
||||
$elemMatch: {
|
||||
id: email,
|
||||
@@ -934,18 +918,18 @@ module.exports = class UsersService {
|
||||
|
||||
/**
|
||||
* Ignore another user
|
||||
* @param {String} userId the id of the user that is ignoring another users
|
||||
* @param {String[]} usersToIgnore Array of user IDs to ignore
|
||||
* @param {String} id the id of the user that is ignoring another users
|
||||
* @param {Array<String>} usersToIgnore Array of user IDs to ignore
|
||||
*/
|
||||
static ignoreUsers(userId, usersToIgnore) {
|
||||
static ignoreUsers(id, usersToIgnore) {
|
||||
assert(Array.isArray(usersToIgnore), 'usersToIgnore is an array');
|
||||
assert(usersToIgnore.every((u) => typeof u === 'string'), 'usersToIgnore is an array of string user IDs');
|
||||
if (usersToIgnore.includes(userId)) {
|
||||
if (usersToIgnore.includes(id)) {
|
||||
throw new Error('Users cannot ignore themselves');
|
||||
}
|
||||
|
||||
// TODO: For each usersToIgnore, make sure they exist?
|
||||
return UserModel.update({id: userId}, {
|
||||
return UserModel.update({id}, {
|
||||
$addToSet: {
|
||||
ignoresUsers: {
|
||||
$each: usersToIgnore
|
||||
@@ -957,12 +941,12 @@ module.exports = class UsersService {
|
||||
/**
|
||||
* Stop ignoring other users
|
||||
* @param {String} userId the id of the user that is ignoring another users
|
||||
* @param {String[]} usersToStopIgnoring Array of user IDs to stop ignoring
|
||||
* @param {Array<String>} usersToStopIgnoring Array of user IDs to stop ignoring
|
||||
*/
|
||||
static async stopIgnoringUsers(userId, usersToStopIgnoring) {
|
||||
static async stopIgnoringUsers(id, usersToStopIgnoring) {
|
||||
assert(Array.isArray(usersToStopIgnoring), 'usersToStopIgnoring is an array');
|
||||
assert(usersToStopIgnoring.every((u) => typeof u === 'string'), 'usersToStopIgnoring is an array of string user IDs');
|
||||
await UserModel.update({id: userId}, {
|
||||
await UserModel.update({id}, {
|
||||
$pullAll: {
|
||||
ignoresUsers: usersToStopIgnoring
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ describe('graph.mutations.addTag', () => {
|
||||
console.error(res.errors);
|
||||
}
|
||||
|
||||
console.log('res.errors', res.errors);
|
||||
expect(res.errors).to.be.empty;
|
||||
|
||||
let {tags} = await CommentsService.findById(comment.id);
|
||||
|
||||
+2
-1
@@ -7,6 +7,7 @@ const _ = require('lodash');
|
||||
const Copy = require('copy-webpack-plugin');
|
||||
const LicenseWebpackPlugin = require('license-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
const debug = require('debug')('talk:webpack');
|
||||
|
||||
// Possibly load the config from the .env file (if there is one).
|
||||
require('dotenv').config();
|
||||
@@ -15,7 +16,7 @@ const {plugins, pluginsPath, PluginManager} = require('./plugins');
|
||||
const manager = new PluginManager(plugins);
|
||||
const targetPlugins = manager.section('targets').plugins;
|
||||
|
||||
console.log(`Using ${pluginsPath} as the plugin configuration path`);
|
||||
debug(`Using ${pluginsPath} as the plugin configuration path`);
|
||||
|
||||
const buildTargets = [
|
||||
'coral-admin',
|
||||
|
||||
Reference in New Issue
Block a user