From e13d7b537ea3f4a938325f40666c0dd6164fa008 Mon Sep 17 00:00:00 2001 From: bruk Date: Fri, 22 Nov 2024 15:55:24 -0500 Subject: [PATCH 01/22] add response schema --- api/src/api/users/user_schemas.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/src/api/users/user_schemas.py b/api/src/api/users/user_schemas.py index 3ea05421f..12754259d 100644 --- a/api/src/api/users/user_schemas.py +++ b/api/src/api/users/user_schemas.py @@ -51,3 +51,7 @@ class UserTokenSchema(Schema): class UserTokenResponseSchema(AbstractResponseSchema): data = fields.Nested(UserTokenSchema) + +class UserTokenRefreshResponseSchema(AbstractResponseSchema): + # No data returned + data = fields.MixinField(metadata={"example": None}) \ No newline at end of file From 5c9cba716c42bb032a0622b2d7492210fd85e273 Mon Sep 17 00:00:00 2001 From: bruk Date: Fri, 22 Nov 2024 15:55:53 -0500 Subject: [PATCH 02/22] add token refresh endpoint --- api/src/api/users/user_routes.py | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/api/src/api/users/user_routes.py b/api/src/api/users/user_routes.py index 1c34f0b5a..1a86527e6 100644 --- a/api/src/api/users/user_routes.py +++ b/api/src/api/users/user_routes.py @@ -1,10 +1,17 @@ import logging +from datetime import timedelta +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 UserTokenRefreshResponseSchema +from src.auth import api_jwt_auth +from src.auth.api_jwt_auth import get_config from src.auth.api_key_auth import api_key_auth +from src.util import datetime_util logger = logging.getLogger(__name__) @@ -34,3 +41,30 @@ def user_token(x_oauth_login_gov: dict) -> response.ApiResponse: logger.info(message) raise_flask_error(400, message) + + +@user_blueprint.post("/token/refresh") +@user_blueprint.output(UserTokenRefreshResponseSchema) +@user_blueprint.doc(responses=[200, 401]) +@user_blueprint.auth_required(api_jwt_auth) +@flask_db.with_db_session() +def user_token_refresh(db_session: db.Session) -> response.ApiResponse: + logger.info("POST /v1/users/token/refresh") + + user_token_session: UserTokenSession = api_jwt_auth.current_user # type: ignore + config = get_config() + expiration_time = datetime_util.utcnow() + timedelta(minutes=config.token_expiration_minutes) + + with db_session.begin(): + user_token_session.expires_at = expiration_time + db_session.add(user_token_session) + + logger.info( + "Refreshed a user token", + 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") \ No newline at end of file From ad4b5936128e9d7788edf096006b67f4d386532b Mon Sep 17 00:00:00 2001 From: bruk Date: Fri, 22 Nov 2024 15:56:16 -0500 Subject: [PATCH 03/22] add suceess and expired token test --- .../src/api/users/test_user_route_token.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) 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..340a05e0e 100644 --- a/api/tests/src/api/users/test_user_route_token.py +++ b/api/tests/src/api/users/test_user_route_token.py @@ -1,6 +1,10 @@ ################## # POST /token ################## +from datetime import datetime + +from src.auth.api_jwt_auth import create_jwt_for_user +from tests.src.db.models.factories import UserFactory def test_post_user_route_token_200(client, api_auth_token): @@ -25,3 +29,34 @@ 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_refresh_200( + enable_factory_create, client, db_session, api_auth_token +): + user = UserFactory.create() + token, user_token_session = create_jwt_for_user(user, db_session) + expiration = user_token_session.expires_at + db_session.commit() + + resp = client.post("v1/users/token/refresh", headers={"X-SGG-Token": token}) + + db_session.refresh(user_token_session) + + assert resp.status_code == 200 + assert not user_token_session.expired_at != expiration + + +def test_post_user_route_token_refresh_expired( + enable_factory_create, client, db_session, api_auth_token +): + user = UserFactory.create() + + token, session = create_jwt_for_user(user, db_session) + session.expires_at = datetime.fromisoformat("1980-01-01 12:00:00+00:00") + db_session.commit() + + resp = client.post("v1/users/token/refresh", headers={"X-SGG-Token": token}) + + assert resp.status_code == 401 + assert resp.get_json()["message"] == "Token expired" \ No newline at end of file From aa8a1920fd33dae638900c215d51141b798e34b2 Mon Sep 17 00:00:00 2001 From: bruk Date: Fri, 22 Nov 2024 16:01:58 -0500 Subject: [PATCH 04/22] cleanup --- api/src/api/users/user_routes.py | 6 +++--- api/src/api/users/user_schemas.py | 3 ++- api/tests/src/api/users/test_user_route_token.py | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/api/src/api/users/user_routes.py b/api/src/api/users/user_routes.py index 1a86527e6..ce4009179 100644 --- a/api/src/api/users/user_routes.py +++ b/api/src/api/users/user_routes.py @@ -8,9 +8,9 @@ from src.api.users import user_schemas from src.api.users.user_blueprint import user_blueprint from src.api.users.user_schemas import UserTokenRefreshResponseSchema -from src.auth import api_jwt_auth -from src.auth.api_jwt_auth import get_config +from src.auth.api_jwt_auth import api_jwt_auth, get_config from src.auth.api_key_auth import api_key_auth +from src.db.models.user_models import UserTokenSession from src.util import datetime_util logger = logging.getLogger(__name__) @@ -67,4 +67,4 @@ def user_token_refresh(db_session: db.Session) -> response.ApiResponse: }, ) - return response.ApiResponse(message="Success") \ No newline at end of file + return response.ApiResponse(message="Success") diff --git a/api/src/api/users/user_schemas.py b/api/src/api/users/user_schemas.py index 12754259d..03a1310ee 100644 --- a/api/src/api/users/user_schemas.py +++ b/api/src/api/users/user_schemas.py @@ -52,6 +52,7 @@ class UserTokenSchema(Schema): class UserTokenResponseSchema(AbstractResponseSchema): data = fields.Nested(UserTokenSchema) + class UserTokenRefreshResponseSchema(AbstractResponseSchema): # No data returned - data = fields.MixinField(metadata={"example": None}) \ No newline at end of file + 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 340a05e0e..d1690542b 100644 --- a/api/tests/src/api/users/test_user_route_token.py +++ b/api/tests/src/api/users/test_user_route_token.py @@ -44,7 +44,7 @@ def test_post_user_route_token_refresh_200( db_session.refresh(user_token_session) assert resp.status_code == 200 - assert not user_token_session.expired_at != expiration + assert user_token_session.expires_at != expiration def test_post_user_route_token_refresh_expired( @@ -59,4 +59,4 @@ def test_post_user_route_token_refresh_expired( resp = client.post("v1/users/token/refresh", headers={"X-SGG-Token": token}) assert resp.status_code == 401 - assert resp.get_json()["message"] == "Token expired" \ No newline at end of file + assert resp.get_json()["message"] == "Token expired" From 67ef19ba2c518ca24eb0ae3ca2a67663bd02b250 Mon Sep 17 00:00:00 2001 From: nava-platform-bot Date: Fri, 22 Nov 2024 21:10:33 +0000 Subject: [PATCH 05/22] Create ERD diagram and Update OpenAPI spec --- api/openapi.generated.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/api/openapi.generated.yml b/api/openapi.generated.yml index 7cda9c07c..e47164754 100644 --- a/api/openapi.generated.yml +++ b/api/openapi.generated.yml @@ -119,6 +119,27 @@ paths: summary: User Token security: - ApiKeyAuth: [] + /v1/users/token/refresh: + post: + parameters: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UserTokenRefreshResponse' + description: Successful response + '401': + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Authentication error + tags: + - User v1 + summary: User Token Refresh + security: + - ApiJwtAuth: [] /v1/opportunities/search: post: parameters: [] @@ -645,6 +666,19 @@ components: type: integer description: The HTTP status code example: 200 + UserTokenRefreshResponse: + 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: From 6821a01d05b592273089dfc53cc5b03c6d97511f Mon Sep 17 00:00:00 2001 From: bruk Date: Fri, 22 Nov 2024 17:01:11 -0500 Subject: [PATCH 06/22] created method to get expiration time --- api/src/auth/api_jwt_auth.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/api/src/auth/api_jwt_auth.py b/api/src/auth/api_jwt_auth.py index 5c0e979d1..5464a5f1e 100644 --- a/api/src/auth/api_jwt_auth.py +++ b/api/src/auth/api_jwt_auth.py @@ -5,6 +5,7 @@ import jwt from apiflask import HTTPTokenAuth +from black import datetime from pydantic import Field from sqlalchemy import select from sqlalchemy.orm import selectinload @@ -205,3 +206,12 @@ def example_method(db_session: db.Session) -> response.ApiResponse: # The message is just the value we set when constructing the JwtValidationError logger.info("JWT Authentication Failed for provided token", extra={"auth.issue": e.message}) raise_flask_error(401, e.message) + + +def set_token_expiration_time(config: ApiJwtConfig | None = None) -> datetime: + if config is None: + config = get_config() + + expiration_time = datetime_util.utcnow() + timedelta(minutes=config.token_expiration_minutes) + + return expiration_time From 695427ddd4493982f8c1bd3503e59df3fecd649d Mon Sep 17 00:00:00 2001 From: bruk Date: Fri, 22 Nov 2024 17:01:24 -0500 Subject: [PATCH 07/22] clean up --- api/src/api/users/user_routes.py | 8 ++------ api/tests/src/api/users/test_user_route_token.py | 6 ++++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/api/src/api/users/user_routes.py b/api/src/api/users/user_routes.py index ce4009179..fce2d2409 100644 --- a/api/src/api/users/user_routes.py +++ b/api/src/api/users/user_routes.py @@ -1,5 +1,4 @@ import logging -from datetime import timedelta from src.adapters import db from src.adapters.db import flask_db @@ -8,10 +7,9 @@ from src.api.users import user_schemas from src.api.users.user_blueprint import user_blueprint from src.api.users.user_schemas import UserTokenRefreshResponseSchema -from src.auth.api_jwt_auth import api_jwt_auth, get_config +from src.auth.api_jwt_auth import api_jwt_auth, set_token_expiration_time from src.auth.api_key_auth import api_key_auth from src.db.models.user_models import UserTokenSession -from src.util import datetime_util logger = logging.getLogger(__name__) @@ -52,11 +50,9 @@ def user_token_refresh(db_session: db.Session) -> response.ApiResponse: logger.info("POST /v1/users/token/refresh") user_token_session: UserTokenSession = api_jwt_auth.current_user # type: ignore - config = get_config() - expiration_time = datetime_util.utcnow() + timedelta(minutes=config.token_expiration_minutes) with db_session.begin(): - user_token_session.expires_at = expiration_time + user_token_session.expires_at = set_token_expiration_time() db_session.add(user_token_session) logger.info( 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 d1690542b..3b3b78f7d 100644 --- a/api/tests/src/api/users/test_user_route_token.py +++ b/api/tests/src/api/users/test_user_route_token.py @@ -3,6 +3,8 @@ ################## from datetime import datetime +from freezegun import freeze_time + from src.auth.api_jwt_auth import create_jwt_for_user from tests.src.db.models.factories import UserFactory @@ -31,12 +33,12 @@ def test_post_user_route_token_400(client, api_auth_token): assert resp.get_json()["message"] == "Missing X-OAuth-login-gov header" +@freeze_time("2024-11-22 12:00:00", tz_offset=0) def test_post_user_route_token_refresh_200( enable_factory_create, client, db_session, api_auth_token ): user = UserFactory.create() token, user_token_session = create_jwt_for_user(user, db_session) - expiration = user_token_session.expires_at db_session.commit() resp = client.post("v1/users/token/refresh", headers={"X-SGG-Token": token}) @@ -44,7 +46,7 @@ def test_post_user_route_token_refresh_200( db_session.refresh(user_token_session) assert resp.status_code == 200 - assert user_token_session.expires_at != expiration + assert user_token_session.expires_at == datetime.fromisoformat("2024-11-22 12:30:00+00:00") def test_post_user_route_token_refresh_expired( From 1bd2f847da29ac792bcd539514a49cbe7baf7ea4 Mon Sep 17 00:00:00 2001 From: nava-platform-bot Date: Fri, 22 Nov 2024 22:08:06 +0000 Subject: [PATCH 08/22] Create ERD diagram and Update OpenAPI spec --- api/openapi.generated.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/api/openapi.generated.yml b/api/openapi.generated.yml index ec774f487..ceabdda5e 100644 --- a/api/openapi.generated.yml +++ b/api/openapi.generated.yml @@ -119,7 +119,7 @@ paths: summary: User Token security: - ApiKeyAuth: [] - /v1/users/token/refresh: + /v1/users/token/logout: post: parameters: [] responses: @@ -127,7 +127,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/UserTokenRefreshResponse' + $ref: '#/components/schemas/UserTokenLogoutResponse' description: Successful response '401': content: @@ -137,10 +137,10 @@ paths: description: Authentication error tags: - User v1 - summary: User Token Refresh + summary: User Token Logout security: - ApiJwtAuth: [] - /v1/users/token/logout: + /v1/users/token/refresh: post: parameters: [] responses: @@ -148,7 +148,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/UserTokenLogoutResponse' + $ref: '#/components/schemas/UserTokenRefreshResponse' description: Successful response '401': content: @@ -158,7 +158,7 @@ paths: description: Authentication error tags: - User v1 - summary: User Token Logout + summary: User Token Refresh security: - ApiJwtAuth: [] /v1/opportunities/search: @@ -687,7 +687,7 @@ components: type: integer description: The HTTP status code example: 200 - UserTokenRefreshResponse: + UserTokenLogoutResponse: type: object properties: message: @@ -700,7 +700,7 @@ components: type: integer description: The HTTP status code example: 200 - UserTokenLogoutResponse: + UserTokenRefreshResponse: type: object properties: message: From d5d6d620d4e46068e678c353333adb1e6f2d1321 Mon Sep 17 00:00:00 2001 From: bruk Date: Fri, 22 Nov 2024 17:35:19 -0500 Subject: [PATCH 09/22] rename func add tokens session param --- api/src/auth/api_jwt_auth.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/src/auth/api_jwt_auth.py b/api/src/auth/api_jwt_auth.py index 5464a5f1e..8ce0ec164 100644 --- a/api/src/auth/api_jwt_auth.py +++ b/api/src/auth/api_jwt_auth.py @@ -208,10 +208,11 @@ def example_method(db_session: db.Session) -> response.ApiResponse: raise_flask_error(401, e.message) -def set_token_expiration_time(config: ApiJwtConfig | None = None) -> datetime: +def refresh_token_expiration(token_session: UserTokenSession, config: ApiJwtConfig | None = None) -> datetime: if config is None: config = get_config() expiration_time = datetime_util.utcnow() + timedelta(minutes=config.token_expiration_minutes) + token_session.expires_at = expiration_time - return expiration_time + return token_session From 2d25edba2151facbe7b05020ba94316c2de1f1b1 Mon Sep 17 00:00:00 2001 From: bruk Date: Fri, 22 Nov 2024 17:37:33 -0500 Subject: [PATCH 10/22] cleanup --- api/src/api/users/user_routes.py | 4 ++-- api/src/auth/api_jwt_auth.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/api/src/api/users/user_routes.py b/api/src/api/users/user_routes.py index e30341be1..2e06ad05e 100644 --- a/api/src/api/users/user_routes.py +++ b/api/src/api/users/user_routes.py @@ -7,7 +7,7 @@ from src.api.users import user_schemas from src.api.users.user_blueprint import user_blueprint from src.api.users.user_schemas import UserTokenLogoutResponseSchema, UserTokenRefreshResponseSchema -from src.auth.api_jwt_auth import api_jwt_auth, set_token_expiration_time +from src.auth.api_jwt_auth import api_jwt_auth, refresh_token_expiration from src.auth.api_key_auth import api_key_auth from src.db.models.user_models import UserTokenSession @@ -52,7 +52,7 @@ def user_token_refresh(db_session: db.Session) -> response.ApiResponse: user_token_session: UserTokenSession = api_jwt_auth.current_user # type: ignore with db_session.begin(): - user_token_session.expires_at = set_token_expiration_time() + refresh_token_expiration(user_token_session) db_session.add(user_token_session) logger.info( diff --git a/api/src/auth/api_jwt_auth.py b/api/src/auth/api_jwt_auth.py index 8ce0ec164..e201771e0 100644 --- a/api/src/auth/api_jwt_auth.py +++ b/api/src/auth/api_jwt_auth.py @@ -5,7 +5,6 @@ import jwt from apiflask import HTTPTokenAuth -from black import datetime from pydantic import Field from sqlalchemy import select from sqlalchemy.orm import selectinload @@ -208,7 +207,9 @@ def example_method(db_session: db.Session) -> response.ApiResponse: raise_flask_error(401, e.message) -def refresh_token_expiration(token_session: UserTokenSession, config: ApiJwtConfig | None = None) -> datetime: +def refresh_token_expiration( + token_session: UserTokenSession, config: ApiJwtConfig | None = None +) -> UserTokenSession: if config is None: config = get_config() From f2ed5d7f5c8911fb5f7cff942ec067fd9b8f7197 Mon Sep 17 00:00:00 2001 From: bruk Date: Mon, 25 Nov 2024 13:18:38 -0500 Subject: [PATCH 11/22] add response schema --- api/src/api/users/user_schemas.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/src/api/users/user_schemas.py b/api/src/api/users/user_schemas.py index 82fdc644a..7d295ec02 100644 --- a/api/src/api/users/user_schemas.py +++ b/api/src/api/users/user_schemas.py @@ -61,3 +61,6 @@ class UserTokenRefreshResponseSchema(AbstractResponseSchema): class UserTokenLogoutResponseSchema(AbstractResponseSchema): # No data returned data = fields.MixinField(metadata={"example": None}) + +class UserGetResponseSchema(AbstractResponseSchema): + data = fields.Nested(UserSchema()) \ No newline at end of file From 29e46edadb801ccc52cf7c9c0b1f285c32ec8225 Mon Sep 17 00:00:00 2001 From: bruk Date: Mon, 25 Nov 2024 13:24:33 -0500 Subject: [PATCH 12/22] add get user route --- api/src/api/users/user_routes.py | 43 +++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/api/src/api/users/user_routes.py b/api/src/api/users/user_routes.py index 2e06ad05e..aacf427a6 100644 --- a/api/src/api/users/user_routes.py +++ b/api/src/api/users/user_routes.py @@ -6,7 +6,8 @@ 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, UserTokenRefreshResponseSchema +from src.api.users.user_schemas import UserTokenLogoutResponseSchema, UserTokenRefreshResponseSchema, \ + UserGetResponseSchema from src.auth.api_jwt_auth import api_jwt_auth, refresh_token_expiration from src.auth.api_key_auth import api_key_auth from src.db.models.user_models import UserTokenSession @@ -41,22 +42,21 @@ def user_token(x_oauth_login_gov: dict) -> response.ApiResponse: raise_flask_error(400, message) -@user_blueprint.post("/token/refresh") -@user_blueprint.output(UserTokenRefreshResponseSchema) +@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_refresh(db_session: db.Session) -> response.ApiResponse: - logger.info("POST /v1/users/token/refresh") +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(): - refresh_token_expiration(user_token_session) + user_token_session.is_valid = False db_session.add(user_token_session) logger.info( - "Refreshed a user token", + "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), @@ -66,21 +66,22 @@ def user_token_refresh(db_session: db.Session) -> response.ApiResponse: return response.ApiResponse(message="Success") -@user_blueprint.post("/token/logout") -@user_blueprint.output(UserTokenLogoutResponseSchema) +@user_blueprint.post("/token/refresh") +@user_blueprint.output(UserTokenRefreshResponseSchema) @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") +def user_token_refresh(db_session: db.Session) -> response.ApiResponse: + logger.info("POST /v1/users/token/refresh") user_token_session: UserTokenSession = api_jwt_auth.current_user # type: ignore + with db_session.begin(): - user_token_session.is_valid = False + refresh_token_expiration(user_token_session) db_session.add(user_token_session) logger.info( - "Logged out a user", + "Refreshed a user token", extra={ "user_token_session.token_id": str(user_token_session.token_id), "user_token_session.user_id": str(user_token_session.user_id), @@ -88,3 +89,17 @@ def user_token_logout(db_session: db.Session) -> response.ApiResponse: ) return response.ApiResponse(message="Success") + +@user_blueprint.post("/") +@user_blueprint.output(UserGetResponseSchema) +@user_blueprint.doc(responses=[200, 401]) +@user_blueprint.auth_required(api_jwt_auth) +@flask_db.with_db_session() +def user_get(db_session: db.Session, user_id: int) -> response.ApiResponse: + logger.info("POST /v1/users/:user_id") + + with db_session.begin(): + user = get_user(db_session, user_id) + + + return response.ApiResponse(message="Success", data=user) From b1d9b9874ef1579be6721d338c67b80339ee740d Mon Sep 17 00:00:00 2001 From: bruk Date: Mon, 25 Nov 2024 18:02:34 -0500 Subject: [PATCH 13/22] add get user route --- api/src/api/users/user_routes.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/api/src/api/users/user_routes.py b/api/src/api/users/user_routes.py index aacf427a6..4ea39f647 100644 --- a/api/src/api/users/user_routes.py +++ b/api/src/api/users/user_routes.py @@ -1,4 +1,6 @@ import logging +from uuid import UUID + from src.adapters import db from src.adapters.db import flask_db @@ -11,6 +13,7 @@ from src.auth.api_jwt_auth import api_jwt_auth, refresh_token_expiration from src.auth.api_key_auth import api_key_auth from src.db.models.user_models import UserTokenSession +from src.services.users.get_user import get_user logger = logging.getLogger(__name__) @@ -90,16 +93,23 @@ def user_token_refresh(db_session: db.Session) -> response.ApiResponse: return response.ApiResponse(message="Success") -@user_blueprint.post("/") +@user_blueprint.get("/") @user_blueprint.output(UserGetResponseSchema) @user_blueprint.doc(responses=[200, 401]) @user_blueprint.auth_required(api_jwt_auth) @flask_db.with_db_session() -def user_get(db_session: db.Session, user_id: int) -> response.ApiResponse: - logger.info("POST /v1/users/:user_id") +def user_get(db_session: db.Session, user_id: UUID) -> response.ApiResponse: + logger.info("GET /v1/users/:user_id") + + user_token_session: UserTokenSession = api_jwt_auth.current_user # type: ignore + + if user_id == user_token_session.user_id: + with db_session.begin(): + user = get_user(db_session, user_id) + + return response.ApiResponse(message="Success", data=user) + + raise_flask_error(401, "Unauthorized") - with db_session.begin(): - user = get_user(db_session, user_id) - return response.ApiResponse(message="Success", data=user) From 8c6af6472995f36e3a345ad0b58bb96105ef7364 Mon Sep 17 00:00:00 2001 From: bruk Date: Mon, 25 Nov 2024 18:02:40 -0500 Subject: [PATCH 14/22] update schema --- api/src/api/users/user_schemas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/api/users/user_schemas.py b/api/src/api/users/user_schemas.py index 7d295ec02..3786bbc29 100644 --- a/api/src/api/users/user_schemas.py +++ b/api/src/api/users/user_schemas.py @@ -63,4 +63,4 @@ class UserTokenLogoutResponseSchema(AbstractResponseSchema): data = fields.MixinField(metadata={"example": None}) class UserGetResponseSchema(AbstractResponseSchema): - data = fields.Nested(UserSchema()) \ No newline at end of file + data = fields.Nested(UserSchema) \ No newline at end of file From 36e2147251add01dafeb3f4f50952d5ed075e66d Mon Sep 17 00:00:00 2001 From: bruk Date: Tue, 26 Nov 2024 11:53:32 -0500 Subject: [PATCH 15/22] cleanup --- api/src/api/users/user_routes.py | 16 ++++++++-------- api/src/api/users/user_schemas.py | 3 ++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/api/src/api/users/user_routes.py b/api/src/api/users/user_routes.py index 4ea39f647..d60601c01 100644 --- a/api/src/api/users/user_routes.py +++ b/api/src/api/users/user_routes.py @@ -1,15 +1,17 @@ import logging from uuid import UUID - 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, UserTokenRefreshResponseSchema, \ - UserGetResponseSchema +from src.api.users.user_schemas import ( + UserGetResponseSchema, + UserTokenLogoutResponseSchema, + UserTokenRefreshResponseSchema, +) from src.auth.api_jwt_auth import api_jwt_auth, refresh_token_expiration from src.auth.api_key_auth import api_key_auth from src.db.models.user_models import UserTokenSession @@ -93,6 +95,7 @@ def user_token_refresh(db_session: db.Session) -> response.ApiResponse: return response.ApiResponse(message="Success") + @user_blueprint.get("/") @user_blueprint.output(UserGetResponseSchema) @user_blueprint.doc(responses=[200, 401]) @@ -103,13 +106,10 @@ def user_get(db_session: db.Session, user_id: UUID) -> response.ApiResponse: user_token_session: UserTokenSession = api_jwt_auth.current_user # type: ignore - if user_id == user_token_session.user_id: + if user_token_session.user_id == user_id: with db_session.begin(): user = get_user(db_session, user_id) return response.ApiResponse(message="Success", data=user) - raise_flask_error(401, "Unauthorized") - - - + raise_flask_error(401, "Unauthorized user") diff --git a/api/src/api/users/user_schemas.py b/api/src/api/users/user_schemas.py index 3786bbc29..2555a7286 100644 --- a/api/src/api/users/user_schemas.py +++ b/api/src/api/users/user_schemas.py @@ -62,5 +62,6 @@ class UserTokenLogoutResponseSchema(AbstractResponseSchema): # No data returned data = fields.MixinField(metadata={"example": None}) + class UserGetResponseSchema(AbstractResponseSchema): - data = fields.Nested(UserSchema) \ No newline at end of file + data = fields.Nested(UserSchema) From eccdea6486b224b576eb3a7a9032f532dac1ac8e Mon Sep 17 00:00:00 2001 From: bruk Date: Tue, 26 Nov 2024 11:54:00 -0500 Subject: [PATCH 16/22] fix LinkExternalUserFactory --- api/tests/src/db/models/factories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/tests/src/db/models/factories.py b/api/tests/src/db/models/factories.py index 31d7ce48d..5a38fb95f 100644 --- a/api/tests/src/db/models/factories.py +++ b/api/tests/src/db/models/factories.py @@ -1856,7 +1856,7 @@ class Meta: user = factory.SubFactory(UserFactory) user_id = factory.LazyAttribute(lambda s: s.user.user_id) - external_user_type_id = factory.fuzzy.FuzzyChoice(ExternalUserType) + external_user_type = factory.fuzzy.FuzzyChoice(ExternalUserType) email = factory.Faker("email") From 4b83a0d0b3a5694cc53b424625611f87184545a3 Mon Sep 17 00:00:00 2001 From: bruk Date: Tue, 26 Nov 2024 11:54:24 -0500 Subject: [PATCH 17/22] cleanup --- api/tests/src/api/users/test_user_route_token.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 69385601d..b38faacaa 100644 --- a/api/tests/src/api/users/test_user_route_token.py +++ b/api/tests/src/api/users/test_user_route_token.py @@ -5,10 +5,11 @@ from src.auth.api_jwt_auth import create_jwt_for_user from tests.src.db.models.factories import UserFactory - ################## # POST /token ################## + + def test_post_user_route_token_200(client, api_auth_token): resp = client.post( "/v1/users/token", headers={"X-Auth": api_auth_token, "X-OAuth-login-gov": "test"} From 9566e1bf87bf0f3a44ace7de6d4becda6e6adce8 Mon Sep 17 00:00:00 2001 From: bruk Date: Tue, 26 Nov 2024 11:54:38 -0500 Subject: [PATCH 18/22] 200 and 401 test --- .../src/api/users/test_user_route_get.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 api/tests/src/api/users/test_user_route_get.py diff --git a/api/tests/src/api/users/test_user_route_get.py b/api/tests/src/api/users/test_user_route_get.py new file mode 100644 index 000000000..8a24beda6 --- /dev/null +++ b/api/tests/src/api/users/test_user_route_get.py @@ -0,0 +1,30 @@ +import uuid + +from src.auth.api_jwt_auth import create_jwt_for_user +from tests.src.db.models.factories import LinkExternalUserFactory + +################ +# GET user tests +################ + + +def test_get_user_200(enable_factory_create, client, db_session, api_auth_token): + external_user = LinkExternalUserFactory.create() + token, _ = create_jwt_for_user(external_user.user, db_session) + db_session.commit() + + resp = client.get(f"/v1/users/{external_user.user_id}", headers={"X-SGG-Token": token}) + + assert resp.status_code == 200 + assert resp.get_json()["data"]["user_id"] == str(external_user.user_id) + + +def test_get_user_401(enable_factory_create, client, db_session, api_auth_token): + external_user = LinkExternalUserFactory.create() + token, _ = create_jwt_for_user(external_user.user, db_session) + db_session.commit() + + random_uuid = str(uuid.uuid4()) + resp = client.get(f"/v1/users/{random_uuid}", headers={"X-SGG-Token": token}) + + assert resp.status_code == 401 From d120218bfdf2025b2de4fe44417a0594fe98b836 Mon Sep 17 00:00:00 2001 From: bruk Date: Tue, 26 Nov 2024 11:55:07 -0500 Subject: [PATCH 19/22] new user service --- api/src/services/users/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 api/src/services/users/__init__.py diff --git a/api/src/services/users/__init__.py b/api/src/services/users/__init__.py new file mode 100644 index 000000000..e69de29bb From 04fb3afb9d689d7b07e5623bfbd11ba3f78f5aad Mon Sep 17 00:00:00 2001 From: bruk Date: Tue, 26 Nov 2024 11:57:51 -0500 Subject: [PATCH 20/22] get user method to fetch returned object --- api/src/services/users/get_user.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 api/src/services/users/get_user.py diff --git a/api/src/services/users/get_user.py b/api/src/services/users/get_user.py new file mode 100644 index 000000000..3c28e282b --- /dev/null +++ b/api/src/services/users/get_user.py @@ -0,0 +1,25 @@ +import logging +from uuid import UUID + +from sqlalchemy import select + +from src.adapters import db +from src.api.route_utils import raise_flask_error +from src.db.models.user_models import LinkExternalUser + +logger = logging.getLogger(__name__) + + +def _fetch_user(db_session: db.Session, user_id: UUID) -> LinkExternalUser: + stmt = select(LinkExternalUser).where(LinkExternalUser.user_id == user_id) + + user = db_session.execute(stmt).scalar_one_or_none() + + if user is None: + raise_flask_error(404, message="User does not exist") + + return user + + +def get_user(db_session: db.Session, user_id: UUID) -> LinkExternalUser: + return _fetch_user(db_session, user_id) From 47decf53c2c83b725af768a1ec4f9af301978a5e Mon Sep 17 00:00:00 2001 From: nava-platform-bot Date: Tue, 26 Nov 2024 17:29:26 +0000 Subject: [PATCH 21/22] Create ERD diagram and Update OpenAPI spec --- api/openapi.generated.yml | 48 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/api/openapi.generated.yml b/api/openapi.generated.yml index ceabdda5e..ab83a8d54 100644 --- a/api/openapi.generated.yml +++ b/api/openapi.generated.yml @@ -315,6 +315,38 @@ paths: sort_direction: descending security: - ApiKeyAuth: [] + /v1/users/{user_id}: + get: + parameters: + - in: path + name: user_id + schema: + type: string + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UserGetResponse' + description: Successful response + '401': + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Authentication error + '404': + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Not found + tags: + - User v1 + summary: User Get + security: + - ApiJwtAuth: [] /v1/opportunities/{opportunity_id}: get: parameters: @@ -1490,6 +1522,22 @@ components: - object allOf: - $ref: '#/components/schemas/OpportunityFacetV1' + UserGetResponse: + type: object + properties: + message: + type: string + description: The message to return + example: Success + data: + type: + - object + allOf: + - $ref: '#/components/schemas/User' + status_code: + type: integer + description: The HTTP status code + example: 200 OpportunityAttachmentV1: type: object properties: From 60d743761f0ead8d3c2309c61b55683270583309 Mon Sep 17 00:00:00 2001 From: bruk Date: Tue, 26 Nov 2024 13:58:12 -0500 Subject: [PATCH 22/22] rm check for db user obj --- api/src/services/users/get_user.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/api/src/services/users/get_user.py b/api/src/services/users/get_user.py index 3c28e282b..125390574 100644 --- a/api/src/services/users/get_user.py +++ b/api/src/services/users/get_user.py @@ -1,25 +1,18 @@ -import logging from uuid import UUID from sqlalchemy import select from src.adapters import db -from src.api.route_utils import raise_flask_error from src.db.models.user_models import LinkExternalUser -logger = logging.getLogger(__name__) - -def _fetch_user(db_session: db.Session, user_id: UUID) -> LinkExternalUser: +def _fetch_user(db_session: db.Session, user_id: UUID) -> LinkExternalUser | None: stmt = select(LinkExternalUser).where(LinkExternalUser.user_id == user_id) user = db_session.execute(stmt).scalar_one_or_none() - if user is None: - raise_flask_error(404, message="User does not exist") - return user -def get_user(db_session: db.Session, user_id: UUID) -> LinkExternalUser: +def get_user(db_session: db.Session, user_id: UUID) -> LinkExternalUser | None: return _fetch_user(db_session, user_id)