diff --git a/bases/rsptx/assignment_server_api/routers/instructor.py b/bases/rsptx/assignment_server_api/routers/instructor.py index 82230c292..b9fb254aa 100644 --- a/bases/rsptx/assignment_server_api/routers/instructor.py +++ b/bases/rsptx/assignment_server_api/routers/instructor.py @@ -1438,11 +1438,54 @@ async def get_add_token_page( "student_page": False, "total_tokens": total_tokens, "token_counts": token_counts, + "tokens": tokens, } return templates.TemplateResponse("assignment/instructor/add_token.html", context) + +@router.delete("/delete_token/{token_id}") +@instructor_role_required() +@with_course() +async def delete_single_token( + request: Request, + token_id: int, + course=None, +): + """ + Delete a single API token for the instructor's course. + + :param token_id: int, the id of the token to delete + :param course: Course object from decorator + :return: JSON response with success status + """ + try: + deleted_count = await delete_api_token(course_id=course.id, token_id=token_id) + + if deleted_count == 0: + return make_json_response( + status=status.HTTP_404_NOT_FOUND, + detail={ + "status": "error", + "message": "Token not found", + }, + ) + + return make_json_response( + status=status.HTTP_200_OK, + detail={ + "status": "success", + "message": "Token deleted successfully", + "deleted_count": deleted_count, + }, + ) + 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("/delete_tokens") @instructor_role_required() @with_course() diff --git a/components/rsptx/templates/assignment/instructor/add_token.html b/components/rsptx/templates/assignment/instructor/add_token.html index 1e6e1e74a..3bf7333c9 100644 --- a/components/rsptx/templates/assignment/instructor/add_token.html +++ b/components/rsptx/templates/assignment/instructor/add_token.html @@ -158,6 +158,28 @@

Current Tokens

{% endfor %} {% endif %} + + + + + + + + + + + {% for token in tokens %} + + + + + + + {% endfor %} + +
ProviderTokenLast UsedActions
{{ token.provider }}****{{ token.token[-4:] if token.token else '****' }}{{ token.last_used or 'Never' }} + +
{% endif %} @@ -330,6 +352,38 @@

Current Tokens

} }); + async function deleteToken(tokenId) { + if (!confirm('Are you sure you want to delete this API token?')) { + return; + } + + try { + const response = await fetch(`/assignment/instructor/delete_token/${tokenId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + } + }); + + const data = await response.json(); + + if (response.ok) { + showAlert(data.detail.message, 'success'); + // Remove the row from the table without reloading + const row = document.querySelector(`tr[data-token-id="${tokenId}"]`); + if (row) row.remove(); + // Reload the page after a short delay to ensure counts are updated + setTimeout(() => { + window.location.reload(); + }, 1500); + } else { + throw new Error(data.detail || 'Failed to delete token'); + } + } catch (error) { + showAlert(error.message, 'danger'); + } + } + async function deleteAllTokens() { if (!confirm('Are you sure you want to delete ALL API tokens for this course? This action cannot be undone.')) { return;