diff --git a/api/openapi.generated.yml b/api/openapi.generated.yml index 7cda9c07c..59648ad25 100644 --- a/api/openapi.generated.yml +++ b/api/openapi.generated.yml @@ -119,6 +119,27 @@ paths: summary: User Token security: - ApiKeyAuth: [] + /v1/users/token/logout: + post: + parameters: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UserTokenLogoutResponse' + description: Successful response + '401': + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Authentication error + tags: + - User v1 + summary: User Token Logout + security: + - ApiJwtAuth: [] /v1/opportunities/search: post: parameters: [] @@ -645,6 +666,19 @@ components: type: integer description: The HTTP status code example: 200 + UserTokenLogoutResponse: + type: object + properties: + message: + type: string + description: The message to return + example: Success + data: + example: null + status_code: + type: integer + description: The HTTP status code + example: 200 FundingInstrumentFilterV1: type: object properties: diff --git a/api/src/api/users/user_routes.py b/api/src/api/users/user_routes.py index 1c34f0b5a..4ad189a3a 100644 --- a/api/src/api/users/user_routes.py +++ b/api/src/api/users/user_routes.py @@ -1,10 +1,15 @@ import logging +from src.adapters import db +from src.adapters.db import flask_db from src.api import response from src.api.route_utils import raise_flask_error from src.api.users import user_schemas from src.api.users.user_blueprint import user_blueprint +from src.api.users.user_schemas import UserTokenLogoutResponseSchema +from src.auth.api_jwt_auth import api_jwt_auth from src.auth.api_key_auth import api_key_auth +from src.db.models.user_models import UserTokenSession logger = logging.getLogger(__name__) @@ -34,3 +39,27 @@ def user_token(x_oauth_login_gov: dict) -> response.ApiResponse: logger.info(message) raise_flask_error(400, message) + + +@user_blueprint.post("/token/logout") +@user_blueprint.output(UserTokenLogoutResponseSchema) +@user_blueprint.doc(responses=[200, 401]) +@user_blueprint.auth_required(api_jwt_auth) +@flask_db.with_db_session() +def user_token_logout(db_session: db.Session) -> response.ApiResponse: + logger.info("POST /v1/users/token/logout") + + user_token_session: UserTokenSession = api_jwt_auth.current_user # type: ignore + with db_session.begin(): + user_token_session.is_valid = False + db_session.add(user_token_session) + + logger.info( + "Logged out a user", + extra={ + "user_token_session.token_id": str(user_token_session.token_id), + "user_token_session.user_id": str(user_token_session.user_id), + }, + ) + + return response.ApiResponse(message="Success") diff --git a/api/src/api/users/user_schemas.py b/api/src/api/users/user_schemas.py index 3ea05421f..b645ce092 100644 --- a/api/src/api/users/user_schemas.py +++ b/api/src/api/users/user_schemas.py @@ -51,3 +51,8 @@ class UserTokenSchema(Schema): class UserTokenResponseSchema(AbstractResponseSchema): data = fields.Nested(UserTokenSchema) + + +class UserTokenLogoutResponseSchema(AbstractResponseSchema): + # No data returned + data = fields.MixinField(metadata={"example": None}) diff --git a/api/tests/src/api/users/test_user_route_token.py b/api/tests/src/api/users/test_user_route_token.py index d0811de94..f309afbec 100644 --- a/api/tests/src/api/users/test_user_route_token.py +++ b/api/tests/src/api/users/test_user_route_token.py @@ -1,3 +1,6 @@ +from src.auth.api_jwt_auth import create_jwt_for_user +from tests.src.db.models.factories import UserFactory + ################## # POST /token ################## @@ -25,3 +28,33 @@ def test_post_user_route_token_400(client, api_auth_token): resp = client.post("v1/users/token", headers={"X-Auth": api_auth_token}) assert resp.status_code == 400 assert resp.get_json()["message"] == "Missing X-OAuth-login-gov header" + + +def test_post_user_route_token_logout_200( + enable_factory_create, client, db_session, api_auth_token +): + user = UserFactory.create() + token, user_token_session = create_jwt_for_user(user, db_session) + db_session.commit() + + resp = client.post("v1/users/token/logout", headers={"X-SGG-Token": token}) + + db_session.refresh(user_token_session) + + assert resp.status_code == 200 + assert not user_token_session.is_valid + + +def test_post_user_route_token_logout_invalid( + enable_factory_create, client, db_session, api_auth_token +): + user = UserFactory.create() + + token, session = create_jwt_for_user(user, db_session) + session.is_valid = False + db_session.commit() + + resp = client.post("v1/users/token/logout", headers={"X-SGG-Token": token}) + + assert resp.status_code == 401 + assert resp.get_json()["message"] == "Token is no longer valid"