Skip to content

[Question] How to modify response message for is_active = False accounts? #122

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

Open
XCanG opened this issue Mar 6, 2025 · 5 comments
Open

Comments

@XCanG
Copy link

XCanG commented Mar 6, 2025

In my project is_active used to block accounts and external table is used to provide reason. I used that functionality in DRF without issues, but I couldn't port it to Django Ninja JWT, because it's implementation is hidden.

What ninja return:

{
  "detail": "No active account found with the given credentials",
  "code": "authentication_failed"
}

urls.py:

api.register_controllers(NinjaJWTDefaultController)

I need a way to change the output and provide block reason from external table.

@Anning01
Copy link

Anning01 commented Mar 6, 2025

Give up, I've tried many methods but they can't do anything. They have an internal definition error and cannot be caught globally unless you redo this function method

@Anning01
Copy link

Anning01 commented Mar 7, 2025

I found the solution

#30 (comment)

@XCanG
Copy link
Author

XCanG commented Mar 10, 2025

I found the solution

#30 (comment)

@Anning01 That really did dig a bit in a right direction, but I'm still stuck. The problem is to get the user from token before inner check for is_active

Here is what I'm currently at:

def api_exception_handler(request: HttpRequest, exc: exceptions.APIException) -> HttpResponse:
    """
    # `APIException` handler
    """

    headers: dict = {}
    extra: dict = {}

    if (
        exc.status_code == 401
        and (path := request.get_full_path()).startswith("/api/")
        and (
            (
                path.endswith(("/token/pair", "/token/refresh"))
                and request.body
                and (username := json.loads(request.body or "{}").get("username"))
            )
            or (token := request.headers.get("X-API-Key")) # stuck here
        )
    ):
        extra["reason"] = (
            f"User is blocked. Reason: {reason.reason}"
            if (reason := User_Block_Reason.objects.get_or_none(user__username=username))
            else "User is not active."
        )

    data: list[exceptions.ErrorDetail] | dict[Any, exceptions.ErrorDetail] = (
        exc.detail
        if isinstance(exc.detail, list)
        else ({**exc.detail, **extra} if isinstance(exc.detail, dict) else {"detail": exc.detail, **extra})
    )

    response: HttpResponse = api.create_response(request, data, status=exc.status_code)
    for k, v in headers.items():
        response.setdefault(k, v)

    return response

So, what I did is add extra dict with nessesary fields that will add into response dict, now, I do check if error is a kind of error for 401, that it bound to my /api/... endpoints (I have different app on backend as well, so I need to separate) and if it pair token then I attempt to get username from login response (this is a success) or I need to get that from token (this is where I stuck). The rest of code is just getting record from table User_Block_Reason and adding that to extra.

Now, if request comes not from login, then I can't get user from here to proceed further.

@eadwinCode
Copy link
Owner

@XCanG @Anning01 Sorry I am late to the show. Reason #123

Regarding your issue, did you try USER_AUTHENTICATION_RULE

 NINJA_JWT = {
    'USER_AUTHENTICATION_RULE': 'ninja_jwt.authentication.default_user_authentication_rule',
}

# ninja_jwt.authentication.default_user_authentication_rule
def default_user_authentication_rule(user) -> bool:
    return user is not None and user.is_active

# This function is called after authentication is successful to check if the user is active

Also, when overriding api_exception_handler you can check the exception type

from django.http import HttpRequest, HttpResponse
from django.contrib.auth import get_user_model
from ninja_extra import exceptions
from ninja_jwt import exceptions as jwt_exceptions
from ninja_extra import service_resolver
from ninja_extra.context import RouteContext

user_name_field = get_user_model().USERNAME_FIELD


def api_exception_handler(request: HttpRequest, exc: exceptions.APIException) -> HttpResponse:
    """
    # `APIException` handler
    """

    headers: dict = {}
    extra: dict = {}
    route_context: RouteContext = service_resolver(RouteContext)

    if isinstance(exc, jwt_exceptions.AuthenticationFailed):
        username = route_context.kwargs[user_name_field]
        extra["reason"] = (
            f"User is blocked. Reason: {reason.reason}"
            if (reason := User_Block_Reason.objects.get_or_none(user__username=username))
            else "User is not active."
        )

    data: list[exceptions.ErrorDetail] | dict[Any, exceptions.ErrorDetail] = (
        exc.detail
        if isinstance(exc.detail, list)
        else ({**exc.detail, **extra} if isinstance(exc.detail, dict) else {"detail": exc.detail, **extra})
    )

    response: HttpResponse = api.create_response(request, data, status=exc.status_code)
    for k, v in headers.items():
        response.setdefault(k, v)

    return response

@XCanG
Copy link
Author

XCanG commented Mar 14, 2025

Regarding your issue, did you try USER_AUTHENTICATION_RULE

Not yet, didn't think it could solve my case.

I will try your proposed code on Monday and come back with reply. If it wouldn't solve 2nd case I will look if this rule will able to handle check later down the line in some util function.

Also a little proposal: enable Github Discussions in this repo, so that questions can be asked here instead of creating issues (for bug reports), unless you fine with it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants