@@ -95,9 +95,20 @@ def create_user():
9595 - `403 Forbidden`: Insufficient privileges to create the requested role
9696 - `429 Too Many Requests`: Rate limit exceeded
9797 - `500 Internal Server Error`: User creation failed
98+
99+ **Query Parameters**:
100+ - `legacy`: If "true" (default), emails the password directly for backwards
101+ compatibility with the QGIS plugin. If "false", sends a password reset
102+ link instead (more secure).
98103 """
99104 logger .info ("[ROUTER]: Creating user" )
100105 body = request .get_json ()
106+
107+ # Check for legacy query parameter (defaults to true for backwards
108+ # compatibility with QGIS plugin)
109+ legacy_param = request .args .get ("legacy" , "true" )
110+ legacy = legacy_param .lower () != "false"
111+
101112 if request .headers .get ("Authorization" , None ) is not None :
102113
103114 @jwt_required ()
@@ -114,7 +125,7 @@ def identity():
114125 else :
115126 body ["role" ] = "USER"
116127 try :
117- user = UserService .create_user (body )
128+ user = UserService .create_user (body , legacy = legacy )
118129 except UserDuplicated as e :
119130 logger .error ("[ROUTER]: " + e .message )
120131 return error (status = 400 , detail = e .message )
@@ -656,13 +667,27 @@ def recover_password(user):
656667 **Path Parameters**:
657668 - `user`: User identifier (email address or numeric ID)
658669
670+ **Query Parameters**:
671+ - `legacy`: (optional, default=true) Password recovery mode:
672+ - `true` (default): Legacy mode - generates new password and emails it
673+ directly. Maintained for backwards compatibility with older QGIS
674+ plugin versions.
675+ - `false`: Secure mode - sends a password reset link that expires after
676+ 1 hour. Recommended for new integrations.
677+
659678 **Request**: No request body required
660679
661- **Recovery Process**:
680+ **Recovery Process (legacy=true, default)**:
681+ 1. Validates user exists
682+ 2. Generates a new secure password
683+ 3. Updates user's password in database
684+ 4. Emails the new password to user
685+
686+ **Recovery Process (legacy=false)**:
662687 1. Validates user exists and account is active
663- 2. Generates secure password reset token with expiration
688+ 2. Generates secure password reset token with 1-hour expiration
664689 3. Sends password recovery email with reset link
665- 4. Logs recovery attempt for security monitoring
690+ 4. User clicks link and sets new password via /user/reset-password endpoint
666691
667692 **Success Response Schema**:
668693 ```json
@@ -671,39 +696,30 @@ def recover_password(user):
671696 "id": "user-123",
672697 "email": "user@example.com",
673698 "name": "John Doe",
674- "role": "USER",
675- "recovery_initiated": true,
676- "recovery_token_expires": "2025-01-15T16:00:00Z"
699+ "role": "USER"
677700 }
678701 }
679702 ```
680703
681- **Email Content**:
682- - Secure reset link with time-limited token
683- - Clear instructions for password reset process
684- - Security notice about unsolicited requests
685- - Link expiration time (typically 1-2 hours)
686-
687- **Security Features**:
688- - Rate limiting prevents email flooding attacks
689- - Tokens expire automatically for security
690- - Email delivery doesn't reveal if account exists (privacy)
691- - Multiple recovery attempts are logged and monitored
692-
693- **Use Cases**:
694- - User forgot their password
695- - Account recovery after security incident
696- - Password reset for inactive accounts
697- - Initial password setup (in some configurations)
704+ **Security Notes**:
705+ - Legacy mode (default) is DEPRECATED but maintained for backwards
706+ compatibility. It sends passwords via email which is less secure.
707+ - New integrations should use `legacy=false` for better security.
708+ - Rate limiting prevents email flooding attacks in both modes.
698709
699710 **Error Responses**:
700- - `404 Not Found`: User does not exist (for security, may return success)
711+ - `404 Not Found`: User does not exist
701712 - `429 Too Many Requests`: Rate limit exceeded
702713 - `500 Internal Server Error`: Email delivery failed or system error
703714 """
704715 logger .info ("[ROUTER]: Recovering password" )
716+
717+ # Parse legacy parameter - defaults to True for backwards compatibility
718+ legacy_param = request .args .get ("legacy" , "true" ).lower ()
719+ use_legacy = legacy_param not in ("false" , "0" , "no" )
720+
705721 try :
706- user = UserService .recover_password (user )
722+ user = UserService .recover_password (user , legacy = use_legacy )
707723 except UserNotFound as e :
708724 logger .error ("[ROUTER]: " + e .message )
709725 return error (status = 404 , detail = e .message )
@@ -716,6 +732,84 @@ def recover_password(user):
716732 return jsonify (data = user .serialize ()), 200
717733
718734
735+ @endpoints .route ("/user/reset-password" , strict_slashes = False , methods = ["POST" ])
736+ @limiter .limit (
737+ lambda : ";" .join (RateLimitConfig .get_password_reset_limits ()) or "3 per hour" ,
738+ key_func = get_admin_aware_key ,
739+ exempt_when = is_rate_limiting_disabled ,
740+ )
741+ def reset_password_with_token ():
742+ """
743+ Reset password using a secure token from password recovery email.
744+
745+ **Rate Limited**: Subject to password recovery rate limits (configurable)
746+ **Access**: Public endpoint - no authentication required
747+ **Security**: Token-based authentication, tokens expire after 1 hour
748+
749+ **Request Body Schema**:
750+ ```json
751+ {
752+ "token": "secure-reset-token-from-email",
753+ "password": "new-secure-password"
754+ }
755+ ```
756+
757+ **Password Requirements**:
758+ - Minimum 8 characters
759+ - Must contain at least one uppercase letter
760+ - Must contain at least one lowercase letter
761+ - Must contain at least one digit
762+
763+ **Success Response Schema**:
764+ ```json
765+ {
766+ "data": {
767+ "message": "Password reset successful"
768+ }
769+ }
770+ ```
771+
772+ **Security Features**:
773+ - Tokens are single-use (marked as used after successful reset)
774+ - Tokens expire after 1 hour
775+ - Rate limiting prevents brute force attacks
776+ - Password strength validation enforced
777+
778+ **Error Responses**:
779+ - `400 Bad Request`: Missing token or password
780+ - `404 Not Found`: Invalid or expired token
781+ - `422 Unprocessable Entity`: Password doesn't meet requirements
782+ - `429 Too Many Requests`: Rate limit exceeded
783+ - `500 Internal Server Error`: System error
784+ """
785+ logger .info ("[ROUTER]: Reset password with token" )
786+ try :
787+ body = request .get_json ()
788+ if not body :
789+ return error (status = 400 , detail = "Request body required" )
790+
791+ token = body .get ("token" )
792+ password = body .get ("password" )
793+
794+ if not token :
795+ return error (status = 400 , detail = "Reset token is required" )
796+ if not password :
797+ return error (status = 400 , detail = "New password is required" )
798+
799+ UserService .reset_password_with_token (token , password )
800+ return jsonify (data = {"message" : "Password reset successful" }), 200
801+
802+ except UserNotFound as e :
803+ logger .error ("[ROUTER]: " + e .message )
804+ return error (status = 404 , detail = e .message )
805+ except PasswordValidationError as e :
806+ logger .error ("[ROUTER]: " + e .message )
807+ return error (status = 422 , detail = e .message )
808+ except Exception as e :
809+ logger .error ("[ROUTER]: " + str (e ))
810+ return error (status = 500 , detail = "Generic Error" )
811+
812+
719813@endpoints .route ("/user/<user>" , strict_slashes = False , methods = ["PATCH" ])
720814@jwt_required ()
721815@validate_user_update
0 commit comments