mirror of
https://github.com/sudoxnym/habitica.git
synced 2026-04-14 19:56:23 +00:00
Username announcement (#10729)
* Change update username API call The call no longer requires a password and also validates the username. * Implement API call to verify username without setting it * Improve coding style * Apply username verification to registration * Update error messages * Validate display names. * Fix API early Stat Point allocation (#10680) * Refactor hasClass check to common so it can be used in shared & server-side code * Check that user has selected class before allocating stat points * chore(event): end Ember Hatching Potions * chore(analytics): reenable navigation tracking * update bcrypt * Point achievement modal links to main site (#10709) * Animal ears after death (#10691) * Animal Ears purchasable with Gold if lost in Death * remove ears from pinned items when set is bought * standardise css and error handling for gems and coins * revert accidental new line * fix client tests * Reduce margin-bottom of checklist-item from 10px to -3px. (#10684) * chore(i18n): update locales * 4.61.1 * feat(content): Subscriber Items and Magic Potions * chore(sprites): compile * chore(i18n): update locales * 4.62.0 * Display notification for users to confirm their username * fix typo * WIP(usernames): Changes to address #10694 * WIP(usernames): Further changes for #10694 * fix(usernames): don't show spurious headings * Change verify username notification to new version * Improve feedback for invalid usernames * Allow user to set their username again to confirm it * Improve validation display for usernames * Temporarily move display name validation outside of schema * Improve rendering banner about sleeping in the inn See #10695 * Display settings in one column * Position inn banner when window is resized * Update inn banner handling * Fix banner offset on initial load * Fix minor issues. * Issue: 10660 - Fixed. Changed default to Please Enter A Value (#10718) * Issue: 10660 - Fixed. Changed default to Please Enter A Value * Issue: 10660 - Fixed/revision 2 Changed default to Enter A Value * chore(news): Bailey announcements * chore(i18n): update locales * 4.62.1 * adjust wiki link for usernameInfo string https://github.com/HabitRPG/habitica-private/issues/7#issuecomment-425405425 * raise coverage for tasks api calls (#10029) * - updates a group task - approval is required - updates a group task with checklist * add expect to test the new checklist length * - moves tasks to a specified position out of length * remove unused line * website getter tasks tests * re-add sanitizeUserChallengeTask * change config.json.example variable to be a string not a boolean * fix tests - pick the text / up/down props too * fix test - remove changes on text/up/down - revert sanitize condition - revert sanitization props * Change update username API call The call no longer requires a password and also validates the username. * feat(content): Subscriber Items and Magic Potions * Re-add register call * Fix merge issue * Fix issue with setting username * Implement new alert style * Display username confirmation status in settings * Add disclaimer to change username field * validate username in settings * Allow specific fields to be focused when opening site settings * Implement requested changes. * Fix merge issue * Fix failing tests * verify username when users register with username and password * Set ID for change username notification * Disable submit button if username is invalid * Improve username confirmation handling * refactor(settings): address remaining code comments on auth form * Revert "refactor(settings): address remaining code comments on auth form" This reverts commit 9b6609ad646b23d9e3e394c1856f149d9a2d0995. * Social user username (#10620) * Refactored private functions to library * Refactored social login code * Added username to social registration * Changed id library * Added new local auth check * Fixed export error. Fixed password check error * fix(settings): password not available on client * refactor(settings): more sensible placement of methods * chore(migration): script to hand out procgen usernames * fix(migration): don't give EVERYONE new names you doofus * fix(migration): limit data retrieved, be extra careful about updates * fix(migration): use missing field, not migration tag, for query * fix(migration): unused var * fix(usernames): only generate 20 characters * fix(migration): set lowerCaseUsername
This commit is contained in:
parent
5a8366468b
commit
ebf3b4aa47
35 changed files with 1279 additions and 176 deletions
99
migrations/users/generate-usernames.js
Normal file
99
migrations/users/generate-usernames.js
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
let authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||
let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
|
||||
|
||||
/*
|
||||
* Generate usernames for users who lack them
|
||||
*/
|
||||
|
||||
import monk from 'monk';
|
||||
import nconf from 'nconf';
|
||||
import { generateUsername } from '../../website/server/libs/auth/utils';
|
||||
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING'); // FOR TEST DATABASE
|
||||
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
|
||||
|
||||
function processUsers (lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
let query = {
|
||||
'auth.local.username': {$exists: false},
|
||||
'auth.timestamps.loggedin': {$gt: new Date('2018-04-01')}, // Initial coverage for users active within last 6 months
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId,
|
||||
};
|
||||
}
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: [
|
||||
'auth',
|
||||
], // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
})
|
||||
.then(updateUsers)
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
return exiting(1, `ERROR! ${ err}`);
|
||||
});
|
||||
}
|
||||
|
||||
let progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
let userPromises = users.map(updateUser);
|
||||
let lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPromises)
|
||||
.then(() => {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser (user) {
|
||||
count++;
|
||||
|
||||
if (!user.auth.local.username) {
|
||||
const newName = generateUsername();
|
||||
dbUsers.update(
|
||||
{_id: user._id},
|
||||
{$set:
|
||||
{
|
||||
'auth.local.username': newName,
|
||||
'auth.local.lowerCaseUsername': newName,
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
if (count % progressCount === 0) console.warn(`${count } ${ user._id}`);
|
||||
if (user._id === authorUuid) console.warn(`${authorName } processed`);
|
||||
}
|
||||
|
||||
function displayData () {
|
||||
console.warn(`\n${ count } users processed\n`);
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function exiting (code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) {
|
||||
msg = 'ERROR!';
|
||||
}
|
||||
if (msg) {
|
||||
if (code) {
|
||||
console.error(msg);
|
||||
} else {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
module.exports = processUsers;
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
|
|
@ -769,6 +769,11 @@
|
|||
"ansi-wrap": "0.1.0"
|
||||
}
|
||||
},
|
||||
"any-base": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz",
|
||||
"integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg=="
|
||||
},
|
||||
"anymatch": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
|
||||
|
|
@ -23763,6 +23768,15 @@
|
|||
"rechoir": "^0.6.2"
|
||||
}
|
||||
},
|
||||
"short-uuid": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/short-uuid/-/short-uuid-3.0.0.tgz",
|
||||
"integrity": "sha512-AMyVEBZ6xlqlRYXaBlYlejjOeSEGfW9fBeRq95itPLt0eq4CDVsuOAgMYDx4TgPBuCQAMQ/NjNfW/lJKfdWejQ==",
|
||||
"requires": {
|
||||
"any-base": "^1.1.0",
|
||||
"uuid": "^3.2.1"
|
||||
}
|
||||
},
|
||||
"sigmund": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@
|
|||
"rimraf": "^2.4.3",
|
||||
"sass-loader": "^7.0.0",
|
||||
"shelljs": "^0.8.2",
|
||||
"short-uuid": "^3.0.0",
|
||||
"smartbanner.js": "^1.9.1",
|
||||
"stripe": "^5.9.0",
|
||||
"superagent": "^3.8.3",
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ describe('PUT /user', () => {
|
|||
});
|
||||
|
||||
|
||||
it('profile.name cannot be an empty string or null', async () => {
|
||||
it('validates profile.name', async () => {
|
||||
await expect(user.put('/user', {
|
||||
'profile.name': ' ', // string should be trimmed
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
|
|
@ -76,7 +76,23 @@ describe('PUT /user', () => {
|
|||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'User validation failed',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
|
||||
await expect(user.put('/user', {
|
||||
'profile.name': 'this is a very long display name that will not be allowed due to length',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('displaynameIssueLength'),
|
||||
});
|
||||
|
||||
await expect(user.put('/user', {
|
||||
'profile.name': 'TESTPLACEHOLDERSLURWORDHERE',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('displaynameIssueSlur'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -41,6 +41,23 @@ describe('POST /user/auth/local/register', () => {
|
|||
expect(user.newUser).to.eql(true);
|
||||
});
|
||||
|
||||
it('registers a new user and sets verifiedUsername to true', async () => {
|
||||
let username = generateRandomUserName();
|
||||
let email = `${username}@example.com`;
|
||||
let password = 'password';
|
||||
|
||||
let user = await api.post('/user/auth/local/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
});
|
||||
|
||||
expect(user._id).to.exist;
|
||||
expect(user.apiToken).to.exist;
|
||||
expect(user.flags.verifiedUsername).to.eql(true);
|
||||
});
|
||||
|
||||
xit('remove spaces from username', async () => {
|
||||
// TODO can probably delete this test now
|
||||
let username = ' usernamewithspaces ';
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ describe('POST /user/auth/social', () => {
|
|||
});
|
||||
|
||||
it('registers a new user', async () => {
|
||||
let response = await api.post(endpoint, {
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: {access_token: randomAccessToken}, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
|
@ -47,7 +47,10 @@ describe('POST /user/auth/social', () => {
|
|||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.newUser).to.be.true;
|
||||
expect(response.username).to.exist;
|
||||
|
||||
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a facebook user');
|
||||
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.exist;
|
||||
});
|
||||
|
||||
it('logs an existing user in', async () => {
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ describe('PUT /user', () => {
|
|||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'User validation failed',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
89
test/api/v4/user/auth/POST-user_verify_username.test.js
Normal file
89
test/api/v4/user/auth/POST-user_verify_username.test.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v4';
|
||||
|
||||
const ENDPOINT = '/user/auth/verify-username';
|
||||
|
||||
describe('POST /user/auth/verify-username', async () => {
|
||||
let user;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('successfully verifies username', async () => {
|
||||
let newUsername = 'new-username';
|
||||
let response = await user.post(ENDPOINT, {
|
||||
username: newUsername,
|
||||
});
|
||||
expect(response).to.eql({ isUsable: true });
|
||||
});
|
||||
|
||||
it('successfully verifies username with allowed characters', async () => {
|
||||
let newUsername = 'new-username_123';
|
||||
let response = await user.post(ENDPOINT, {
|
||||
username: newUsername,
|
||||
});
|
||||
expect(response).to.eql({ isUsable: true });
|
||||
});
|
||||
|
||||
context('errors', async () => {
|
||||
it('errors if username is not provided', async () => {
|
||||
await expect(user.post(ENDPOINT, {
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('errors if username is a slur', async () => {
|
||||
await expect(user.post(ENDPOINT, {
|
||||
username: 'TESTPLACEHOLDERSLURWORDHERE',
|
||||
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueLength'), t('usernameIssueSlur')] });
|
||||
});
|
||||
|
||||
it('errors if username contains a slur', async () => {
|
||||
await expect(user.post(ENDPOINT, {
|
||||
username: 'TESTPLACEHOLDERSLURWORDHERE_otherword',
|
||||
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueLength'), t('usernameIssueSlur')] });
|
||||
await expect(user.post(ENDPOINT, {
|
||||
username: 'something_TESTPLACEHOLDERSLURWORDHERE',
|
||||
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueLength'), t('usernameIssueSlur')] });
|
||||
await expect(user.post(ENDPOINT, {
|
||||
username: 'somethingTESTPLACEHOLDERSLURWORDHEREotherword',
|
||||
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueLength'), t('usernameIssueSlur')] });
|
||||
});
|
||||
|
||||
it('errors if username is not allowed', async () => {
|
||||
await expect(user.post(ENDPOINT, {
|
||||
username: 'support',
|
||||
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueForbidden')] });
|
||||
});
|
||||
|
||||
it('errors if username is not allowed regardless of casing', async () => {
|
||||
await expect(user.post(ENDPOINT, {
|
||||
username: 'SUppORT',
|
||||
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueForbidden')] });
|
||||
});
|
||||
|
||||
it('errors if username has incorrect length', async () => {
|
||||
await expect(user.post(ENDPOINT, {
|
||||
username: 'thisisaverylongusernameover20characters',
|
||||
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueLength')] });
|
||||
});
|
||||
|
||||
it('errors if username contains invalid characters', async () => {
|
||||
await expect(user.post(ENDPOINT, {
|
||||
username: 'Eichhörnchen',
|
||||
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueInvalidCharacters')] });
|
||||
await expect(user.post(ENDPOINT, {
|
||||
username: 'test.name',
|
||||
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueInvalidCharacters')] });
|
||||
await expect(user.post(ENDPOINT, {
|
||||
username: '🤬',
|
||||
})).to.eventually.eql({ isUsable: false, issues: [t('usernameIssueInvalidCharacters')] });
|
||||
});
|
||||
});
|
||||
});
|
||||
224
test/api/v4/user/auth/PUT-user_update_username.test.js
Normal file
224
test/api/v4/user/auth/PUT-user_update_username.test.js
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v4';
|
||||
import {
|
||||
bcryptCompare,
|
||||
sha1MakeSalt,
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
} from '../../../../../website/server/libs/password';
|
||||
|
||||
const ENDPOINT = '/user/auth/update-username';
|
||||
|
||||
describe('PUT /user/auth/update-username', async () => {
|
||||
let user;
|
||||
let password = 'password'; // from habitrpg/test/helpers/api-integration/v4/object-generators.js
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('successfully changes username with password', async () => {
|
||||
let newUsername = 'new-username';
|
||||
let response = await user.put(ENDPOINT, {
|
||||
username: newUsername,
|
||||
password,
|
||||
});
|
||||
expect(response).to.eql({ username: newUsername });
|
||||
await user.sync();
|
||||
expect(user.auth.local.username).to.eql(newUsername);
|
||||
});
|
||||
|
||||
it('successfully changes username without password', async () => {
|
||||
let newUsername = 'new-username-nopw';
|
||||
let response = await user.put(ENDPOINT, {
|
||||
username: newUsername,
|
||||
});
|
||||
expect(response).to.eql({ username: newUsername });
|
||||
await user.sync();
|
||||
expect(user.auth.local.username).to.eql(newUsername);
|
||||
});
|
||||
|
||||
it('successfully changes username containing number and underscore', async () => {
|
||||
let newUsername = 'new_username9';
|
||||
let response = await user.put(ENDPOINT, {
|
||||
username: newUsername,
|
||||
});
|
||||
expect(response).to.eql({ username: newUsername });
|
||||
await user.sync();
|
||||
expect(user.auth.local.username).to.eql(newUsername);
|
||||
});
|
||||
|
||||
it('sets verifiedUsername when changing username', async () => {
|
||||
user.flags.verifiedUsername = false;
|
||||
await user.sync();
|
||||
let newUsername = 'new-username-verify';
|
||||
let response = await user.put(ENDPOINT, {
|
||||
username: newUsername,
|
||||
});
|
||||
expect(response).to.eql({ username: newUsername });
|
||||
await user.sync();
|
||||
expect(user.flags.verifiedUsername).to.eql(true);
|
||||
});
|
||||
|
||||
it('converts user with SHA1 encrypted password to bcrypt encryption', async () => {
|
||||
let myNewUsername = 'my-new-username';
|
||||
let textPassword = 'mySecretPassword';
|
||||
let salt = sha1MakeSalt();
|
||||
let sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
|
||||
await user.update({
|
||||
'auth.local.hashed_password': sha1HashedPassword,
|
||||
'auth.local.passwordHashMethod': 'sha1',
|
||||
'auth.local.salt': salt,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('sha1');
|
||||
expect(user.auth.local.salt).to.equal(salt);
|
||||
expect(user.auth.local.hashed_password).to.equal(sha1HashedPassword);
|
||||
|
||||
// update email
|
||||
let response = await user.put(ENDPOINT, {
|
||||
username: myNewUsername,
|
||||
password: textPassword,
|
||||
});
|
||||
expect(response).to.eql({ username: myNewUsername });
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.auth.local.username).to.eql(myNewUsername);
|
||||
expect(user.auth.local.passwordHashMethod).to.equal('bcrypt');
|
||||
expect(user.auth.local.salt).to.be.undefined;
|
||||
expect(user.auth.local.hashed_password).not.to.equal(sha1HashedPassword);
|
||||
|
||||
let isValidPassword = await bcryptCompare(textPassword, user.auth.local.hashed_password);
|
||||
expect(isValidPassword).to.equal(true);
|
||||
});
|
||||
|
||||
context('errors', async () => {
|
||||
it('prevents username update if new username is already taken', async () => {
|
||||
let existingUsername = 'existing-username';
|
||||
await generateUser({'auth.local.username': existingUsername, 'auth.local.lowerCaseUsername': existingUsername });
|
||||
|
||||
await expect(user.put(ENDPOINT, {
|
||||
username: existingUsername,
|
||||
password,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('usernameTaken'),
|
||||
});
|
||||
});
|
||||
|
||||
it('errors if password is wrong', async () => {
|
||||
let newUsername = 'new-username';
|
||||
await expect(user.put(ENDPOINT, {
|
||||
username: newUsername,
|
||||
password: 'wrong-password',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('wrongPassword'),
|
||||
});
|
||||
});
|
||||
|
||||
it('errors if new username is not provided', async () => {
|
||||
await expect(user.put(ENDPOINT, {
|
||||
password,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('errors if new username is a slur', async () => {
|
||||
await expect(user.put(ENDPOINT, {
|
||||
username: 'TESTPLACEHOLDERSLURWORDHERE',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: [t('usernameIssueLength'), t('usernameIssueSlur')].join(' '),
|
||||
});
|
||||
});
|
||||
|
||||
it('errors if new username contains a slur', async () => {
|
||||
await expect(user.put(ENDPOINT, {
|
||||
username: 'TESTPLACEHOLDERSLURWORDHERE_otherword',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: [t('usernameIssueLength'), t('usernameIssueSlur')].join(' '),
|
||||
});
|
||||
await expect(user.put(ENDPOINT, {
|
||||
username: 'something_TESTPLACEHOLDERSLURWORDHERE',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: [t('usernameIssueLength'), t('usernameIssueSlur')].join(' '),
|
||||
});
|
||||
await expect(user.put(ENDPOINT, {
|
||||
username: 'somethingTESTPLACEHOLDERSLURWORDHEREotherword',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: [t('usernameIssueLength'), t('usernameIssueSlur')].join(' '),
|
||||
});
|
||||
});
|
||||
|
||||
it('errors if new username is not allowed', async () => {
|
||||
await expect(user.put(ENDPOINT, {
|
||||
username: 'support',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('usernameIssueForbidden'),
|
||||
});
|
||||
});
|
||||
|
||||
it('errors if new username is not allowed regardless of casing', async () => {
|
||||
await expect(user.put(ENDPOINT, {
|
||||
username: 'SUppORT',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('usernameIssueForbidden'),
|
||||
});
|
||||
});
|
||||
|
||||
it('errors if username has incorrect length', async () => {
|
||||
await expect(user.put(ENDPOINT, {
|
||||
username: 'thisisaverylongusernameover20characters',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('usernameIssueLength'),
|
||||
});
|
||||
});
|
||||
|
||||
it('errors if new username contains invalid characters', async () => {
|
||||
await expect(user.put(ENDPOINT, {
|
||||
username: 'Eichhörnchen',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('usernameIssueInvalidCharacters'),
|
||||
});
|
||||
await expect(user.put(ENDPOINT, {
|
||||
username: 'test.name',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('usernameIssueInvalidCharacters'),
|
||||
});
|
||||
await expect(user.put(ENDPOINT, {
|
||||
username: '🤬',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('usernameIssueInvalidCharacters'),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
56
website/client/assets/scss/iconalert.scss
Normal file
56
website/client/assets/scss/iconalert.scss
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
.iconalert {
|
||||
position: relative;
|
||||
padding: $alert-padding-y $alert-padding-x;
|
||||
margin-bottom: $alert-margin-bottom;
|
||||
border-radius: 2px;
|
||||
color: white;
|
||||
font-family: Roboto;
|
||||
font-size: 14px;
|
||||
line-height: 1.43;
|
||||
padding-left: 60px
|
||||
}
|
||||
|
||||
.iconalert::before {
|
||||
height:100%;
|
||||
content:' ';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 44px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
margin-right: $alert-padding-x;
|
||||
border-top-left-radius: 2px;
|
||||
border-bottom-left-radius: 2px;
|
||||
}
|
||||
|
||||
.iconalert-success {
|
||||
background-color: #24cc8f;
|
||||
}
|
||||
|
||||
.iconalert-success::before {
|
||||
background-image: url(~client/assets/svg/for-css/checkbox-white.svg);
|
||||
background-size: 13px 10px;
|
||||
background-color: #1ca372;
|
||||
}
|
||||
|
||||
.iconalert-warning::before, .iconalert-error::before {
|
||||
background-image: url(~client/assets/svg/for-css/alert-white.svg);
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
|
||||
.iconalert-warning {
|
||||
background-color: #ffa623;
|
||||
}
|
||||
|
||||
.iconalert-warning::before {
|
||||
background-color: #ee9109;
|
||||
}
|
||||
|
||||
.iconalert-error {
|
||||
background-color: #f74e52;
|
||||
}
|
||||
|
||||
.iconalert-error::before {
|
||||
background-color: #de3f3f;
|
||||
}
|
||||
|
|
@ -34,3 +34,4 @@
|
|||
@import './progress-bar';
|
||||
@import './pin';
|
||||
@import './animals';
|
||||
@import './iconalert';
|
||||
|
|
|
|||
3
website/client/assets/svg/for-css/alert-white.svg
Normal file
3
website/client/assets/svg/for-css/alert-white.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="#FFF" fill-rule="evenodd" d="M0 1.994C0 .893.895 0 1.994 0h12.012C15.107 0 16 .895 16 1.994v12.012A1.995 1.995 0 0 1 14.006 16H1.994A1.995 1.995 0 0 1 0 14.006V1.994zM2 2v12h12V2H2zm5 2h2v5H7V4zm0 6h2v2H7v-2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 319 B |
|
|
@ -11,20 +11,20 @@
|
|||
span {{registering ? $t('signUpWithSocial', {social: 'Google'}) : $t('loginWithSocial', {social: 'Google'})}}
|
||||
.form-group(v-if='registering')
|
||||
label(for='usernameInput', v-once) {{$t('username')}}
|
||||
input#usernameInput.form-control(type='text', :placeholder='$t("usernamePlaceholder")', v-model='username')
|
||||
input#usernameInput.form-control(type='text', :placeholder='$t("usernamePlaceholder")', v-model='username', :class='{"input-valid": usernameValid, "input-invalid": usernameInvalid}')
|
||||
.form-group(v-if='!registering')
|
||||
label(for='usernameInput', v-once) {{$t('emailOrUsername')}}
|
||||
input#usernameInput.form-control(type='text', :placeholder='$t("emailOrUsername")', v-model='username')
|
||||
.form-group(v-if='registering')
|
||||
label(for='emailInput', v-once) {{$t('email')}}
|
||||
input#emailInput.form-control(type='email', :placeholder='$t("emailPlaceholder")', v-model='email')
|
||||
input#emailInput.form-control(type='email', :placeholder='$t("emailPlaceholder")', v-model='email', :class='{"input-invalid": emailInvalid, "input-valid": emailValid}')
|
||||
.form-group
|
||||
label(for='passwordInput', v-once) {{$t('password')}}
|
||||
a.float-right.forgot-password(v-once, v-if='!registering', @click='forgotPassword = true') {{$t('forgotPassword')}}
|
||||
input#passwordInput.form-control(type='password', :placeholder='$t(registering ? "passwordPlaceholder" : "password")', v-model='password')
|
||||
.form-group(v-if='registering')
|
||||
label(for='confirmPasswordInput', v-once) {{$t('confirmPassword')}}
|
||||
input#confirmPasswordInput.form-control(type='password', :placeholder='$t("confirmPasswordPlaceholder")', v-model='passwordConfirm')
|
||||
input#confirmPasswordInput.form-control(type='password', :placeholder='$t("confirmPasswordPlaceholder")', v-model='passwordConfirm', :class='{"input-invalid": passwordConfirmInvalid, "input-valid": passwordConfirmValid}')
|
||||
small.form-text(v-once, v-html="$t('termsAndAgreement')")
|
||||
.text-center
|
||||
.btn.btn-info(@click='register()', v-if='registering', v-once) {{$t('joinHabitica')}}
|
||||
|
|
@ -71,12 +71,17 @@
|
|||
small.form-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.input-valid {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import hello from 'hellojs';
|
||||
import { setUpAxios } from 'client/libs/auth';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
import facebookSquareIcon from 'assets/svg/facebook-square.svg';
|
||||
import googleIcon from 'assets/svg/google.svg';
|
||||
|
|
@ -90,6 +95,7 @@ export default {
|
|||
email: '',
|
||||
password: '',
|
||||
passwordConfirm: '',
|
||||
usernameIssues: [],
|
||||
};
|
||||
|
||||
data.icons = Object.freeze({
|
||||
|
|
@ -106,7 +112,50 @@ export default {
|
|||
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
emailValid () {
|
||||
if (this.email.length <= 3) return false;
|
||||
return this.validateEmail(this.email);
|
||||
},
|
||||
emailInvalid () {
|
||||
return !this.emailValid;
|
||||
},
|
||||
usernameValid () {
|
||||
if (this.username.length <= 3) return false;
|
||||
return this.usernameIssues.length === 0;
|
||||
},
|
||||
usernameInvalid () {
|
||||
return !this.usernameValid;
|
||||
},
|
||||
passwordConfirmValid () {
|
||||
if (this.passwordConfirm.length <= 3) return false;
|
||||
return this.passwordConfirm === this.password;
|
||||
},
|
||||
passwordConfirmInvalid () {
|
||||
return !this.passwordConfirmValid;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
username () {
|
||||
this.validateUsername(this.username);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// eslint-disable-next-line func-names
|
||||
validateUsername: debounce(function (username) {
|
||||
if (username.length <= 3) {
|
||||
return;
|
||||
}
|
||||
this.$store.dispatch('auth:verifyUsername', {
|
||||
username: this.username,
|
||||
}).then(res => {
|
||||
if (res.issues !== undefined) {
|
||||
this.usernameIssues = res.issues;
|
||||
} else {
|
||||
this.usernameIssues = [];
|
||||
}
|
||||
});
|
||||
}, 500),
|
||||
// @TODO: Abstract hello in to action or lib
|
||||
async socialAuth (network) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -24,20 +24,21 @@
|
|||
.text {{registering ? $t('signUpWithSocial', {social: 'Google'}) : $t('loginWithSocial', {social: 'Google'})}}
|
||||
.form-group(v-if='registering')
|
||||
label(for='usernameInput', v-once) {{$t('username')}}
|
||||
input#usernameInput.form-control(type='text', :placeholder='$t("usernamePlaceholder")', v-model='username')
|
||||
input#usernameInput.form-control(type='text', :placeholder='$t("usernamePlaceholder")', v-model='username', :class='{"input-valid": usernameValid, "input-invalid": usernameInvalid}')
|
||||
.input-error(v-for="issue in usernameIssues") {{ issue }}
|
||||
.form-group(v-if='!registering')
|
||||
label(for='usernameInput', v-once) {{$t('emailOrUsername')}}
|
||||
input#usernameInput.form-control(type='text', :placeholder='$t("emailOrUsername")', v-model='username')
|
||||
.form-group(v-if='registering')
|
||||
label(for='emailInput', v-once) {{$t('email')}}
|
||||
input#emailInput.form-control(type='email', :placeholder='$t("emailPlaceholder")', v-model='email')
|
||||
input#emailInput.form-control(type='email', :placeholder='$t("emailPlaceholder")', v-model='email', :class='{"input-invalid": emailInvalid, "input-valid": emailValid}')
|
||||
.form-group
|
||||
label(for='passwordInput', v-once) {{$t('password')}}
|
||||
a.float-right.forgot-password(v-once, v-if='!registering', @click='forgotPassword = true') {{$t('forgotPassword')}}
|
||||
input#passwordInput.form-control(type='password', :placeholder='$t(registering ? "passwordPlaceholder" : "password")', v-model='password')
|
||||
.form-group(v-if='registering')
|
||||
label(for='confirmPasswordInput', v-once) {{$t('confirmPassword')}}
|
||||
input#confirmPasswordInput.form-control(type='password', :placeholder='$t("confirmPasswordPlaceholder")', v-model='passwordConfirm')
|
||||
input#confirmPasswordInput.form-control(type='password', :placeholder='$t("confirmPasswordPlaceholder")', v-model='passwordConfirm', :class='{"input-invalid": passwordConfirmInvalid, "input-valid": passwordConfirmValid}')
|
||||
small.form-text(v-once, v-html="$t('termsAndAgreement')")
|
||||
.text-center
|
||||
.btn.btn-info(@click='register()', v-if='registering', v-once) {{$t('joinHabitica')}}
|
||||
|
|
@ -200,6 +201,10 @@
|
|||
color: $white;
|
||||
}
|
||||
|
||||
#usernameInput.input-invalid {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.form-text {
|
||||
font-size: 14px;
|
||||
color: $white;
|
||||
|
|
@ -277,11 +282,19 @@
|
|||
.forgot-password {
|
||||
color: #bda8ff !important;
|
||||
}
|
||||
|
||||
.input-error {
|
||||
color: #fff;
|
||||
font-size: 90%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import hello from 'hellojs';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
import gryphon from 'assets/svg/gryphon.svg';
|
||||
import habiticaIcon from 'assets/svg/habitica-logo.svg';
|
||||
|
|
@ -300,6 +313,7 @@ export default {
|
|||
hasError: null,
|
||||
code: null,
|
||||
},
|
||||
usernameIssues: [],
|
||||
};
|
||||
|
||||
data.icons = Object.freeze({
|
||||
|
|
@ -324,6 +338,30 @@ export default {
|
|||
}
|
||||
return false;
|
||||
},
|
||||
emailValid () {
|
||||
if (this.email.length <= 3) return false;
|
||||
return this.validateEmail(this.email);
|
||||
},
|
||||
emailInvalid () {
|
||||
if (this.email.length <= 3) return false;
|
||||
return !this.emailValid;
|
||||
},
|
||||
usernameValid () {
|
||||
if (this.username.length <= 3) return false;
|
||||
return this.usernameIssues.length === 0;
|
||||
},
|
||||
usernameInvalid () {
|
||||
if (this.username.length <= 3) return false;
|
||||
return !this.usernameValid;
|
||||
},
|
||||
passwordConfirmValid () {
|
||||
if (this.passwordConfirm.length <= 3) return false;
|
||||
return this.passwordConfirm === this.password;
|
||||
},
|
||||
passwordConfirmInvalid () {
|
||||
if (this.passwordConfirm.length <= 3) return false;
|
||||
return !this.passwordConfirmValid;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
hello.init({
|
||||
|
|
@ -357,8 +395,26 @@ export default {
|
|||
},
|
||||
immediate: true,
|
||||
},
|
||||
username () {
|
||||
this.validateUsername(this.username);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// eslint-disable-next-line func-names
|
||||
validateUsername: debounce(function (username) {
|
||||
if (username.length <= 3 || !this.registering) {
|
||||
return;
|
||||
}
|
||||
this.$store.dispatch('auth:verifyUsername', {
|
||||
username: this.username,
|
||||
}).then(res => {
|
||||
if (res.issues !== undefined) {
|
||||
this.usernameIssues = res.issues;
|
||||
} else {
|
||||
this.usernameIssues = [];
|
||||
}
|
||||
});
|
||||
}, 500),
|
||||
async register () {
|
||||
// @TODO do not use alert
|
||||
if (!this.email) {
|
||||
|
|
|
|||
|
|
@ -61,4 +61,4 @@ export default {
|
|||
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="false",
|
||||
:has-icon="false",
|
||||
:read-after-click="false",
|
||||
:notification="{}",
|
||||
@click="action",
|
||||
)
|
||||
div.text-center(slot="content")
|
||||
div.username-notification-title {{ $t('setUsernameNotificationTitle') }}
|
||||
div {{ $t('setUsernameNotificationBody') }}
|
||||
div.current-username-container.mx-auto
|
||||
label.font-weight-bold {{ $t('currentUsername') + " " }}
|
||||
label @
|
||||
label {{ user.auth.local.username }}
|
||||
.notifications-buttons
|
||||
.btn.btn-small.btn-secondary(@click.stop="changeUsername()") {{ $t('goToSettings') }}
|
||||
</template>
|
||||
<style lang='scss'>
|
||||
@import '../../../assets/scss/colors.scss';
|
||||
|
||||
.username-notification-title {
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: bold;
|
||||
color: $purple-300;
|
||||
}
|
||||
|
||||
.current-username-container {
|
||||
border-radius: 2px;
|
||||
background-color: #f9f9f9;
|
||||
border: solid 1px #e1e0e3;
|
||||
padding: 8px 16px 8px 16px;
|
||||
display: inline-block;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
label {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.notification-buttons {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import axios from 'axios';
|
||||
|
||||
export default {
|
||||
props: ['notification'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
this.$router.push({ name: 'site' });
|
||||
},
|
||||
async confirmUsername () {
|
||||
await axios.put('/api/v4/user/auth/update-username', {username: this.user.auth.local.username});
|
||||
},
|
||||
changeUsername () {
|
||||
this.$router.push({ name: 'site' });
|
||||
},
|
||||
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -93,6 +93,7 @@ import CARD_RECEIVED from './notifications/cardReceived';
|
|||
import NEW_INBOX_MESSAGE from './notifications/newInboxMessage';
|
||||
import NEW_CHAT_MESSAGE from './notifications/newChatMessage';
|
||||
import WORLD_BOSS from './notifications/worldBoss';
|
||||
import VERIFY_USERNAME from './notifications/verifyUsername';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -105,6 +106,7 @@ export default {
|
|||
UNALLOCATED_STATS_POINTS, NEW_MYSTERY_ITEMS, CARD_RECEIVED,
|
||||
NEW_INBOX_MESSAGE, NEW_CHAT_MESSAGE,
|
||||
WorldBoss: WORLD_BOSS,
|
||||
VERIFY_USERNAME,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
|
@ -127,6 +129,7 @@ export default {
|
|||
'QUEST_INVITATION', 'GROUP_TASK_APPROVAL', 'GROUP_TASK_APPROVED',
|
||||
'NEW_MYSTERY_ITEMS', 'CARD_RECEIVED',
|
||||
'NEW_INBOX_MESSAGE', 'NEW_CHAT_MESSAGE', 'UNALLOCATED_STATS_POINTS',
|
||||
'VERIFY_USERNAME',
|
||||
],
|
||||
};
|
||||
},
|
||||
|
|
@ -179,6 +182,16 @@ export default {
|
|||
});
|
||||
}
|
||||
|
||||
if (this.user.flags.verifiedUsername !== true) {
|
||||
notifications.push({
|
||||
type: 'VERIFY_USERNAME',
|
||||
data: {
|
||||
username: this.user.auth.local.username,
|
||||
},
|
||||
id: 'custom-change-username',
|
||||
});
|
||||
}
|
||||
|
||||
const orderMap = this.notificationsOrder;
|
||||
|
||||
// Push the notifications stored in user.notifications
|
||||
|
|
|
|||
|
|
@ -115,13 +115,9 @@
|
|||
button.btn.btn-primary.mb-2(disabled='disabled', v-if='!hasBackupAuthOption(network.key) && user.auth[network.key].id') {{ $t('registeredWithSocial', {network: network.name}) }}
|
||||
button.btn.btn-danger(@click='deleteSocialAuth(network)', v-if='hasBackupAuthOption(network.key) && user.auth[network.key].id') {{ $t('detachSocial', {network: network.name}) }}
|
||||
hr
|
||||
div(v-if='!user.auth.local.username')
|
||||
div(v-if='!user.auth.local.email')
|
||||
p {{ $t('addLocalAuth') }}
|
||||
p {{ $t('usernameLimitations') }}
|
||||
.form(name='localAuth', novalidate)
|
||||
//-.alert.alert-danger(ng-messages='changeUsername.$error && changeUsername.submitted') {{ $t('fillAll') }}
|
||||
.form-group
|
||||
input.form-control(type='text', :placeholder="$t('username')", v-model='localAuth.username', required)
|
||||
.form-group
|
||||
input.form-control(type='text', :placeholder="$t('email')", v-model='localAuth.email', required)
|
||||
.form-group
|
||||
|
|
@ -130,37 +126,37 @@
|
|||
input.form-control(type='password', :placeholder="$t('confirmPass')", v-model='localAuth.confirmPassword', required)
|
||||
button.btn.btn-primary(type='submit', @click='addLocalAuth()') {{ $t('submit') }}
|
||||
|
||||
.usersettings(v-if='user.auth.local.username')
|
||||
p {{ $t('username') }}
|
||||
|: {{user.auth.local.username}}
|
||||
p
|
||||
small.muted
|
||||
| {{ $t('loginNameDescription') }}
|
||||
p {{ $t('email') }}
|
||||
|: {{user.auth.local.email}}
|
||||
hr
|
||||
.usersettings
|
||||
h5 {{ $t('changeDisplayName') }}
|
||||
.form(name='changeDisplayName', novalidate)
|
||||
.form-group
|
||||
input#changeDisplayname.form-control(type='text', :placeholder="$t('newDisplayName')", v-model='temporaryDisplayName')
|
||||
button.btn.btn-primary(type='submit', @click='changeDisplayName(temporaryDisplayName)') {{ $t('submit') }}
|
||||
|
||||
h5 {{ $t('changeUsername') }}
|
||||
.form(v-if='user.auth.local', name='changeUsername', novalidate)
|
||||
//-.alert.alert-danger(ng-messages='changeUsername.$error && changeUsername.submitted') {{ $t('fillAll') }}
|
||||
.form(name='changeUsername', novalidate)
|
||||
.iconalert.iconalert-success(v-if='verifiedUsername') {{ $t('usernameVerifiedConfirmation', {'username': user.auth.local.username}) }}
|
||||
.iconalert.iconalert-warning(v-else)
|
||||
div.align-middle
|
||||
span {{ $t('usernameNotVerified') }}
|
||||
button.btn.btn-secondary.btn-small.float-right(@click='changeUser("username", {username: user.auth.local.username})') {{ $t('confirmUsername') }}
|
||||
.form-group
|
||||
input.form-control(type='text', :placeholder="$t('newUsername')", v-model='usernameUpdates.username')
|
||||
input#changeUsername.form-control(type='text', :placeholder="$t('newUsername')", v-model='usernameUpdates.username', :class='{"is-invalid input-invalid": usernameInvalid}')
|
||||
.input-error(v-for="issue in usernameIssues") {{ issue }}
|
||||
small.form-text.text-muted {{ $t('changeUsernameDisclaimer') }}
|
||||
button.btn.btn-primary(type='submit', @click='changeUser("username", usernameUpdates)', :disabled='usernameCanSubmit') {{ $t('submit') }}
|
||||
h5(v-if='user.auth.local.email') {{ $t('changeEmail') }}
|
||||
.form(v-if='user.auth.local.email', name='changeEmail', novalidate)
|
||||
.form-group
|
||||
input.form-control(type='password', :placeholder="$t('password')", v-model='usernameUpdates.password')
|
||||
button.btn.btn-primary(type='submit', @click='changeUser("username", usernameUpdates)') {{ $t('submit') }}
|
||||
|
||||
h5 {{ $t('changeEmail') }}
|
||||
.form(v-if='user.auth.local', name='changeEmail', novalidate)
|
||||
.form-group
|
||||
input.form-control(type='text', :placeholder="$t('newEmail')", v-model='emailUpdates.newEmail')
|
||||
input#changeEmail.form-control(type='text', :placeholder="$t('newEmail')", v-model='emailUpdates.newEmail')
|
||||
.form-group
|
||||
input.form-control(type='password', :placeholder="$t('password')", v-model='emailUpdates.password')
|
||||
button.btn.btn-primary(type='submit', @click='changeUser("email", emailUpdates)') {{ $t('submit') }}
|
||||
|
||||
h5 {{ $t('changePass') }}
|
||||
.form(v-if='user.auth.local', name='changePassword', novalidate)
|
||||
h5(v-if='user.auth.local.email') {{ $t('changePass') }}
|
||||
.form(v-if='user.auth.local.email', name='changePassword', novalidate)
|
||||
.form-group
|
||||
input.form-control(type='password', :placeholder="$t('oldPass')", v-model='passwordUpdates.password')
|
||||
input#changePassword.form-control(type='password', :placeholder="$t('oldPass')", v-model='passwordUpdates.password')
|
||||
.form-group
|
||||
input.form-control(type='password', :placeholder="$t('newPass')", v-model='passwordUpdates.newPassword')
|
||||
.form-group
|
||||
|
|
@ -177,10 +173,33 @@
|
|||
popover-trigger='mouseenter', v-b-popover.hover.auto="$t('deleteAccPop')") {{ $t('deleteAccount') }}
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
input {
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.usersettings h5 {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.iconalert > div > span {
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.iconalert > div:after {
|
||||
clear: both;
|
||||
content: '';
|
||||
display: table;
|
||||
}
|
||||
|
||||
.input-error {
|
||||
color: $red-50;
|
||||
font-size: 90%;
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
|
@ -188,7 +207,7 @@ import hello from 'hellojs';
|
|||
import moment from 'moment';
|
||||
import axios from 'axios';
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
import debounce from 'lodash/debounce';
|
||||
import restoreModal from './restoreModal';
|
||||
import resetModal from './resetModal';
|
||||
import deleteModal from './deleteModal';
|
||||
|
|
@ -224,7 +243,8 @@ export default {
|
|||
availableFormats: ['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy/MM/dd'],
|
||||
dayStartOptions,
|
||||
newDayStart: 0,
|
||||
usernameUpdates: {},
|
||||
temporaryDisplayName: '',
|
||||
usernameUpdates: {username: ''},
|
||||
emailUpdates: {},
|
||||
passwordUpdates: {},
|
||||
localAuth: {
|
||||
|
|
@ -233,6 +253,7 @@ export default {
|
|||
password: '',
|
||||
confirmPassword: '',
|
||||
},
|
||||
usernameIssues: [],
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
|
|
@ -240,12 +261,25 @@ export default {
|
|||
// @TODO: We may need to request the party here
|
||||
this.party = this.$store.state.party;
|
||||
this.newDayStart = this.user.preferences.dayStart;
|
||||
this.usernameUpdates.username = this.user.auth.local.username || null;
|
||||
this.temporaryDisplayName = this.user.profile.name;
|
||||
this.emailUpdates.newEmail = this.user.auth.local.email || null;
|
||||
hello.init({
|
||||
facebook: process.env.FACEBOOK_KEY, // eslint-disable-line no-process-env
|
||||
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line no-process-env
|
||||
}, {
|
||||
redirect_uri: '', // eslint-disable-line
|
||||
});
|
||||
|
||||
const focusID = this.$route.query.focus;
|
||||
if (focusID !== undefined && focusID !== null) {
|
||||
this.$nextTick(() => {
|
||||
const element = document.getElementById(focusID);
|
||||
if (element !== undefined && element !== null) {
|
||||
element.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
|
|
@ -275,8 +309,47 @@ export default {
|
|||
hasClass () {
|
||||
return this.$store.getters['members:hasClass'](this.user);
|
||||
},
|
||||
verifiedUsername () {
|
||||
return this.user.flags.verifiedUsername;
|
||||
},
|
||||
usernameValid () {
|
||||
if (this.usernameUpdates.username.length <= 1) return false;
|
||||
return this.usernameIssues.length === 0;
|
||||
},
|
||||
usernameInvalid () {
|
||||
if (this.usernameUpdates.username.length <= 1) return false;
|
||||
return !this.usernameValid;
|
||||
},
|
||||
usernameCanSubmit () {
|
||||
if (this.usernameUpdates.username.length <= 1) return true;
|
||||
return !this.usernameValid;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
usernameUpdates: {
|
||||
handler () {
|
||||
this.validateUsername(this.usernameUpdates.username);
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// eslint-disable-next-line func-names
|
||||
validateUsername: debounce(function (username) {
|
||||
if (username.length <= 1 || username === this.user.auth.local.username) {
|
||||
this.usernameIssues = [];
|
||||
return;
|
||||
}
|
||||
this.$store.dispatch('auth:verifyUsername', {
|
||||
username,
|
||||
}).then(res => {
|
||||
if (res.issues !== undefined) {
|
||||
this.usernameIssues = res.issues;
|
||||
} else {
|
||||
this.usernameIssues = [];
|
||||
}
|
||||
});
|
||||
}, 500),
|
||||
set (preferenceType, subtype) {
|
||||
let settings = {};
|
||||
if (!subtype) {
|
||||
|
|
@ -349,8 +422,18 @@ export default {
|
|||
},
|
||||
async changeUser (attribute, updates) {
|
||||
await axios.put(`/api/v4/user/auth/update-${attribute}`, updates);
|
||||
alert(this.$t(`${attribute}Success`));
|
||||
this.user[attribute] = updates[attribute];
|
||||
if (attribute === 'username') {
|
||||
this.user.auth.local.username = updates[attribute];
|
||||
this.user.flags.verifiedUsername = true;
|
||||
} else if (attribute === 'email') {
|
||||
this.user.auth.local.email = updates[attribute];
|
||||
}
|
||||
},
|
||||
async changeDisplayName (newName) {
|
||||
await axios.put('/api/v4/user/', {'profile.name': newName});
|
||||
alert(this.$t('displayNameSuccess'));
|
||||
this.user.profile.name = newName;
|
||||
this.temporaryDisplayName = newName;
|
||||
},
|
||||
openRestoreModal () {
|
||||
this.$root.$emit('bv::show::modal', 'restore');
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@
|
|||
span {{$t('or')}}
|
||||
.form(@keyup.enter="register()")
|
||||
p.form-text {{$t('usernameLimitations')}}
|
||||
input.form-control(type='text', placeholder='Login Name', v-model='username', :class='{"input-valid": username.length > 3}')
|
||||
input#usernameInput.form-control(type='text', placeholder='Login Name', v-model='username', :class='{"input-valid": usernameValid, "input-invalid": usernameInvalid}')
|
||||
.input-error(v-for="issue in usernameIssues") {{ issue }}
|
||||
input.form-control(type='email', placeholder='Email', v-model='email', :class='{"input-invalid": emailInvalid, "input-valid": emailValid}')
|
||||
input.form-control(type='password', placeholder='Password', v-model='password', :class='{"input-valid": password.length > 3}')
|
||||
input.form-control(type='password', placeholder='Confirm Password', v-model='passwordConfirm', :class='{"input-invalid": passwordConfirmInvalid, "input-valid": passwordConfirmValid}')
|
||||
|
|
@ -293,6 +294,10 @@
|
|||
transition: border .5s, color .5s;
|
||||
}
|
||||
|
||||
#usernameInput.input-invalid {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.input-valid {
|
||||
color: #fff;
|
||||
}
|
||||
|
|
@ -525,10 +530,19 @@
|
|||
margin-bottom: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.input-error {
|
||||
color: #fff;
|
||||
font-size: 90%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import hello from 'hellojs';
|
||||
import debounce from 'lodash/debounce';
|
||||
import googlePlay from 'assets/images/home/google-play-badge.svg';
|
||||
import iosAppStore from 'assets/images/home/ios-app-store.svg';
|
||||
import iphones from 'assets/images/home/iphones.svg';
|
||||
|
|
@ -575,6 +589,7 @@
|
|||
password: '',
|
||||
passwordConfirm: '',
|
||||
email: '',
|
||||
usernameIssues: [],
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
|
|
@ -600,6 +615,14 @@
|
|||
if (this.email.length <= 3) return false;
|
||||
return !this.validateEmail(this.email);
|
||||
},
|
||||
usernameValid () {
|
||||
if (this.username.length <= 3) return false;
|
||||
return this.usernameIssues.length === 0;
|
||||
},
|
||||
usernameInvalid () {
|
||||
if (this.username.length <= 3) return false;
|
||||
return !this.usernameValid;
|
||||
},
|
||||
passwordConfirmValid () {
|
||||
if (this.passwordConfirm.length <= 3) return false;
|
||||
return this.passwordConfirm === this.password;
|
||||
|
|
@ -609,11 +632,31 @@
|
|||
return this.passwordConfirm !== this.password;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
username () {
|
||||
this.validateUsername(this.username);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
validateEmail (email) {
|
||||
let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
return re.test(email);
|
||||
},
|
||||
// eslint-disable-next-line func-names
|
||||
validateUsername: debounce(function (username) {
|
||||
if (username.length <= 3) {
|
||||
return;
|
||||
}
|
||||
this.$store.dispatch('auth:verifyUsername', {
|
||||
username: this.username,
|
||||
}).then(res => {
|
||||
if (res.issues !== undefined) {
|
||||
this.usernameIssues = res.issues;
|
||||
} else {
|
||||
this.usernameIssues = [];
|
||||
}
|
||||
});
|
||||
}, 500),
|
||||
// @TODO this is totally duplicate from the registerLogin component
|
||||
async register () {
|
||||
let groupInvite = '';
|
||||
|
|
|
|||
|
|
@ -46,6 +46,15 @@ export async function login (store, params) {
|
|||
localStorage.setItem(LOCALSTORAGE_AUTH_KEY, userLocalData);
|
||||
}
|
||||
|
||||
export async function verifyUsername (store, params) {
|
||||
let url = '/api/v4/user/auth/verify-username';
|
||||
let result = await axios.post(url, {
|
||||
username: params.username,
|
||||
});
|
||||
|
||||
return result.data.data;
|
||||
}
|
||||
|
||||
export async function socialAuth (store, params) {
|
||||
let url = '/api/v4/user/auth/social';
|
||||
let result = await axios.post(url, {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
"other": "Other",
|
||||
"fullName": "Full Name",
|
||||
"displayName": "Display Name",
|
||||
"changeDisplayName": "Change Display Name",
|
||||
"newDisplayName": "New Display Name",
|
||||
"displayPhoto": "Photo",
|
||||
"displayBlurb": "Blurb",
|
||||
"displayBlurbPlaceholder": "Please introduce yourself",
|
||||
|
|
@ -181,7 +183,7 @@
|
|||
"lostMana": "You used some Mana",
|
||||
"lostHealth": "You lost some Health",
|
||||
"lostExperience": "You lost some Experience",
|
||||
"displayNameDescription1": "This is what appears in messages you post in the Tavern, guilds, and party chat, along with what is displayed on your avatar. To change it, click the Edit button above. If instead you want to change your login name, go to",
|
||||
"displayNameDescription1": "This is what appears in messages you post in the Tavern, guilds, and party chat, along with what is displayed on your avatar. To change it, click the Edit button above. If instead you want to change your username, go to",
|
||||
"displayNameDescription2": "Settings->Site",
|
||||
"displayNameDescription3": "and look in the Registration section.",
|
||||
"unequipBattleGear": "Unequip Battle Gear",
|
||||
|
|
|
|||
|
|
@ -211,8 +211,8 @@
|
|||
"unlockByline2": "Unlock new motivational tools, such as pet collecting, random rewards, spell-casting, and more!",
|
||||
"unlockHeadline": "As you stay productive, you unlock new content!",
|
||||
"useUUID": "Use UUID / API Token (For Facebook Users)",
|
||||
"username": "Login Name",
|
||||
"emailOrUsername": "Email or Login Name (case-sensitive)",
|
||||
"username": "Username",
|
||||
"emailOrUsername": "Email or Username (case-sensitive)",
|
||||
"watchVideos": "Watch Videos",
|
||||
"work": "Work",
|
||||
"zelahQuote": "With [Habitica], I can be persuaded to go to bed on time by the thought of gaining points for an early night or losing health for a late one!",
|
||||
|
|
@ -259,9 +259,9 @@
|
|||
"altAttrSlack": "Slack",
|
||||
"missingAuthHeaders": "Missing authentication headers.",
|
||||
"missingAuthParams": "Missing authentication parameters.",
|
||||
"missingUsernameEmail": "Missing Login Name or email.",
|
||||
"missingUsernameEmail": "Missing username or email.",
|
||||
"missingEmail": "Missing email.",
|
||||
"missingUsername": "Missing Login Name.",
|
||||
"missingUsername": "Missing username.",
|
||||
"missingPassword": "Missing password.",
|
||||
"missingNewPassword": "Missing new password.",
|
||||
"invalidEmailDomain": "You cannot register with emails with the following domains: <%= domains %>",
|
||||
|
|
@ -287,9 +287,9 @@
|
|||
"passwordResetEmailSubject": "Password Reset for Habitica",
|
||||
"passwordResetEmailText": "If you requested a password reset for <%= username %> on Habitica, head to <%= passwordResetLink %> to set a new one. The link will expire after 24 hours. If you haven't requested a password reset, please ignore this email.",
|
||||
"passwordResetEmailHtml": "If you requested a password reset for <strong><%= username %></strong> on Habitica, <a href=\"<%= passwordResetLink %>\">click here</a> to set a new one. The link will expire after 24 hours.<br/><br>If you haven't requested a password reset, please ignore this email.",
|
||||
"invalidLoginCredentialsLong": "Uh-oh - your email address / login name or password is incorrect.\n- Make sure they are typed correctly. Your login name and password are case-sensitive.\n- You may have signed up with Facebook or Google-sign-in, not email so double-check by trying them.\n- If you forgot your password, click \"Forgot Password\".",
|
||||
"invalidLoginCredentialsLong": "Uh-oh - your email address / username or password is incorrect.\n- Make sure they are typed correctly. Your username and password are case-sensitive.\n- You may have signed up with Facebook or Google-sign-in, not email so double-check by trying them.\n- If you forgot your password, click \"Forgot Password\".",
|
||||
"invalidCredentials": "There is no account that uses those credentials.",
|
||||
"accountSuspended": "This account, User ID \"<%= userId %>\", has been blocked for breaking the [Community Guidelines](https://habitica.com/static/community-guidelines) or [Terms of Service](https://habitica.com/static/terms). For details or to ask to be unblocked, please email our Community Manager at <%= communityManagerEmail %> or ask your parent or guardian to email them. Please copy your User ID into the email and include your Profile Name.",
|
||||
"accountSuspended": "This account, User ID \"<%= userId %>\", has been blocked for breaking the [Community Guidelines](https://habitica.com/static/community-guidelines) or [Terms of Service](https://habitica.com/static/terms). For details or to ask to be unblocked, please email our Community Manager at <%= communityManagerEmail %> or ask your parent or guardian to email them. Please copy your User ID into the email and include your username.",
|
||||
"accountSuspendedTitle": "Account has been suspended",
|
||||
"unsupportedNetwork": "This network is not currently supported.",
|
||||
"cantDetachSocial": "Account lacks another authentication method; can't detach this authentication method.",
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@
|
|||
|
||||
"messageAuthPasswordMustMatch": ":password and :confirmPassword don't match",
|
||||
"messageAuthCredentialsRequired": ":username, :email, :password, :confirmPassword required",
|
||||
"messageAuthUsernameTaken": "Login Name already taken",
|
||||
"messageAuthUsernameTaken": "Username already taken",
|
||||
"messageAuthEmailTaken": "Email already taken",
|
||||
"messageAuthNoUserFound": "No user found.",
|
||||
"messageAuthMustBeLoggedIn": "You must be logged in.",
|
||||
|
|
|
|||
|
|
@ -54,13 +54,13 @@
|
|||
"misc": "Misc",
|
||||
"showHeader": "Show Header",
|
||||
"changePass": "Change Password",
|
||||
"changeUsername": "Change Login Name",
|
||||
"changeUsername": "Change Username",
|
||||
"changeEmail": "Change Email Address",
|
||||
"newEmail": "New Email Address",
|
||||
"oldPass": "Old Password",
|
||||
"newPass": "New Password",
|
||||
"confirmPass": "Confirm New Password",
|
||||
"newUsername": "New Login Name",
|
||||
"newUsername": "New Username",
|
||||
"dangerZone": "Danger Zone",
|
||||
"resetText1": "WARNING! This resets many parts of your account. This is highly discouraged, but some people find it useful in the beginning after playing with the site for a short time.",
|
||||
"resetText2": "You will lose all your levels, Gold, and Experience points. All your tasks (except those from challenges) will be deleted permanently and you will lose all of their historical data. You will lose all your equipment but you will be able to buy it all back, including all limited edition equipment or subscriber Mystery items that you already own (you will need to be in the correct class to re-buy class-specific gear). You will keep your current class and your pets and mounts. You might prefer to use an Orb of Rebirth instead, which is a much safer option and which will preserve your tasks and equipment.",
|
||||
|
|
@ -95,14 +95,15 @@
|
|||
"invalidPasswordResetCode": "The supplied password reset code is invalid or has expired.",
|
||||
"passwordChangeSuccess": "Your password was successfully changed to the one you just chose. You can now use it to access your account.",
|
||||
"passwordSuccess": "Password successfully changed",
|
||||
"usernameSuccess": "Login Name successfully changed",
|
||||
"usernameSuccess": "Username successfully changed",
|
||||
"displayNameSuccess": "Display name successfully changed",
|
||||
"emailSuccess": "Email successfully changed",
|
||||
"detachSocial": "De-register <%= network %>",
|
||||
"detachedSocial": "Successfully removed <%= network %> authentication from your account",
|
||||
"addedLocalAuth": "Successfully added local authentication",
|
||||
"data": "Data",
|
||||
"exportData": "Export Data",
|
||||
"usernameOrEmail": "Login Name or Email",
|
||||
"usernameOrEmail": "Username or Email",
|
||||
"email": "Email",
|
||||
"registerWithSocial": "Register with <%= network %>",
|
||||
"registeredWithSocial": "Registered with <%= network %>",
|
||||
|
|
@ -151,7 +152,7 @@
|
|||
"couponText": "We sometimes have events and give out coupon codes for special gear. (eg, those who stop by our Wondercon booth)",
|
||||
"saveCustomDayStart": "Save Custom Day Start",
|
||||
"registration": "Registration",
|
||||
"addLocalAuth": "Add local authentication:",
|
||||
"addLocalAuth": "Add Email and Password Login",
|
||||
"generateCodes": "Generate Codes",
|
||||
"generate": "Generate",
|
||||
"getCodes": "Get Codes",
|
||||
|
|
@ -189,5 +190,18 @@
|
|||
"timezoneUTC": "Habitica uses the time zone set on your PC, which is: <strong><%= utc %></strong>",
|
||||
"timezoneInfo": "If that time zone is wrong, first reload this page using your browser's reload or refresh button to ensure that Habitica has the most recent information. If it is still wrong, adjust the time zone on your PC and then reload this page again.<br><br> <strong>If you use Habitica on other PCs or mobile devices, the time zone must be the same on them all.</strong> If your Dailies have been resetting at the wrong time, repeat this check on all other PCs and on a browser on your mobile devices.",
|
||||
"push": "Push",
|
||||
"about": "About"
|
||||
"about": "About",
|
||||
"setUsernameNotificationTitle": "Confirm your username!",
|
||||
"setUsernameNotificationBody": "We will be transitioning login names to unique, public usernames soon. This username will be used for invitations, @mentions in chat, and messaging.",
|
||||
"usernameIssueSlur": "Usernames may not contain inapporpriate language.",
|
||||
"usernameIssueForbidden": "Usernames may not contain restricted words.",
|
||||
"usernameIssueLength": "Usernames must be between 1 and 20 characters.",
|
||||
"usernameIssueInvalidCharacters": "Usernames can only contain letters, numbers and underscores.",
|
||||
"currentUsername": "Current username:",
|
||||
"displaynameIssueLength": "Display Names must be between 1 and 30 characters.",
|
||||
"displaynameIssueSlur": "Display Names may not contain inappropriate language",
|
||||
"goToSettings": "Go to Settings",
|
||||
"usernameVerifiedConfirmation": "Your username, <%= username %>, is confirmed!",
|
||||
"usernameNotVerified": "Please confirm your username.",
|
||||
"changeUsernameDisclaimer": "We will be transitioning login names to unique, public usernames soon. This username will be used for invitations, @mentions in chat, and messaging."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,46 +1,35 @@
|
|||
import validator from 'validator';
|
||||
import moment from 'moment';
|
||||
import passport from 'passport';
|
||||
import nconf from 'nconf';
|
||||
|
||||
import {
|
||||
authWithHeaders,
|
||||
} from '../../middlewares/auth';
|
||||
import { model as User } from '../../models/user';
|
||||
import common from '../../../common';
|
||||
import {
|
||||
NotAuthorized,
|
||||
BadRequest,
|
||||
NotFound,
|
||||
} from '../../libs/errors';
|
||||
import * as passwordUtils from '../../libs/password';
|
||||
import { model as User } from '../../models/user';
|
||||
import { model as EmailUnsubscription } from '../../models/emailUnsubscription';
|
||||
import { sendTxn as sendTxnEmail } from '../../libs/email';
|
||||
import { send as sendEmail } from '../../libs/email';
|
||||
import pusher from '../../libs/pusher';
|
||||
import common from '../../../common';
|
||||
import { validatePasswordResetCodeAndFindUser, convertToBcrypt} from '../../libs/password';
|
||||
import { encrypt } from '../../libs/encryption';
|
||||
import * as authLib from '../../libs/auth';
|
||||
import {
|
||||
loginRes,
|
||||
hasBackupAuth,
|
||||
hasLocalAuth,
|
||||
loginSocial,
|
||||
registerLocal,
|
||||
} from '../../libs/auth';
|
||||
|
||||
const BASE_URL = nconf.get('BASE_URL');
|
||||
const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL');
|
||||
const COMMUNITY_MANAGER_EMAIL = nconf.get('EMAILS:COMMUNITY_MANAGER_EMAIL');
|
||||
|
||||
let api = {};
|
||||
|
||||
function hasBackupAuth (user, networkToRemove) {
|
||||
if (user.auth.local.username) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let hasAlternateNetwork = common.constants.SUPPORTED_SOCIAL_NETWORKS.find((network) => {
|
||||
return network.key !== networkToRemove && user.auth[network.key].id;
|
||||
});
|
||||
|
||||
return hasAlternateNetwork;
|
||||
}
|
||||
|
||||
/* NOTE this route has also an API v4 version */
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/user/auth/local/register Register
|
||||
* @apiDescription Register a new user with email, login name, and password or attach local auth to a social user
|
||||
|
|
@ -61,15 +50,10 @@ api.registerLocal = {
|
|||
})],
|
||||
url: '/user/auth/local/register',
|
||||
async handler (req, res) {
|
||||
await authLib.registerLocal(req, res, { isV3: true });
|
||||
await registerLocal(req, res, { isV3: true });
|
||||
},
|
||||
};
|
||||
|
||||
function _loginRes (user, req, res) {
|
||||
if (user.auth.blocked) throw new NotAuthorized(res.t('accountSuspended', {communityManagerEmail: COMMUNITY_MANAGER_EMAIL, userId: user._id}));
|
||||
return res.respond(200, {id: user._id, apiToken: user.apiToken, newUser: user.newUser || false});
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/user/auth/local/login Login
|
||||
* @apiDescription Login a user with email / username and password
|
||||
|
|
@ -141,101 +125,19 @@ api.loginLocal = {
|
|||
headers: req.headers,
|
||||
});
|
||||
|
||||
return _loginRes(user, ...arguments);
|
||||
return loginRes(user, ...arguments);
|
||||
},
|
||||
};
|
||||
|
||||
function _passportProfile (network, accessToken) {
|
||||
return new Promise((resolve, reject) => {
|
||||
passport._strategies[network].userProfile(accessToken, (err, profile) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(profile);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Called as a callback by Facebook (or other social providers). Internal route
|
||||
api.loginSocial = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders({
|
||||
optional: true,
|
||||
})],
|
||||
url: '/user/auth/social', // this isn't the most appropriate url but must be the same as v2
|
||||
url: '/user/auth/social',
|
||||
async handler (req, res) {
|
||||
let existingUser = res.locals.user;
|
||||
let accessToken = req.body.authResponse.access_token;
|
||||
let network = req.body.network;
|
||||
|
||||
let isSupportedNetwork = common.constants.SUPPORTED_SOCIAL_NETWORKS.find(supportedNetwork => {
|
||||
return supportedNetwork.key === network;
|
||||
});
|
||||
if (!isSupportedNetwork) throw new BadRequest(res.t('unsupportedNetwork'));
|
||||
|
||||
let profile = await _passportProfile(network, accessToken);
|
||||
|
||||
let user = await User.findOne({
|
||||
[`auth.${network}.id`]: profile.id,
|
||||
}, {_id: 1, apiToken: 1, auth: 1}).exec();
|
||||
|
||||
// User already signed up
|
||||
if (user) {
|
||||
_loginRes(user, ...arguments);
|
||||
} else { // Create new user
|
||||
user = {
|
||||
auth: {
|
||||
[network]: {
|
||||
id: profile.id,
|
||||
emails: profile.emails,
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
name: profile.displayName || profile.name || profile.username,
|
||||
},
|
||||
preferences: {
|
||||
language: req.language,
|
||||
},
|
||||
};
|
||||
if (existingUser) {
|
||||
existingUser.auth[network] = user.auth[network];
|
||||
user = existingUser;
|
||||
} else {
|
||||
user = new User(user);
|
||||
user.registeredThrough = req.headers['x-client']; // Not saved, used to create the correct tasks based on the device used
|
||||
}
|
||||
|
||||
let savedUser = await user.save();
|
||||
|
||||
if (!existingUser) {
|
||||
user.newUser = true;
|
||||
}
|
||||
_loginRes(user, ...arguments);
|
||||
|
||||
// Clean previous email preferences
|
||||
if (savedUser.auth[network].emails && savedUser.auth[network].emails[0] && savedUser.auth[network].emails[0].value) {
|
||||
EmailUnsubscription
|
||||
.remove({email: savedUser.auth[network].emails[0].value.toLowerCase()})
|
||||
.exec()
|
||||
.then(() => {
|
||||
if (!existingUser) sendTxnEmail(savedUser, 'welcome');
|
||||
}); // eslint-disable-line max-nested-callbacks
|
||||
}
|
||||
|
||||
if (!existingUser) {
|
||||
res.analytics.track('register', {
|
||||
category: 'acquisition',
|
||||
type: network,
|
||||
gaLabel: network,
|
||||
uuid: savedUser._id,
|
||||
headers: req.headers,
|
||||
user: savedUser,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
return await loginSocial(req, res);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -338,7 +240,7 @@ api.updateUsername = {
|
|||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
if (!user.auth.local.username) throw new BadRequest(res.t('userHasNoLocalRegistration'));
|
||||
if (!hasLocalAuth(user)) throw new BadRequest(res.t('userHasNoLocalRegistration'));
|
||||
|
||||
let password = req.body.password;
|
||||
let isValidPassword = await passwordUtils.compare(user, password);
|
||||
|
|
|
|||
|
|
@ -45,8 +45,11 @@ api.getNews = {
|
|||
<div class="promo_forest_friends_bundle center-block"></div>
|
||||
<h3>Last Chance for Forest Friends Quest Bundle</h3>
|
||||
<p>This is also the final week to buy the discounted Forest Friends Pet Quest Bundle, featuring the Deer, Hedgehog, and Treeling quests all for seven Gems! Be sure to grab this bundle from the <a href='/shops/quests' target='_blank'>Quest Shop</a> before it scampers into the underbrush!</p>
|
||||
<p>After they're gone, it will be at least a year before the Ghost or Glow-in-the-Dark Hatching Potions are available again, so be sure to get them now!</p>
|
||||
<div class="small">by Beffymaroo and SabreCat</div>
|
||||
<div class="small">Art by Uncommon Criminal, InspectorCaracal, Leephon, aurakami, FuzzyTrees, PainterProphet, and plumilla</div>
|
||||
<h3>Blog Post: Contributing to Habitica</h3>
|
||||
<p>This month's <a href='https://habitica.wordpress.com/2018/09/19/contributing-to-habitica/' target='_blank'>featured Wiki article</a> is about Contributing to Habitica. We hope that it will help you get involved with our open-source project and our great community! Be sure to check it out, and let us know what you think by reaching out on <a href='https://twitter.com/habitica' target='_blank'>Twitter</a>, <a href='http://blog.habitrpg.com' target='_blank'>Tumblr</a>, and <a href='https://facebook.com/habitica' target='_blank'>Facebook</a>.</p>
|
||||
<div class="small mb-3">Writing by Daniel the Bard, Flutter Bee, and Lemoness</div>
|
||||
</div>
|
||||
`,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,100 @@
|
|||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import {
|
||||
authWithHeaders,
|
||||
} from '../../middlewares/auth';
|
||||
import * as authLib from '../../libs/auth';
|
||||
import {
|
||||
NotAuthorized,
|
||||
BadRequest,
|
||||
} from '../../libs/errors';
|
||||
import * as passwordUtils from '../../libs/password';
|
||||
import { model as User } from '../../models/user';
|
||||
import {verifyUsername} from '../../libs/user/validation';
|
||||
|
||||
const api = {};
|
||||
|
||||
/**
|
||||
* @api {put} /api/v4/user/auth/update-username Update username
|
||||
* @apiDescription Update the username of a local user
|
||||
* @apiName UpdateUsername
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiParam (Body) {String} username The new username
|
||||
|
||||
* @apiSuccess {String} data.username The new username
|
||||
**/
|
||||
api.updateUsername = {
|
||||
method: 'PUT',
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/auth/update-username',
|
||||
async handler (req, res) {
|
||||
const user = res.locals.user;
|
||||
|
||||
req.checkBody({
|
||||
username: {
|
||||
notEmpty: {errorMessage: res.t('missingUsername')},
|
||||
},
|
||||
});
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
const newUsername = req.body.username;
|
||||
|
||||
const issues = verifyUsername(newUsername, res);
|
||||
if (issues.length > 0) throw new BadRequest(issues.join(' '));
|
||||
|
||||
const password = req.body.password;
|
||||
if (password !== undefined) {
|
||||
let isValidPassword = await passwordUtils.compare(user, password);
|
||||
if (!isValidPassword) throw new NotAuthorized(res.t('wrongPassword'));
|
||||
}
|
||||
|
||||
const existingUser = await User.findOne({ 'auth.local.lowerCaseUsername': newUsername.toLowerCase() }, {auth: 1}).exec();
|
||||
if (existingUser !== undefined && existingUser !== null && existingUser._id !== user._id) {
|
||||
throw new BadRequest(res.t('usernameTaken'));
|
||||
}
|
||||
|
||||
// if password is using old sha1 encryption, change it
|
||||
if (user.auth.local.passwordHashMethod === 'sha1' && password !== undefined) {
|
||||
await passwordUtils.convertToBcrypt(user, password); // user is saved a few lines below
|
||||
}
|
||||
|
||||
// save username
|
||||
user.auth.local.lowerCaseUsername = newUsername.toLowerCase();
|
||||
user.auth.local.username = newUsername;
|
||||
user.flags.verifiedUsername = true;
|
||||
await user.save();
|
||||
|
||||
res.respond(200, { username: req.body.username });
|
||||
},
|
||||
};
|
||||
|
||||
api.verifyUsername = {
|
||||
method: 'POST',
|
||||
url: '/user/auth/verify-username',
|
||||
async handler (req, res) {
|
||||
req.checkBody({
|
||||
username: {
|
||||
notEmpty: {errorMessage: res.t('missingUsername')},
|
||||
},
|
||||
});
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
const issues = verifyUsername(req.body.username, res);
|
||||
|
||||
const count = await User.count({ 'auth.local.lowerCaseUsername': req.body.username.toLowerCase() });
|
||||
if (count > 0) issues.push(res.t('usernameTaken'));
|
||||
|
||||
if (issues.length > 0) {
|
||||
res.respond(200, { isUsable: false, issues });
|
||||
} else {
|
||||
res.respond(200, { isUsable: true });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
* NOTE most user routes are still in the v3 controller
|
||||
* here there are only routes that had to be split from the v3 version because of
|
||||
|
|
@ -35,4 +127,4 @@ api.registerLocal = {
|
|||
},
|
||||
};
|
||||
|
||||
module.exports = api;
|
||||
module.exports = api;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
NotFound,
|
||||
} from '../../libs/errors';
|
||||
|
|
@ -11,6 +12,9 @@ import logger from '../../libs/logger';
|
|||
import { decrypt } from '../../libs/encryption';
|
||||
import { model as Group } from '../../models/group';
|
||||
import moment from 'moment';
|
||||
import { loginSocial } from './social.js';
|
||||
import { loginRes } from './utils';
|
||||
import { verifyUsername } from '../user/validation';
|
||||
|
||||
const USERNAME_LENGTH_MIN = 1;
|
||||
const USERNAME_LENGTH_MAX = 20;
|
||||
|
|
@ -51,7 +55,23 @@ async function _handleGroupInvitation (user, invite) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function registerLocal (req, res, { isV3 = false }) {
|
||||
function hasLocalAuth (user) {
|
||||
return user.auth.local.email && user.auth.local.hashed_password;
|
||||
}
|
||||
|
||||
function hasBackupAuth (user, networkToRemove) {
|
||||
if (hasLocalAuth(user)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let hasAlternateNetwork = common.constants.SUPPORTED_SOCIAL_NETWORKS.find((network) => {
|
||||
return network.key !== networkToRemove && user.auth[network.key].id;
|
||||
});
|
||||
|
||||
return hasAlternateNetwork;
|
||||
}
|
||||
|
||||
async function registerLocal (req, res, { isV3 = false }) {
|
||||
const existingUser = res.locals.user; // If adding local auth to social user
|
||||
|
||||
req.checkBody({
|
||||
|
|
@ -77,6 +97,9 @@ export async function registerLocal (req, res, { isV3 = false }) {
|
|||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
const issues = verifyUsername(req.body.username, res);
|
||||
if (issues.length > 0) throw new BadRequest(issues.join(' '));
|
||||
|
||||
let { email, username, password } = req.body;
|
||||
|
||||
// Get the lowercase version of username to check that we do not have duplicates
|
||||
|
|
@ -132,6 +155,8 @@ export async function registerLocal (req, res, { isV3 = false }) {
|
|||
await _handleGroupInvitation(newUser, req.query.groupInvite || req.query.partyInvite);
|
||||
}
|
||||
|
||||
newUser.flags.verifiedUsername = true;
|
||||
|
||||
let savedUser = await newUser.save();
|
||||
|
||||
let userToJSON;
|
||||
|
|
@ -168,4 +193,12 @@ export async function registerLocal (req, res, { isV3 = false }) {
|
|||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loginRes,
|
||||
hasBackupAuth,
|
||||
hasLocalAuth,
|
||||
loginSocial,
|
||||
registerLocal,
|
||||
};
|
||||
|
|
|
|||
108
website/server/libs/auth/social.js
Normal file
108
website/server/libs/auth/social.js
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import passport from 'passport';
|
||||
import common from '../../../common';
|
||||
import { BadRequest } from '../errors';
|
||||
import {
|
||||
generateUsername,
|
||||
loginRes,
|
||||
} from './utils';
|
||||
import { model as User } from '../../models/user';
|
||||
import { model as EmailUnsubscription } from '../../models/emailUnsubscription';
|
||||
import { sendTxn as sendTxnEmail } from '../email';
|
||||
|
||||
function _passportProfile (network, accessToken) {
|
||||
return new Promise((resolve, reject) => {
|
||||
passport._strategies[network].userProfile(accessToken, (err, profile) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(profile);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function loginSocial (req, res) {
|
||||
const existingUser = res.locals.user;
|
||||
const accessToken = req.body.authResponse.access_token;
|
||||
const network = req.body.network;
|
||||
|
||||
const isSupportedNetwork = common.constants.SUPPORTED_SOCIAL_NETWORKS.find(supportedNetwork => {
|
||||
return supportedNetwork.key === network;
|
||||
});
|
||||
if (!isSupportedNetwork) throw new BadRequest(res.t('unsupportedNetwork'));
|
||||
|
||||
const profile = await _passportProfile(network, accessToken);
|
||||
|
||||
let user = await User.findOne({
|
||||
[`auth.${network}.id`]: profile.id,
|
||||
}, {_id: 1, apiToken: 1, auth: 1}).exec();
|
||||
|
||||
// User already signed up
|
||||
if (user) {
|
||||
return loginRes(user, ...arguments);
|
||||
}
|
||||
|
||||
const generatedUsername = generateUsername();
|
||||
|
||||
user = {
|
||||
auth: {
|
||||
[network]: {
|
||||
id: profile.id,
|
||||
emails: profile.emails,
|
||||
},
|
||||
local: {
|
||||
username: generatedUsername,
|
||||
lowerCaseUsername: generatedUsername,
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
name: profile.displayName || profile.name || profile.username,
|
||||
},
|
||||
preferences: {
|
||||
language: req.language,
|
||||
},
|
||||
};
|
||||
|
||||
if (existingUser) {
|
||||
existingUser.auth[network] = user.auth[network];
|
||||
user = existingUser;
|
||||
} else {
|
||||
user = new User(user);
|
||||
user.registeredThrough = req.headers['x-client']; // Not saved, used to create the correct tasks based on the device used
|
||||
}
|
||||
|
||||
const savedUser = await user.save();
|
||||
|
||||
if (!existingUser) {
|
||||
user.newUser = true;
|
||||
}
|
||||
|
||||
loginRes(user, ...arguments);
|
||||
|
||||
// Clean previous email preferences
|
||||
if (savedUser.auth[network].emails && savedUser.auth[network].emails[0] && savedUser.auth[network].emails[0].value) {
|
||||
EmailUnsubscription
|
||||
.remove({email: savedUser.auth[network].emails[0].value.toLowerCase()})
|
||||
.exec()
|
||||
.then(() => {
|
||||
if (!existingUser) sendTxnEmail(savedUser, 'welcome');
|
||||
}); // eslint-disable-line max-nested-callbacks
|
||||
}
|
||||
|
||||
if (!existingUser) {
|
||||
res.analytics.track('register', {
|
||||
category: 'acquisition',
|
||||
type: network,
|
||||
gaLabel: network,
|
||||
uuid: savedUser._id,
|
||||
headers: req.headers,
|
||||
user: savedUser,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loginSocial,
|
||||
};
|
||||
30
website/server/libs/auth/utils.js
Normal file
30
website/server/libs/auth/utils.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import nconf from 'nconf';
|
||||
import shortid from 'short-uuid';
|
||||
|
||||
import { NotAuthorized } from '../errors';
|
||||
|
||||
const COMMUNITY_MANAGER_EMAIL = nconf.get('EMAILS:COMMUNITY_MANAGER_EMAIL');
|
||||
const translator = shortid('0123456789abcdefghijklmnopqrstuvwxyz');
|
||||
|
||||
function generateUsername () {
|
||||
let newName = `hb-${translator.new()}`;
|
||||
return newName.substring(0,20);
|
||||
}
|
||||
|
||||
function loginRes (user, req, res) {
|
||||
if (user.auth.blocked) throw new NotAuthorized(res.t('accountSuspended', {communityManagerEmail: COMMUNITY_MANAGER_EMAIL, userId: user._id}));
|
||||
|
||||
const responseData = {
|
||||
id: user._id,
|
||||
apiToken: user.apiToken,
|
||||
newUser: user.newUser || false,
|
||||
username: user.auth.local.username,
|
||||
};
|
||||
|
||||
return res.respond(200, responseData);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateUsername,
|
||||
loginRes,
|
||||
};
|
||||
20
website/server/libs/forbiddenUsernames.js
Normal file
20
website/server/libs/forbiddenUsernames.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* eslint-disable no-multiple-empty-lines */
|
||||
|
||||
// This file contains usernames that we do not want users to use, because they give the account more legitimacy and may deceive users.
|
||||
let bannedWords = [
|
||||
'TESTPLACEHOLDERSWEARWORDHERE',
|
||||
'TESTPLACEHOLDERSWEARWORDHERE1',
|
||||
|
||||
|
||||
|
||||
|
||||
'support',
|
||||
'habitica',
|
||||
'habitrpg',
|
||||
'admin',
|
||||
'administrator',
|
||||
'moderator',
|
||||
'gryphon',
|
||||
];
|
||||
|
||||
module.exports = bannedWords;
|
||||
|
|
@ -6,6 +6,8 @@ import {
|
|||
NotAuthorized,
|
||||
} from '../../libs/errors';
|
||||
import { model as User } from '../../models/user';
|
||||
import {nameContainsSlur} from './validation';
|
||||
|
||||
|
||||
export async function get (req, res, { isV3 = false }) {
|
||||
const user = res.locals.user;
|
||||
|
|
@ -108,6 +110,13 @@ export async function update (req, res, { isV3 = false }) {
|
|||
|
||||
let promisesForTagsRemoval = [];
|
||||
|
||||
if (req.body['profile.name'] !== undefined) {
|
||||
const newName = req.body['profile.name'];
|
||||
if (newName === null) throw new BadRequest(res.t('invalidReqParams'));
|
||||
if (newName.length > 30) throw new BadRequest(res.t('displaynameIssueLength'));
|
||||
if (nameContainsSlur(newName)) throw new BadRequest(res.t('displaynameIssueSlur'));
|
||||
}
|
||||
|
||||
_.each(req.body, (val, key) => {
|
||||
let purchasable = requiresPurchase[key];
|
||||
|
||||
|
|
@ -239,4 +248,4 @@ export async function rebirth (req, res, { isV3 = false }) {
|
|||
await Promise.all(toSave);
|
||||
|
||||
res.respond(200, ...rebirthRes);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
37
website/server/libs/user/validation.js
Normal file
37
website/server/libs/user/validation.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import bannedSlurs from '../bannedSlurs';
|
||||
import {getMatchesByWordArray} from '../stringUtils';
|
||||
import forbiddenUsernames from '../forbiddenUsernames';
|
||||
|
||||
const bannedSlurRegexs = bannedSlurs.map((word) => new RegExp(`.*${word}.*`, 'i'));
|
||||
|
||||
export function nameContainsSlur (username) {
|
||||
for (let i = 0; i < bannedSlurRegexs.length; i += 1) {
|
||||
const regEx = bannedSlurRegexs[i];
|
||||
const match = username.match(regEx);
|
||||
if (match !== null && match[0] !== null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function usernameIsForbidden (username) {
|
||||
const forbidddenWordsMatched = getMatchesByWordArray(username, forbiddenUsernames);
|
||||
return forbidddenWordsMatched.length > 0;
|
||||
}
|
||||
|
||||
const invalidCharsRegex = new RegExp('[^a-z0-9_-]', 'i');
|
||||
function usernameContainsInvalidCharacters (username) {
|
||||
let match = username.match(invalidCharsRegex);
|
||||
return match !== null && match[0] !== null;
|
||||
}
|
||||
|
||||
export function verifyUsername (username, res) {
|
||||
let issues = [];
|
||||
if (username.length < 1 || username.length > 20) issues.push(res.t('usernameIssueLength'));
|
||||
if (usernameContainsInvalidCharacters(username)) issues.push(res.t('usernameIssueInvalidCharacters'));
|
||||
if (nameContainsSlur(username)) issues.push(res.t('usernameIssueSlur'));
|
||||
if (usernameIsForbidden(username)) issues.push(res.t('usernameIssueForbidden'));
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
|
@ -33,6 +33,8 @@ app.use('/api/v3', v3Router);
|
|||
|
||||
// A list of v3 routes in the format METHOD-URL to skip
|
||||
const v4RouterOverrides = [
|
||||
// 'GET-/status', Example to override the GET /status api call
|
||||
'PUT-/user/auth/update-username',
|
||||
'POST-/user/auth/local/register',
|
||||
'GET-/user',
|
||||
'PUT-/user',
|
||||
|
|
|
|||
|
|
@ -241,6 +241,7 @@ let schema = new Schema({
|
|||
armoireEmpty: {type: Boolean, default: false},
|
||||
cardReceived: {type: Boolean, default: false},
|
||||
warnedLowHealth: {type: Boolean, default: false},
|
||||
verifiedUsername: {type: Boolean, default: false},
|
||||
},
|
||||
|
||||
history: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue