Add logout and fixed user specific requirements retrieval
This commit is contained in:
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user