Skip to content

Commit

Permalink
Merge pull request #418 from inaturalist/378-password-reset-endpoint
Browse files Browse the repository at this point in the history
Add v2 endpoint for user password reset #378
  • Loading branch information
pleary authored Jan 3, 2024
2 parents a8a79e6 + b0a1901 commit f9a5b97
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 6 deletions.
29 changes: 28 additions & 1 deletion lib/controllers/v2/users_controller.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const _ = require( "lodash" );
const { users } = require( "inaturalistjs" );
const fetch = require( "node-fetch" );
const config = require( "../../../config" );
const pgClient = require( "../../pg_client" );
const esClient = require( "../../es_client" );
const User = require( "../../models/user" );
Expand Down Expand Up @@ -107,6 +109,30 @@ const update = async req => {
return user;
};

const resetPassword = async req => {
const requestAbortController = new AbortController( );
const requestTimeout = setTimeout( ( ) => {
requestAbortController.abort( );
}, 10000 );
try {
const response = await fetch( `${config.apiURL}/users/password`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
},
body: `user[email]=${encodeURIComponent( req.body.user.email )}`,
signal: requestAbortController.signal
} );
if ( !response.ok ) {
throw httpError( 500, "Password reset failed" );
}
} catch ( error ) {
throw httpError( 500, "Password reset failed" );
} finally {
clearTimeout( requestTimeout );
}
};

module.exports = {
index,
show,
Expand All @@ -115,5 +141,6 @@ module.exports = {
mute: ctrlv1.mute,
unmute: ctrlv1.unmute,
block: ctrlv1.block,
unblock: ctrlv1.unblock
unblock: ctrlv1.unblock,
resetPassword
};
18 changes: 15 additions & 3 deletions lib/inaturalist_api_v2.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ const InaturalistAPIV2 = class InaturalistAPIV2 {
}
}

console.log( "errorMiddleware trace:" );
console.trace( err );
console.log( ":end trace" );
if ( !( process.env.NODE_ENV === "test" && err.status === 401 ) ) {
console.log( "errorMiddleware trace:" );
console.trace( err );
console.log( ":end trace" );
}
// Confusingly, the response object we get here has not already had its CORS
// access control headers set, even though that middleware should be set in
// inaturalist_api.js. Without this, clients like Chrome will simply bail
Expand Down Expand Up @@ -783,6 +785,16 @@ const InaturalistAPIV2 = class InaturalistAPIV2 {
}
throw JWT_MISSING_OR_INVALID_ERROR;
},
appJwtRequired: async req => {
await InaturalistAPIV2.verifyHeaderJWTs( req );
if ( req.applicationSession ) {
if ( req.userSession ) {
throw JWT_MISSING_OR_INVALID_ERROR;
}
return true;
}
throw JWT_MISSING_OR_INVALID_ERROR;
},
appOrUserJwtRequired: async req => {
await InaturalistAPIV2.verifyHeaderJWTs( req );
if ( req.userSession || req.applicationSession ) {
Expand Down
7 changes: 7 additions & 0 deletions openapi/doc.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,13 @@ Privacy Policy: <https://www.inaturalist.org/privacy>`
in: "header",
description: "User-specific JSON Web Token optional, may be used to customize responses for the authenticated user, e.g. localizing common names"
},
appJwtRequired: {
type: "apiKey",
name: "Authorization",
in: "header",
description: "Application JSON Web Token required (application tokens only available to "
+ "official apps), and user-specific JSON Web Tokens are not allowed"
},
appOrUserJwtRequired: {
type: "apiKey",
name: "Authorization",
Expand Down
37 changes: 37 additions & 0 deletions openapi/paths/v2/users/reset_password.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const UsersController = require( "../../../../lib/controllers/v2/users_controller" );

module.exports = sendWrapper => {
async function POST( req, res ) {
await UsersController.resetPassword( req );
sendWrapper( req, res.status( 204 ) );
}

POST.apiDoc = {
tags: ["Users"],
summary: "Reset a user password",
security: [{
appJwtRequired: []
}],
requestBody: {
content: {
"application/json": {
schema: {
$ref: "#/components/schemas/UsersResetPassword"
}
}
}
},
responses: {
204: {
description: "No response body; success implies reset request was received"
},
default: {
$ref: "#/components/responses/Error"
}
}
};

return {
POST
};
};
7 changes: 7 additions & 0 deletions openapi/schema/request/users_reset_password.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const Joi = require( "joi" );

module.exports = Joi.object( ).keys( {
user: Joi.object( ).keys( {
email: Joi.string( ).email( ).required( )
} )
} );
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions test/integration/v2/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -319,4 +319,48 @@ describe( "Users", ( ) => {
.expect( 200, done );
} );
} );

describe( "reset_password", ( ) => {
const currentUser = fixtures.elasticsearch.users.user[0];
const userToken = jwt.sign( { user_id: currentUser.id },
config.jwtSecret || "secret",
{ algorithm: "HS512" } );
const applicationToken = jwt.sign(
{ application: "whatever" },
config.jwtApplicationSecret || "application_secret",
{ algorithm: "HS512" }
);

it( "should 401 without auth", function ( done ) {
request( this.app )
.post( "/v2/users/reset_password" )
.expect( 401, done );
} );

it( "should 401 with a user token", function ( done ) {
request( this.app )
.post( "/v2/users/reset_password" )
.set( "Authorization", userToken )
.expect( 401, done );
} );

it( "should hit the Rails equivalent and return 200", function ( done ) {
const nockScope = nock( "http://localhost:3000" )
.post( "/users/password" )
.reply( 200 );
request( this.app )
.post( "/v2/users/reset_password" )
.set( "Authorization", applicationToken )
.send( {
user: {
email: "[email protected]"
}
} )
.expect( ( ) => {
// Raise an exception if the nocked endpoint doesn't get called
nockScope.done( );
} )
.expect( 204, done );
} );
} );
} );

0 comments on commit f9a5b97

Please sign in to comment.