mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 22:27:26 +01:00
* Fix resetting account for social accounts * added integration tests * chore(packages): reinstall modules * only enable reset button if user typed RESET * fix enabling reset button --------- Co-authored-by: negue <eugen.bolz@gmail.com> Co-authored-by: Sabe Jones <sabe@habitica.com>
322 lines
8.8 KiB
JavaScript
322 lines
8.8 KiB
JavaScript
import { authWithHeaders } from '../../middlewares/auth';
|
|
import * as userLib from '../../libs/user';
|
|
import { verifyDisplayName } from '../../libs/user/validation';
|
|
import common from '../../../common';
|
|
import { TransactionModel as Transaction } from '../../models/transaction';
|
|
import { BadRequest, NotAuthorized } from '../../libs/errors';
|
|
import * as passwordUtils from '../../libs/password';
|
|
|
|
const api = {};
|
|
const RESET_CONFIRMATION = 'RESET';
|
|
|
|
/*
|
|
* 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
|
|
* some breaking change (for example because their returned the entire user object).
|
|
*/
|
|
|
|
/* NOTE this route has also an API v3 version */
|
|
|
|
/**
|
|
* @apiIgnore
|
|
* @api {get} /api/v4/user Get the authenticated user's profile
|
|
* @apiName UserGet
|
|
* @apiGroup User
|
|
*
|
|
* @apiDescription The user profile contains data related to the authenticated user
|
|
* including (but not limited to);
|
|
* Achievements
|
|
* Authentications (including types and timestamps)
|
|
* Challenges
|
|
* Flags (including armoire, tutorial, tour etc...)
|
|
* Guilds
|
|
* History (including timestamps and values)
|
|
* Inbox (without messages in v4)
|
|
* Invitations (to parties/guilds)
|
|
* Items (character's full inventory)
|
|
* New Messages (flags for groups/guilds that have new messages)
|
|
* Notifications
|
|
* Party (includes current quest information)
|
|
* Preferences (user selected prefs)
|
|
* Profile (name, photo url, blurb)
|
|
* Purchased (includes purchase history, gem purchased items, plans)
|
|
* PushDevices (identifiers for mobile devices authorized)
|
|
* Stats (standard RPG stats, class, buffs, xp, etc..)
|
|
* Tags
|
|
* TasksOrder (list of all ids for dailys, habits, rewards and todos)
|
|
*
|
|
* @apiParam (Query) {String} [userFields] A list of comma separated user fields
|
|
* to be returned instead of the entire document.
|
|
* Notifications are always returned.
|
|
*
|
|
* @apiExample {curl} Example use:
|
|
* curl -i https://habitica.com/api/v3/user?userFields=achievements,items.mounts
|
|
*
|
|
* @apiSuccess {Object} data The user object
|
|
*
|
|
* @apiSuccessExample {json} Result:
|
|
* {
|
|
* "success": true,
|
|
* "data": {
|
|
* -- User data included here, for details of the user model see:
|
|
* -- https://github.com/HabitRPG/habitica/tree/develop/website/server/models/user
|
|
* }
|
|
* }
|
|
*
|
|
*/
|
|
api.getUser = {
|
|
method: 'GET',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user',
|
|
async handler (req, res) {
|
|
await userLib.get(req, res, { isV3: false });
|
|
},
|
|
};
|
|
|
|
/* NOTE this route has also an API v3 version */
|
|
|
|
/**
|
|
* @apiIgnore
|
|
* @api {put} /api/v4/user Update the user
|
|
* @apiName UserUpdate
|
|
* @apiGroup User
|
|
*
|
|
* @apiDescription Some of the user items can be updated, such as preferences, flags and stats.
|
|
^
|
|
* @apiParamExample {json} Request-Example:
|
|
* {
|
|
* "achievements.habitBirthdays": 2,
|
|
* "profile.name": "MadPink",
|
|
* "stats.hp": 53,
|
|
* "flags.warnedLowHealth":false,
|
|
* "preferences.allocationMode":"flat",
|
|
* "preferences.hair.bangs": 3
|
|
* }
|
|
*
|
|
* @apiSuccess {Object} data The updated user object, the result is identical to the get user call
|
|
*
|
|
* @apiError (401) {NotAuthorized} messageUserOperationProtected Returned if the
|
|
* change is not allowed.
|
|
*
|
|
* @apiErrorExample {json} Error-Response:
|
|
* {
|
|
* "success": false,
|
|
* "error": "NotAuthorized",
|
|
* "message": "path `stats.class` was not saved, as it's a protected path."
|
|
* }
|
|
*/
|
|
api.updateUser = {
|
|
method: 'PUT',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user',
|
|
async handler (req, res) {
|
|
await userLib.update(req, res, { isV3: false });
|
|
},
|
|
};
|
|
|
|
/* NOTE this route has also an API v3 version */
|
|
|
|
/**
|
|
* @apiIgnore
|
|
* @api {post} /api/v4/user/rebirth Use Orb of Rebirth on user
|
|
* @apiName UserRebirth
|
|
* @apiGroup User
|
|
*
|
|
* @apiSuccess {Object} data.user
|
|
* @apiSuccess {Array} data.tasks User's modified tasks (no rewards)
|
|
* @apiSuccess {String} message Success message
|
|
*
|
|
* @apiSuccessExample {json}
|
|
* {
|
|
* "success": true,
|
|
* "data": {
|
|
* },
|
|
* "message": "You have been reborn!"
|
|
* {
|
|
* "type": "REBIRTH_ACHIEVEMENT",
|
|
* "data": {},
|
|
* "id": "424d69fa-3a6d-47db-96a4-6db42ed77a43"
|
|
* }
|
|
* ]
|
|
* }
|
|
*
|
|
* @apiError {NotAuthorized} Not enough gems
|
|
*
|
|
* @apiErrorExample {json}
|
|
* {"success":false,"error":"NotAuthorized","message":"Not enough Gems"}
|
|
*/
|
|
api.userRebirth = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/rebirth',
|
|
async handler (req, res) {
|
|
await userLib.rebirth(req, res, { isV3: false });
|
|
},
|
|
};
|
|
|
|
/* NOTE this route has also an API v3 version */
|
|
|
|
/**
|
|
* @apiIgnore
|
|
* @api {post} /api/v4/user/reroll Reroll a user using the Fortify Potion
|
|
* @apiName UserReroll
|
|
* @apiGroup User
|
|
*
|
|
* @apiSuccess {Object} data.user
|
|
* @apiSuccess {Object} data.tasks User's modified tasks (no rewards)
|
|
* @apiSuccess {Object} message Success message
|
|
*
|
|
* @apiSuccessExample {json}
|
|
* {
|
|
* "success": true,
|
|
* "data": {
|
|
* },
|
|
* "message": "Fortify complete!"
|
|
* }
|
|
*
|
|
* @apiError {NotAuthorized} Not enough gems
|
|
*
|
|
* @apiErrorExample {json}
|
|
* {"success":false,"error":"NotAuthorized","message":"Not enough Gems"}
|
|
*/
|
|
api.userReroll = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/reroll',
|
|
async handler (req, res) {
|
|
await userLib.reroll(req, res, { isV3: false });
|
|
},
|
|
};
|
|
|
|
/* NOTE this route has also an API v3 version */
|
|
|
|
/**
|
|
* @apiIgnore
|
|
* @api {post} /api/v4/user/reset Reset user
|
|
* @apiName UserReset
|
|
* @apiGroup User
|
|
*
|
|
* @apiParam (Body) {String} password The user's password
|
|
*
|
|
* @apiSuccess {Object} data.user
|
|
* @apiSuccess {Array} data.tasksToRemove IDs of removed tasks
|
|
* @apiSuccess {String} message Success message
|
|
*
|
|
* @apiSuccessExample {json}
|
|
* {
|
|
* "success": true,
|
|
* "data": {--TRUNCATED--},
|
|
* "tasksToRemove": [
|
|
* "ebb8748c-0565-431e-9036-b908da25c6b4",
|
|
* "12a1cecf-68eb-40a7-b282-4f388c32124c"
|
|
* ]
|
|
* },
|
|
* "message": "Reset complete!"
|
|
* }
|
|
*/
|
|
api.userReset = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/reset',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const { password } = req.body;
|
|
if (!password) {
|
|
throw new BadRequest(res.t('missingPassword'));
|
|
}
|
|
|
|
if (user.auth.local.hashed_password && user.auth.local.email) {
|
|
const isValidPassword = await passwordUtils.compare(user, password);
|
|
if (!isValidPassword) throw new NotAuthorized(res.t('wrongPassword'));
|
|
} else if (
|
|
(user.auth.facebook.id || user.auth.google.id || user.auth.apple.id)
|
|
&& password !== RESET_CONFIRMATION
|
|
) {
|
|
throw new NotAuthorized(res.t('incorrectResetPhrase', { magicWord: RESET_CONFIRMATION }));
|
|
}
|
|
|
|
await userLib.reset(req, res, { isV3: false });
|
|
},
|
|
};
|
|
|
|
api.verifyDisplayName = {
|
|
method: 'POST',
|
|
url: '/user/auth/verify-display-name',
|
|
middlewares: [authWithHeaders({
|
|
optional: true,
|
|
})],
|
|
async handler (req, res) {
|
|
req.checkBody({
|
|
displayName: {
|
|
notEmpty: { errorMessage: res.t('messageMissingDisplayName') },
|
|
},
|
|
});
|
|
|
|
const validationErrors = req.validationErrors();
|
|
if (validationErrors) throw validationErrors;
|
|
|
|
const chosenDisplayName = req.body.displayName;
|
|
|
|
const issues = verifyDisplayName(chosenDisplayName, res);
|
|
|
|
if (issues.length > 0) {
|
|
res.respond(200, { isUsable: false, issues });
|
|
} else {
|
|
res.respond(200, { isUsable: true });
|
|
}
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v4/user/unequip/:type Unequip all items by type
|
|
* @apiName UserUnEquipByType
|
|
* @apiGroup User
|
|
*
|
|
* @apiParam (Path) {String="pet-mount-background","costume","equipped"} type The type of items
|
|
* to unequip.
|
|
*
|
|
* @apiParamExample {URL} Example-URL
|
|
* https://habitica.com/api/v4/user/unequip/equipped
|
|
*
|
|
* @apiSuccess {Object} data user.items
|
|
* @apiSuccess {String} message Optional success message for unequipping an items
|
|
*
|
|
* @apiSuccessExample {json} Example return:
|
|
* {
|
|
* "success": true,
|
|
* "data": {---DATA TRUNCATED---},
|
|
* "message": "Battle Gear unequipped.
|
|
* }
|
|
*
|
|
*/
|
|
api.unequip = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/unequip/:type',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const equipRes = common.ops.unEquipByType(user, req);
|
|
await user.save();
|
|
res.respond(200, ...equipRes);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {get} /api/v4/user/purchase-history Get users purchase history
|
|
* @apiName UserGetPurchaseHistory
|
|
* @apiGroup User
|
|
*
|
|
*/
|
|
api.purchaseHistory = {
|
|
method: 'GET',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/purchase-history',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const transactions = await Transaction.find({ userId: user._id }).sort({ createdAt: -1 });
|
|
res.respond(200, transactions);
|
|
},
|
|
};
|
|
|
|
export default api;
|