Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add v2 endpoint for user password reset #378 #418

Merged
merged 1 commit into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 );
} );
} );
} );