Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
43 changes: 43 additions & 0 deletions bases/rsptx/assignment_server_api/routers/instructor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)}",
)
Comment on lines +1463 to +1488
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete_single_token mixes response shapes: success/404 return detail as an object with message, but the exception path returns detail as a plain string. This inconsistency makes client handling brittle (and currently the UI assumes a consistent shape). Return a consistent JSON object for all outcomes (e.g., always {status, message, ...} under detail).

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s no blank line between the end of delete_single_token and the next route decorator @router.delete("/delete_tokens"). This breaks the spacing pattern used elsewhere in this module (e.g., a blank line separates route functions around instructor.py:1340-1348 and 1444-1448) and makes the route boundary easy to miss. Add a blank line before the next decorator for readability/consistency.

Suggested change
)
)

Copilot uses AI. Check for mistakes.
@router.delete("/delete_tokens")
@instructor_role_required()
@with_course()
Expand Down
54 changes: 54 additions & 0 deletions components/rsptx/templates/assignment/instructor/add_token.html
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,28 @@ <h3>Current Tokens</h3>
{% endfor %}
</ul>
{% endif %}
<table class="table table-sm mt-3">
<thead>
<tr>
<th>Provider</th>
<th>Token</th>
<th>Last Used</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for token in tokens %}
<tr data-token-id="{{ token.id }}">
<td>{{ token.provider }}</td>
<td>****{{ token.token[-4:] if token.token else '****' }}</td>
<td>{{ token.last_used or 'Never' }}</td>
<td>
<button type="button" class="btn btn-danger btn-sm" onclick="deleteToken({{ token.id }})">Delete</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}

Expand Down Expand Up @@ -330,6 +352,38 @@ <h3>Current Tokens</h3>
}
});

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();

Comment on lines +360 to +369
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleteToken unconditionally calls await response.json(). If the session expires or the server returns a non-JSON response (e.g., HTML redirect/login), this will throw a JSON parse error and show a confusing message to the user. Consider checking response.headers.get('content-type') before parsing, and fall back to await response.text() (or a generic auth error) when the response isn't JSON.

Copilot uses AI. Check for mistakes.
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');
}
Comment on lines +368 to +381
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In deleteToken, the non-OK path throws new Error(data.detail || ...), but this endpoint can return detail as an object (e.g., 404 returns {status,message}), which will surface to users as [object Object]. Update the error handling to extract a message consistently (e.g., data.detail.message when detail is an object), or standardize the API response shape so the frontend can always read detail.message.

Copilot uses AI. Check for mistakes.
} 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;
Expand Down
Loading