Add logout and fixed user specific requirements retrieval

This commit is contained in:
gulimabr
2025-11-30 19:24:06 -03:00
parent eb70598cab
commit 67efbfd317
4 changed files with 148 additions and 27 deletions

View File

@@ -126,13 +126,25 @@ class AuthController:
@staticmethod @staticmethod
def logout() -> JSONResponse: def logout() -> JSONResponse:
""" """
Logout the user by clearing the authentication cookie. Logout the user by clearing the authentication cookie and returning
the Keycloak logout URL for full session termination.
Returns: Returns:
JSONResponse: Success message with cookie cleared. JSONResponse: Contains the Keycloak logout URL and clears the cookie.
""" """
# Build Keycloak logout URL
keycloak_logout_url = (
f"{settings.keycloak_external_url}realms/{settings.keycloak_realm}"
f"/protocol/openid-connect/logout"
f"?client_id={settings.keycloak_client_id}"
f"&post_logout_redirect_uri={settings.frontend_url}"
)
response = JSONResponse( response = JSONResponse(
content={"message": "Successfully logged out"}, content={
"message": "Successfully logged out",
"logout_url": keycloak_logout_url
},
status_code=status.HTTP_200_OK status_code=status.HTTP_200_OK
) )

View File

@@ -266,34 +266,52 @@ def _build_requirement_response(req) -> RequirementResponse:
@app.get("/api/requirements", response_model=List[RequirementResponse]) @app.get("/api/requirements", response_model=List[RequirementResponse])
async def get_requirements( async def get_requirements(
request: Request,
group_id: Optional[int] = None, group_id: Optional[int] = None,
tag_id: Optional[int] = None, tag_id: Optional[int] = None,
db: AsyncSession = Depends(get_db) db: AsyncSession = Depends(get_db)
): ):
""" """
Get all requirements, optionally filtered by group or tag. Get all requirements for the authenticated user, optionally filtered by group or tag.
Args: Args:
group_id: Optional group ID to filter by group_id: Optional group ID to filter by
tag_id: Optional tag ID to filter by tag_id: Optional tag ID to filter by
Returns: Returns:
List of all requirements with their related data. List of requirements owned by the authenticated user.
""" """
# Get the current user from cookie
user_info = AuthController.get_current_user(request)
# Get the user's database ID
from src.repositories import UserRepository
user_repo = UserRepository(db)
user = await user_repo.get_by_sub(user_info.sub)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found in database"
)
req_repo = RequirementRepository(db) req_repo = RequirementRepository(db)
if group_id: if group_id:
requirements = await req_repo.get_by_group_id(group_id) requirements = await req_repo.get_by_group_id(group_id, user_id=user.id)
elif tag_id: elif tag_id:
requirements = await req_repo.get_by_tag_id(tag_id) requirements = await req_repo.get_by_tag_id(tag_id, user_id=user.id)
else: else:
requirements = await req_repo.get_all() requirements = await req_repo.get_by_user_id(user.id)
return [_build_requirement_response(req) for req in requirements] return [_build_requirement_response(req) for req in requirements]
@app.get("/api/requirements/{requirement_id}", response_model=RequirementResponse) @app.get("/api/requirements/{requirement_id}", response_model=RequirementResponse)
async def get_requirement(requirement_id: int, db: AsyncSession = Depends(get_db)): async def get_requirement(
requirement_id: int,
request: Request,
db: AsyncSession = Depends(get_db)
):
""" """
Get a specific requirement by ID. Get a specific requirement by ID.
@@ -301,8 +319,21 @@ async def get_requirement(requirement_id: int, db: AsyncSession = Depends(get_db
requirement_id: The requirement ID requirement_id: The requirement ID
Returns: Returns:
The requirement if found. The requirement if found and owned by the authenticated user.
""" """
# Get the current user from cookie
user_info = AuthController.get_current_user(request)
# Get the user's database ID
from src.repositories import UserRepository
user_repo = UserRepository(db)
user = await user_repo.get_by_sub(user_info.sub)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found in database"
)
req_repo = RequirementRepository(db) req_repo = RequirementRepository(db)
requirement = await req_repo.get_by_id(requirement_id) requirement = await req_repo.get_by_id(requirement_id)
if not requirement: if not requirement:
@@ -310,6 +341,14 @@ async def get_requirement(requirement_id: int, db: AsyncSession = Depends(get_db
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Requirement with id {requirement_id} not found" detail=f"Requirement with id {requirement_id} not found"
) )
# Verify user owns this requirement
if requirement.user_id != user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You do not have permission to access this requirement"
)
return _build_requirement_response(requirement) return _build_requirement_response(requirement)
@@ -386,6 +425,21 @@ async def update_requirement(
) )
req_repo = RequirementRepository(db) req_repo = RequirementRepository(db)
# First check if requirement exists and user owns it
existing_req = await req_repo.get_by_id(requirement_id)
if not existing_req:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Requirement with id {requirement_id} not found"
)
if existing_req.user_id != user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You do not have permission to update this requirement"
)
requirement = await req_repo.update( requirement = await req_repo.update(
requirement_id=requirement_id, requirement_id=requirement_id,
editor_id=user.id, editor_id=user.id,
@@ -396,12 +450,6 @@ async def update_requirement(
group_ids=req_data.group_ids, group_ids=req_data.group_ids,
) )
if not requirement:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Requirement with id {requirement_id} not found"
)
await db.commit() await db.commit()
return _build_requirement_response(requirement) return _build_requirement_response(requirement)
@@ -418,16 +466,34 @@ async def delete_requirement(
Args: Args:
requirement_id: The requirement ID to delete requirement_id: The requirement ID to delete
""" """
# Verify user is authenticated # Get the current user from cookie
AuthController.get_current_user(request) user_info = AuthController.get_current_user(request)
# Get the user's database ID
from src.repositories import UserRepository
user_repo = UserRepository(db)
user = await user_repo.get_by_sub(user_info.sub)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found in database"
)
req_repo = RequirementRepository(db) req_repo = RequirementRepository(db)
deleted = await req_repo.delete(requirement_id)
if not deleted: # First check if requirement exists and user owns it
existing_req = await req_repo.get_by_id(requirement_id)
if not existing_req:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail=f"Requirement with id {requirement_id} not found" detail=f"Requirement with id {requirement_id} not found"
) )
if existing_req.user_id != user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You do not have permission to delete this requirement"
)
await req_repo.delete(requirement_id)
await db.commit() await db.commit()

