mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 06:37:23 +01:00
reset the ApiToken on password changes/resets (#15433)
* reset the ApiToken on password changes/resets * fix/add tests * fix(typo): test grammar * update new API Token Strings, removed unused one --------- Co-authored-by: Kalista Payne <sabrecat@gmail.com>
This commit is contained in:
@@ -238,6 +238,28 @@ describe('POST /user/auth/reset-password-set-new-one', () => {
|
||||
expect(isPassValid).to.equal(true);
|
||||
});
|
||||
|
||||
it('changes the apiToken on password reset', async () => {
|
||||
const user = await generateUser();
|
||||
const previousToken = user.apiToken;
|
||||
|
||||
const code = encrypt(JSON.stringify({
|
||||
userId: user._id,
|
||||
expiresAt: moment().add({ days: 1 }),
|
||||
}));
|
||||
await user.updateOne({
|
||||
'auth.local.passwordResetCode': code,
|
||||
});
|
||||
|
||||
await api.post(`${endpoint}`, {
|
||||
newPassword: 'my new password',
|
||||
confirmPassword: 'my new password',
|
||||
code,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.apiToken).to.not.eql(previousToken);
|
||||
});
|
||||
|
||||
it('renders the success page and convert the password from sha1 to bcrypt', async () => {
|
||||
const user = await generateUser();
|
||||
|
||||
|
||||
@@ -27,11 +27,30 @@ describe('PUT /user/auth/update-password', async () => {
|
||||
newPassword,
|
||||
confirmPassword: newPassword,
|
||||
});
|
||||
expect(response).to.eql({});
|
||||
|
||||
expect(response).to.exist;
|
||||
expect(response.apiToken).to.exist;
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.local.hashed_password).to.not.eql(previousHashedPassword);
|
||||
});
|
||||
|
||||
it('should change the apiToken on password change', async () => {
|
||||
const previousToken = user.apiToken;
|
||||
const response = await user.put(ENDPOINT, {
|
||||
password,
|
||||
newPassword,
|
||||
confirmPassword: newPassword,
|
||||
});
|
||||
|
||||
const newToken = response.apiToken;
|
||||
expect(newToken).to.exist;
|
||||
|
||||
await user.sync();
|
||||
expect(user.apiToken).to.eql(newToken);
|
||||
expect(user.apiToken).to.not.eql(previousToken);
|
||||
});
|
||||
|
||||
it('returns an error when confirmPassword does not match newPassword', async () => {
|
||||
await expect(user.put(ENDPOINT, {
|
||||
password,
|
||||
|
||||
@@ -111,6 +111,7 @@ import axios from 'axios';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { mapState } from '@/libs/store';
|
||||
import snackbars from '@/components/snackbars/notifications';
|
||||
import { LOCALSTORAGE_AUTH_KEY } from '@/libs/auth';
|
||||
|
||||
const COMMUNITY_MANAGER_EMAIL = import.meta.env.EMAILS_COMMUNITY_MANAGER_EMAIL;
|
||||
|
||||
@@ -280,7 +281,7 @@ export default {
|
||||
this.loading = false;
|
||||
},
|
||||
checkForBannedUser (error) {
|
||||
const AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings');
|
||||
const AUTH_SETTINGS = localStorage.getItem(LOCALSTORAGE_AUTH_KEY);
|
||||
const parseSettings = JSON.parse(AUTH_SETTINGS);
|
||||
const errorMessage = error.response.data.message;
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
<script>
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import { LOCALSTORAGE_AUTH_KEY } from '@/libs/auth';
|
||||
|
||||
const COMMUNITY_MANAGER_EMAIL = import.meta.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
|
||||
|
||||
@@ -39,7 +40,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
bannedMessage () {
|
||||
const AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings');
|
||||
const AUTH_SETTINGS = localStorage.getItem(LOCALSTORAGE_AUTH_KEY);
|
||||
const parseSettings = JSON.parse(AUTH_SETTINGS);
|
||||
const userId = parseSettings ? parseSettings.auth.apiId : '';
|
||||
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import axios from 'axios';
|
||||
import moment from 'moment';
|
||||
|
||||
export const LOCALSTORAGE_AUTH_KEY = 'habit-mobile-settings';
|
||||
|
||||
export function authAsCredentialsState (authObject) {
|
||||
return {
|
||||
API_ID: authObject.auth.apiId,
|
||||
API_TOKEN: authObject.auth.apiToken,
|
||||
};
|
||||
}
|
||||
|
||||
export function setUpAxios (AUTH_SETTINGS) { // eslint-disable-line import/prefer-default-export
|
||||
if (!AUTH_SETTINGS) {
|
||||
AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings'); // eslint-disable-line no-param-reassign, max-len
|
||||
AUTH_SETTINGS = localStorage.getItem(LOCALSTORAGE_AUTH_KEY); // eslint-disable-line no-param-reassign, max-len
|
||||
if (!AUTH_SETTINGS) return false;
|
||||
AUTH_SETTINGS = JSON.parse(AUTH_SETTINGS); // eslint-disable-line no-param-reassign
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { LOCALSTORAGE_AUTH_KEY } from '@/libs/auth';
|
||||
|
||||
// @TODO: I have abstracted this in another PR. Use that function when merged
|
||||
function getApiKey () {
|
||||
let AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings');
|
||||
let AUTH_SETTINGS = localStorage.getItem(LOCALSTORAGE_AUTH_KEY);
|
||||
|
||||
if (AUTH_SETTINGS) {
|
||||
AUTH_SETTINGS = JSON.parse(AUTH_SETTINGS);
|
||||
|
||||
@@ -158,7 +158,14 @@ export default {
|
||||
confirmPassword: this.passwordUpdates.confirmPassword,
|
||||
};
|
||||
|
||||
await axios.put('/api/v4/user/auth/update-password', localAuthData);
|
||||
const updatePasswordResult = await axios.put('/api/v4/user/auth/update-password', localAuthData);
|
||||
|
||||
const newToken = updatePasswordResult.data.data.apiToken;
|
||||
|
||||
this.$store.dispatch('auth:setNewToken', {
|
||||
userId: this.user._id,
|
||||
apiToken: newToken,
|
||||
});
|
||||
|
||||
this.passwordUpdates = {};
|
||||
this.$store.dispatch('snackbars:add', {
|
||||
|
||||
@@ -147,8 +147,6 @@ import {
|
||||
const bugReportModal = () => import('@/components/bugReportModal');
|
||||
const bugReportSuccessModal = () => import('@/components/bugReportSuccessModal');
|
||||
|
||||
const COMMUNITY_MANAGER_EMAIL = import.meta.env.EMAILS_COMMUNITY_MANAGER_EMAIL;
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
@@ -325,29 +323,6 @@ export default {
|
||||
if (loadingScreen) document.body.removeChild(loadingScreen);
|
||||
},
|
||||
methods: {
|
||||
checkForBannedUser (error) {
|
||||
const AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings');
|
||||
const parseSettings = JSON.parse(AUTH_SETTINGS);
|
||||
const errorMessage = error.response.data.message;
|
||||
|
||||
// Case where user is not logged in
|
||||
if (!parseSettings) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bannedMessage = this.$t('accountSuspended', {
|
||||
communityManagerEmail: COMMUNITY_MANAGER_EMAIL,
|
||||
userId: parseSettings.auth.apiId,
|
||||
});
|
||||
|
||||
if (errorMessage !== bannedMessage) return false;
|
||||
|
||||
this.$store.dispatch('auth:logout', { redirectToLogin: true });
|
||||
return true;
|
||||
},
|
||||
itemSelected (item) {
|
||||
this.selectedItemToBuy = item;
|
||||
},
|
||||
genericPurchase (item) {
|
||||
if (!item) return false;
|
||||
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
import axios from 'axios';
|
||||
import { authAsCredentialsState, LOCALSTORAGE_AUTH_KEY } from '@/libs/auth';
|
||||
|
||||
const LOCALSTORAGE_AUTH_KEY = 'habit-mobile-settings';
|
||||
function saveLocalDataAuth (store, apiId, apiToken) {
|
||||
const credentialsObj = {
|
||||
auth: {
|
||||
apiId,
|
||||
apiToken,
|
||||
},
|
||||
};
|
||||
|
||||
const userLocalData = JSON.stringify(credentialsObj);
|
||||
|
||||
localStorage.setItem(LOCALSTORAGE_AUTH_KEY, userLocalData);
|
||||
|
||||
store.state.credentials = authAsCredentialsState(credentialsObj);
|
||||
}
|
||||
|
||||
export async function register (store, params) {
|
||||
let url = '/api/v4/user/auth/local/register';
|
||||
@@ -16,13 +30,7 @@ export async function register (store, params) {
|
||||
|
||||
const user = result.data.data;
|
||||
|
||||
const userLocalData = JSON.stringify({
|
||||
auth: {
|
||||
apiId: user._id,
|
||||
apiToken: user.apiToken,
|
||||
},
|
||||
});
|
||||
localStorage.setItem(LOCALSTORAGE_AUTH_KEY, userLocalData);
|
||||
saveLocalDataAuth(store, user.id, user.apiToken);
|
||||
}
|
||||
|
||||
export async function login (store, params) {
|
||||
@@ -35,14 +43,7 @@ export async function login (store, params) {
|
||||
|
||||
const user = result.data.data;
|
||||
|
||||
const userLocalData = JSON.stringify({
|
||||
auth: {
|
||||
apiId: user.id,
|
||||
apiToken: user.apiToken,
|
||||
},
|
||||
});
|
||||
|
||||
localStorage.setItem(LOCALSTORAGE_AUTH_KEY, userLocalData);
|
||||
saveLocalDataAuth(store, user.id, user.apiToken);
|
||||
}
|
||||
|
||||
export async function verifyUsername (store, params) {
|
||||
@@ -72,14 +73,7 @@ export async function socialAuth (store, params) {
|
||||
|
||||
const user = result.data.data;
|
||||
|
||||
const userLocalData = JSON.stringify({
|
||||
auth: {
|
||||
apiId: user.id,
|
||||
apiToken: user.apiToken,
|
||||
},
|
||||
});
|
||||
|
||||
localStorage.setItem(LOCALSTORAGE_AUTH_KEY, userLocalData);
|
||||
saveLocalDataAuth(store, user.id, user.apiToken);
|
||||
}
|
||||
|
||||
export async function appleAuth (store, params) {
|
||||
@@ -93,14 +87,7 @@ export async function appleAuth (store, params) {
|
||||
|
||||
const user = result.data.data;
|
||||
|
||||
const userLocalData = JSON.stringify({
|
||||
auth: {
|
||||
apiId: user.id,
|
||||
apiToken: user.apiToken,
|
||||
},
|
||||
});
|
||||
|
||||
localStorage.setItem(LOCALSTORAGE_AUTH_KEY, userLocalData);
|
||||
saveLocalDataAuth(store, user.id, user.apiToken);
|
||||
}
|
||||
|
||||
export function logout (store, options = {}) {
|
||||
@@ -108,3 +95,7 @@ export function logout (store, options = {}) {
|
||||
const query = options.redirectToLogin === true ? '?redirectToLogin=true' : '';
|
||||
window.location.href = `/logout-server${query}`;
|
||||
}
|
||||
|
||||
export function setNewToken (store, params) {
|
||||
saveLocalDataAuth(store, params.userId, params.apiToken);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { DAY_MAPPING } from '@/../../common/script/cron';
|
||||
import deepFreeze from '@/libs/deepFreeze';
|
||||
import Store from '@/libs/store';
|
||||
import { asyncResourceFactory } from '@/libs/asyncResource';
|
||||
import { setUpAxios } from '@/libs/auth';
|
||||
import { authAsCredentialsState, LOCALSTORAGE_AUTH_KEY, setUpAxios } from '@/libs/auth';
|
||||
|
||||
import actions from './actions';
|
||||
import getters from './getters';
|
||||
@@ -22,7 +22,7 @@ const browserTimezoneUtcOffset = moment().utcOffset();
|
||||
|
||||
axios.defaults.headers.common['x-client'] = 'habitica-web';
|
||||
|
||||
let AUTH_SETTINGS = window.localStorage.getItem('habit-mobile-settings');
|
||||
let AUTH_SETTINGS = window.localStorage.getItem(LOCALSTORAGE_AUTH_KEY);
|
||||
if (AUTH_SETTINGS) {
|
||||
AUTH_SETTINGS = JSON.parse(AUTH_SETTINGS);
|
||||
isUserLoggedIn = setUpAxios(AUTH_SETTINGS);
|
||||
@@ -64,10 +64,7 @@ export default function clientStore () {
|
||||
// see https://github.com/HabitRPG/habitica/issues/9242
|
||||
notificationsRemoved: [],
|
||||
worldState: asyncResourceFactory(),
|
||||
credentials: isUserLoggedIn ? {
|
||||
API_ID: AUTH_SETTINGS.auth.apiId,
|
||||
API_TOKEN: AUTH_SETTINGS.auth.apiToken,
|
||||
} : {},
|
||||
credentials: isUserLoggedIn ? authAsCredentialsState(AUTH_SETTINGS) : {},
|
||||
// store the timezone offset in case it's different than the one in
|
||||
// user.preferences.timezoneOffset and change it after the user is synced
|
||||
// in app.vue
|
||||
|
||||
@@ -87,13 +87,12 @@
|
||||
"API": "API",
|
||||
"APICopied": "API token copied to clipboard.",
|
||||
"APITokenTitle": "API Token",
|
||||
"APITokenDisclaimer": "<b>Your API Token is like a password; Do not share it publicly.</b> You may occasionally be asked for your User ID, but never post your API Token where others can see it, including on Github.<br><br><b>Note:</b> If you need a new API Token (e.g., if you accidentally shared it), email <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a> with your User ID and current Token. Once it is reset you will need to re-authorize everything by logging out of the website and mobile app and by providing the new Token to any other Habitica tools that you use.",
|
||||
"APITokenDisclaimer": "<b>Your API Token is like a password; Do not share it publicly.</b> You may occasionally be asked for your User ID, but never post your API Token where others can see it, including on Github.<br><br><b>If you need a new API Token</b> (e.g., if you accidentally shared it), you can change your password to reset it. Once it is reset, you will need to log back in to any other devices you use Habitica on and provide the new API Token to third-party tools you may use.",
|
||||
"APIv3": "API v3",
|
||||
"APIText": "Copy these for use in third party applications. However, think of your API Token like a password, and do not share it publicly. You may occasionally be asked for your User ID, but never post your API Token where others can see it, including on Github.",
|
||||
"APIToken": "API Token (this is a password - see warning above!)",
|
||||
"showAPIToken": "Show API Token",
|
||||
"hideAPIToken": "Hide API Token",
|
||||
"APITokenWarning": "If you need a new API Token (e.g., if you accidentally shared it), email <%= hrefTechAssistanceEmail %> with your User ID and current Token. Once it is reset you will need to re-authorize everything by logging out of the website and mobile app and by providing the new Token to any other Habitica tools that you use.",
|
||||
"thirdPartyApps": "Third Party Apps",
|
||||
"thirdPartyTools": "Find third party apps, extensions, and all kinds of other tools you can use with your account on the <a href='https://habitica.fandom.com/wiki/Extensions,_Add-Ons,_and_Customizations' target='_blank'>Habitica wiki</a>.",
|
||||
"resetDo": "Do it, reset my account!",
|
||||
@@ -212,7 +211,7 @@
|
||||
"changeUsernameDisclaimer": "Your username is used for invitations, @mentions in chat, and messaging. It must be 1 to 20 characters, containing only letters a to z, numbers 0 to 9, hyphens, or underscores, and cannot include any inappropriate terms.",
|
||||
"changeEmailDisclaimer": "This is the email address that you use to log in to Habitica, as well as receive notifications.",
|
||||
"changeDisplayNameDisclaimer": "This is the name that will be displayed for your Avatar in Habitica.",
|
||||
"changePasswordDisclaimer": "Password must be 8 characters or more. We recommend a strong password that you're not using elsewhere.",
|
||||
"changePasswordDisclaimer": "Passwords must be 8 characters or more. Changing your password will log you out of any other devices and third-party tools you may use.",
|
||||
"dateFormatDisclaimer": "Adjust the date formatting across Habitica.",
|
||||
"verifyUsernameVeteranPet": "One of these Veteran Pets will be waiting for you after you've finished confirming!",
|
||||
"enableAudio": "Enable Audio",
|
||||
|
||||
@@ -271,7 +271,7 @@ api.updateUsername = {
|
||||
* @apiParam (Body) {String} newPassword The new password
|
||||
* @apiParam (Body) {String} confirmPassword New password confirmation
|
||||
*
|
||||
* @apiSuccess {Object} data An empty object
|
||||
* @apiSuccess {String} data.apiToken The new apiToken
|
||||
* */
|
||||
api.updatePassword = {
|
||||
method: 'PUT',
|
||||
@@ -316,9 +316,14 @@ api.updatePassword = {
|
||||
|
||||
// set new password and make sure it's using bcrypt for hashing
|
||||
await passwordUtils.convertToBcrypt(user, newPassword);
|
||||
|
||||
user.apiToken = common.uuid();
|
||||
|
||||
await user.save();
|
||||
|
||||
res.respond(200, {});
|
||||
res.respond(200, {
|
||||
apiToken: user.apiToken,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -350,6 +355,7 @@ api.resetPassword = {
|
||||
{ 'auth.local.email': email }, // Prefer to reset password for local auth
|
||||
{ auth: 1 },
|
||||
).exec();
|
||||
|
||||
if (!user) { // If no local auth with that email...
|
||||
const potentialUsers = await User.find(
|
||||
{
|
||||
@@ -486,6 +492,9 @@ api.resetPasswordSetNewOne = {
|
||||
await passwordUtils.convertToBcrypt(user, String(newPassword));
|
||||
user.auth.local.passwordResetCode = undefined; // Reset saved password reset code
|
||||
if (!user.auth.local.email) user.auth.local.email = await socialEmailToLocal(user);
|
||||
|
||||
user.apiToken = common.uuid();
|
||||
|
||||
await user.save();
|
||||
|
||||
return res.respond(200, {}, res.t('passwordChangeSuccess'));
|
||||
|
||||
Reference in New Issue
Block a user