- Story: {comment.asset.title}
+ {t('common.story')}:{' '}
+ {comment.asset.title ? comment.asset.title : comment.asset.url}
{
{t('modqueue.moderate')}
@@ -109,6 +110,7 @@ class UserDetailComment extends React.Component {
- Story: {comment.asset.title}
+ {t('common.story')}:{' '}
+ {comment.asset.title ? comment.asset.title : comment.asset.url}
{!currentAsset && (
{t('modqueue.moderate')}
@@ -156,6 +157,7 @@ class Comment extends React.Component {
+ {/* TODO: translate string */}
Contains Link
diff --git a/routes/api/v1/auth.js b/routes/api/v1/auth.js
index 369f4278a..d8e0544a9 100644
--- a/routes/api/v1/auth.js
+++ b/routes/api/v1/auth.js
@@ -4,6 +4,7 @@ const {
HandleGenerateCredentials,
HandleLogout,
} = require('../../../services/passport');
+const authz = require('../../../middleware/authorization');
const router = express.Router();
/**
@@ -26,7 +27,7 @@ router.get('/', (req, res, next) => {
/**
* This blacklists the token used to authenticate.
*/
-router.delete('/', HandleLogout);
+router.delete('/', authz.needed(), HandleLogout);
//==============================================================================
// PASSPORT ROUTES
diff --git a/services/passport.js b/services/passport.js
index 59215dd54..5748c911b 100644
--- a/services/passport.js
+++ b/services/passport.js
@@ -184,24 +184,24 @@ async function ValidateUserLogin(loginProfile, user, done) {
* Revoke the token on the request.
*/
const HandleLogout = async (req, res, next) => {
- const { jwt } = req;
-
- const now = new Date();
- const expiry = (jwt.exp - now.getTime() / 1000).toFixed(0);
-
try {
+ const { jwt } = req;
+
+ const now = new Date();
+ const expiry = (jwt.exp - now.getTime() / 1000).toFixed(0);
+
await client().set(`jtir[${jwt.jti}]`, now.toISOString(), 'EX', expiry);
+
+ // Only clear the cookie on logout if enabled.
+ if (JWT_CLEAR_COOKIE_LOGOUT) {
+ debug('clearing the login cookie');
+ res.clearCookie(JWT_SIGNING_COOKIE_NAME);
+ }
+
+ res.status(204).end();
} catch (err) {
return next(err);
}
-
- // Only clear the cookie on logout if enabled.
- if (JWT_CLEAR_COOKIE_LOGOUT) {
- debug('clearing the login cookie');
- res.clearCookie(JWT_SIGNING_COOKIE_NAME);
- }
-
- res.status(204).end();
};
const checkGeneralTokenBlacklist = jwt =>
diff --git a/services/users.js b/services/users.js
index f5319122c..aa488bbbc 100644
--- a/services/users.js
+++ b/services/users.js
@@ -1,7 +1,7 @@
const uuid = require('uuid');
const bcrypt = require('bcryptjs');
const errors = require('../errors');
-const { some, merge } = require('lodash');
+const { difference, sample, some, merge, random } = require('lodash');
const { ROOT_URL } = require('../config');
const { jwt: JWT_SECRET } = require('../secrets');
const debug = require('debug')('talk:services:users');
@@ -346,6 +346,70 @@ class UsersService {
return username.replace(/ /g, '_').replace(/[^a-zA-Z_]/g, '');
}
+ /**
+ * Creates the initial username for an external account. Searches to make
+ * sure username not already used. Adds a random number if username already
+ * in use.
+ */
+ static async getInitialUsername(username) {
+ const MAX_ATTEMPTS = 10;
+ const END_NUMBER_MAX = 99999;
+ const GROUP_ATTEMPTS = 50;
+
+ // Cast the original username.
+ const castedName = UsersService.castUsername(username);
+ const lowercaseUsername = castedName.toLowerCase();
+
+ // Try to see if our first guess has been taken.
+ const existingUserWithName = await UserModel.findOne({
+ lowercaseUsername,
+ });
+ if (!existingUserWithName) {
+ return castedName;
+ }
+
+ // Our first username was taken, lets try to find a non-taken name.
+ for (let i = 0; i < MAX_ATTEMPTS; i++) {
+ // Generate `GROUP_ATTEMPTS` guesses for the username.
+ const usernameGuesses = Array.from(Array(GROUP_ATTEMPTS)).map(
+ () => `${castedName}_${random(0, END_NUMBER_MAX)}`
+ );
+
+ // Map them all to lowercase.
+ const lowercaseUsernameGuesses = usernameGuesses.map(guess =>
+ guess.toLowerCase()
+ );
+
+ // See if any of these users aren't taken already.
+ const existingUsernames = (await UserModel.find(
+ {
+ lowercaseUsername: { $in: lowercaseUsernameGuesses },
+ },
+ { lowercaseUsername: 1 }
+ )).map(({ lowercaseUsername }) => lowercaseUsername);
+ if (existingUsernames.length === lowercaseUsernameGuesses.length) {
+ // The number of found users is the same as the number of username
+ // guesses, aka, all the usernames are taken.
+ continue;
+ }
+
+ // At least one of the usernames wasn't taken! Let's filter this to only
+ // include unused usernames and grab one random entry from the list.
+ const foundLowercaseUsernameIndex = lowercaseUsernameGuesses.indexOf(
+ sample(difference(lowercaseUsernameGuesses, existingUsernames))
+ );
+
+ // Now we get the uppercase version of that string.
+ return usernameGuesses[foundLowercaseUsernameIndex];
+ }
+
+ throw new Error(
+ 'cannot find free name after ' +
+ (MAX_ATTEMPTS * GROUP_ATTEMPTS + 1) +
+ ' tries'
+ );
+ }
+
/**
* Finds a user given a social profile and if the user does not exist, creates
* them.
@@ -368,7 +432,7 @@ class UsersService {
// User does not exist and need to be created.
// Create an initial username for the user.
- let username = UsersService.castUsername(displayName);
+ let username = await UsersService.getInitialUsername(displayName);
// The user was not found, lets create them!
user = new UserModel({
diff --git a/test/server/services/users.js b/test/server/services/users.js
index 1676f517b..06ba3813e 100644
--- a/test/server/services/users.js
+++ b/test/server/services/users.js
@@ -58,6 +58,19 @@ describe('services.UsersService', () => {
});
});
+ describe('#getInitialUsername', () => {
+ it('should find the first result when there is no conflict', async () => {
+ const username = await UsersService.getInitialUsername(
+ 'TheGreatSockmonster'
+ );
+ expect(username).to.equal('TheGreatSockmonster');
+ });
+ it('should find a first result when there is a conflict', async () => {
+ const username = await UsersService.getInitialUsername('Sockmonster');
+ expect(username).to.match(/Sockmonster_[0-9]+/);
+ });
+ });
+
describe('#findPublicByIdArray()', () => {
it('should find an array of users from an array of ids', async () => {
const ids = mockUsers.map(user => user.id);