View File

@@ -36,6 +36,29 @@ class RequirementRepository:
) )
return list(result.scalars().all()) return list(result.scalars().all())
async def get_by_user_id(self, user_id: int) -> List[Requirement]:
"""
Get all requirements for a specific user.
Args:
user_id: The user's database ID
Returns:
List of requirements owned by the user
"""
result = await self.session.execute(
select(Requirement)
.options(
selectinload(Requirement.tag),
selectinload(Requirement.priority),
selectinload(Requirement.groups),
selectinload(Requirement.validations).selectinload(Validation.status),
)
.where(Requirement.user_id == user_id)
.order_by(Requirement.created_at.desc())
)
return list(result.scalars().all())
async def get_by_id(self, requirement_id: int) -> Optional[Requirement]: async def get_by_id(self, requirement_id: int) -> Optional[Requirement]:
""" """
Get a requirement by ID with its related data. Get a requirement by ID with its related data.
@@ -60,17 +83,18 @@ class RequirementRepository:
) )
return result.scalar_one_or_none() return result.scalar_one_or_none()
async def get_by_group_id(self, group_id: int) -> List[Requirement]: async def get_by_group_id(self, group_id: int, user_id: Optional[int] = None) -> List[Requirement]:
""" """
Get all requirements belonging to a specific group. Get all requirements belonging to a specific group.
Args: Args:
group_id: The group ID group_id: The group ID
user_id: Optional user ID to filter by
Returns: Returns:
List of requirements in the group List of requirements in the group
""" """
result = await self.session.execute( query = (
select(Requirement) select(Requirement)
.options( .options(
selectinload(Requirement.tag), selectinload(Requirement.tag),
@@ -80,21 +104,27 @@ class RequirementRepository:
) )
.join(Requirement.groups) .join(Requirement.groups)
.where(Group.id == group_id) .where(Group.id == group_id)
.order_by(Requirement.created_at.desc())
) )
if user_id is not None:
query = query.where(Requirement.user_id == user_id)
query = query.order_by(Requirement.created_at.desc())
result = await self.session.execute(query)
return list(result.scalars().all()) return list(result.scalars().all())
async def get_by_tag_id(self, tag_id: int) -> List[Requirement]: async def get_by_tag_id(self, tag_id: int, user_id: Optional[int] = None) -> List[Requirement]:
""" """
Get all requirements with a specific tag. Get all requirements with a specific tag.
Args: Args:
tag_id: The tag ID tag_id: The tag ID
user_id: Optional user ID to filter by
Returns: Returns:
List of requirements with the tag List of requirements with the tag
""" """
result = await self.session.execute( query = (
select(Requirement) select(Requirement)
.options( .options(
selectinload(Requirement.tag), selectinload(Requirement.tag),
@@ -103,8 +133,13 @@ class RequirementRepository:
selectinload(Requirement.validations).selectinload(Validation.status), selectinload(Requirement.validations).selectinload(Validation.status),
) )
.where(Requirement.tag_id == tag_id) .where(Requirement.tag_id == tag_id)
.order_by(Requirement.created_at.desc())
) )
if user_id is not None:
query = query.where(Requirement.user_id == user_id)
query = query.order_by(Requirement.created_at.desc())
result = await self.session.execute(query)
return list(result.scalars().all()) return list(result.scalars().all())
async def create( async def create(

View File

@@ -33,7 +33,8 @@ class AuthService {
} }
/** /**
* Logout the current user by clearing the session cookie. * Logout the current user by clearing the session cookie
* and redirecting to Keycloak logout to end the SSO session.
*/ */
async logout(): Promise<void> { async logout(): Promise<void> {
try { try {
@@ -48,6 +49,13 @@ class AuthService {
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`) throw new Error(`HTTP error! status: ${response.status}`)
} }
const data = await response.json()
// Redirect to Keycloak logout URL to end the SSO session
if (data.logout_url) {
window.location.href = data.logout_url
}
} catch (error) { } catch (error) {
console.error('Logout failed:', error) console.error('Logout failed:', error)
throw error throw error