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

[Issue 2676] Create a GET /users/:userID endpoint #3041

Merged
merged 31 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e13d7b5
add response schema
babebe Nov 22, 2024
5c9cba7
add token refresh endpoint
babebe Nov 22, 2024
ad4b593
add suceess and expired token test
babebe Nov 22, 2024
aa8a192
cleanup
babebe Nov 22, 2024
67ef19b
Create ERD diagram and Update OpenAPI spec
nava-platform-bot Nov 22, 2024
6821a01
created method to get expiration time
babebe Nov 22, 2024
695427d
clean up
babebe Nov 22, 2024
47a473e
Merge branch '2817/refresh' of https://github.com/HHS/simpler-grants-…
babebe Nov 22, 2024
15c1b4b
clean up
babebe Nov 22, 2024
1bd2f84
Create ERD diagram and Update OpenAPI spec
nava-platform-bot Nov 22, 2024
d5d6d62
rename func add tokens session param
babebe Nov 22, 2024
2d25edb
cleanup
babebe Nov 22, 2024
ea1b756
Merge branch '2817/refresh' of https://github.com/HHS/simpler-grants-…
babebe Nov 22, 2024
5a44d5c
Merge branch 'main' of https://github.com/HHS/simpler-grants-gov into…
babebe Nov 25, 2024
4e9c340
Merge branch 'main' of https://github.com/HHS/simpler-grants-gov into…
babebe Nov 25, 2024
f2ed5d7
add response schema
babebe Nov 25, 2024
29e46ed
add get user route
babebe Nov 25, 2024
b1d9b98
add get user route
babebe Nov 25, 2024
8c6af64
update schema
babebe Nov 25, 2024
36e2147
cleanup
babebe Nov 26, 2024
eccdea6
fix LinkExternalUserFactory
babebe Nov 26, 2024
4b83a0d
cleanup
babebe Nov 26, 2024
9566e1b
200 and 401 test
babebe Nov 26, 2024
d120218
new user service
babebe Nov 26, 2024
04fb3af
get user method to fetch returned object
babebe Nov 26, 2024
21526ca
merge main, fix merge conflict
babebe Nov 26, 2024
47decf5
Create ERD diagram and Update OpenAPI spec
nava-platform-bot Nov 26, 2024
60d7437
rm check for db user obj
babebe Nov 26, 2024
ecde085
Merge branch '2676/get-user-endpoint' of https://github.com/HHS/simpl…
babebe Nov 26, 2024
696d01d
Merge branch 'main' of https://github.com/HHS/simpler-grants-gov into…
babebe Nov 26, 2024
ba94509
Merge branch 'main' of https://github.com/HHS/simpler-grants-gov into…
babebe Nov 26, 2024
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
48 changes: 48 additions & 0 deletions api/openapi.generated.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
53 changes: 39 additions & 14 deletions api/src/api/users/user_routes.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
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
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
from src.services.users.get_user import get_user

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -41,22 +47,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),
Expand All @@ -66,25 +71,45 @@ 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),
},
)

return response.ApiResponse(message="Success")


@user_blueprint.get("/<uuid:user_id>")
@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: UUID) -> response.ApiResponse:
logger.info("GET /v1/users/:user_id")

user_token_session: UserTokenSession = api_jwt_auth.current_user # type: ignore

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 user")
4 changes: 4 additions & 0 deletions api/src/api/users/user_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,7 @@ class UserTokenRefreshResponseSchema(AbstractResponseSchema):
class UserTokenLogoutResponseSchema(AbstractResponseSchema):
# No data returned
data = fields.MixinField(metadata={"example": None})


class UserGetResponseSchema(AbstractResponseSchema):
data = fields.Nested(UserSchema)
Empty file.
18 changes: 18 additions & 0 deletions api/src/services/users/get_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from uuid import UUID

from sqlalchemy import select

from src.adapters import db
from src.db.models.user_models import 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()

return user


def get_user(db_session: db.Session, user_id: UUID) -> LinkExternalUser | None:
return _fetch_user(db_session, user_id)
30 changes: 30 additions & 0 deletions api/tests/src/api/users/test_user_route_get.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion api/tests/src/db/models/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -1858,7 +1858,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)
babebe marked this conversation as resolved.
Show resolved Hide resolved

email = factory.Faker("email")

Expand Down