diff --git a/bases/rsptx/assignment_server_api/routers/instructor.py b/bases/rsptx/assignment_server_api/routers/instructor.py index 111f57fe7..59b5ae99e 100644 --- a/bases/rsptx/assignment_server_api/routers/instructor.py +++ b/bases/rsptx/assignment_server_api/routers/instructor.py @@ -1463,11 +1463,20 @@ async def get_add_token_page( # Fetch all tokens for the course tokens = await fetch_all_api_tokens(course.id) - # Count tokens by provider - token_counts = {} + # Build the itemized token list for the template + token_list = [] for token in tokens: - provider = token.provider - token_counts[provider] = token_counts.get(provider, 0) + 1 + token_list.append( + { + "id": token.id, + "provider": token.provider, + "masked_token": ( + token.token[:4] + "****" + token.token[-4:] + if len(token.token) > 8 + else "****" + ), + } + ) total_tokens = len(tokens) @@ -1479,7 +1488,7 @@ async def get_add_token_page( "is_instructor": True, "student_page": False, "total_tokens": total_tokens, - "token_counts": token_counts, + "token_list": token_list, } return templates.TemplateResponse("assignment/instructor/add_token.html", context) @@ -1517,6 +1526,46 @@ async def delete_course_tokens( ) +@router.delete("/delete_token/{token_id}") +@instructor_role_required() +@with_course() +async def delete_single_token( + token_id: int, + request: Request, + course=None, +): + """ + Delete a specific API token for the instructor's course. + + :param token_id: ID of the token to delete + :param course: Course object from decorator + :return: JSON response with success status + """ + try: + deleted = await delete_api_token(course_id=course.id, token_id=token_id) + if deleted: + return make_json_response( + status=status.HTTP_200_OK, + detail={ + "status": "success", + "message": "Token deleted successfully", + }, + ) + else: + return make_json_response( + status=status.HTTP_404_NOT_FOUND, + detail="Token not found", + ) + except Exception as e: + rslogger.error( + f"Error deleting API token {token_id} for course {course.id}: {e}" + ) + return make_json_response( + status=status.HTTP_400_BAD_REQUEST, + detail=f"Error deleting token: {str(e)}", + ) + + @router.delete("/assignments/{assignment_id}") @instructor_role_required() async def remove_assignment(assignment_id: int, request: Request): diff --git a/components/rsptx/templates/assignment/instructor/add_token.html b/components/rsptx/templates/assignment/instructor/add_token.html index 1e6e1e74a..9a7d18539 100644 --- a/components/rsptx/templates/assignment/instructor/add_token.html +++ b/components/rsptx/templates/assignment/instructor/add_token.html @@ -137,6 +137,43 @@ font-size: 16px; margin-bottom: 10px; } + + .token-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + margin-bottom: 6px; + background: #fff; + border: 1px solid #dee2e6; + border-radius: 4px; + } + + .token-item .token-provider { + font-weight: 600; + min-width: 100px; + } + + .token-item .token-masked { + font-family: monospace; + color: #6c757d; + flex: 1; + } + + .token-item .remove-token-btn { + background-color: #dc3545; + color: white; + border: none; + padding: 4px 10px; + border-radius: 4px; + cursor: pointer; + font-size: 13px; + margin-left: 0; + } + + .token-item .remove-token-btn:hover { + background-color: #c82333; + } {% endblock %} @@ -151,12 +188,16 @@