Skip to content
Merged
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
59 changes: 54 additions & 5 deletions bases/rsptx/assignment_server_api/routers/instructor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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,
}
Comment thread
Mr-Neutr0n marked this conversation as resolved.

return templates.TemplateResponse("assignment/instructor/add_token.html", context)
Expand Down Expand Up @@ -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):
Expand Down
96 changes: 91 additions & 5 deletions components/rsptx/templates/assignment/instructor/add_token.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
</style>
{% endblock %}

Expand All @@ -151,12 +188,16 @@ <h1>Manage API Tokens</h1>
<div class="token-info">
<h3>Current Tokens</h3>
<div class="total">Total tokens stored: {{ total_tokens }}</div>
{% if token_counts %}
<ul>
{% for provider, count in token_counts.items() %}
<li>{{ provider }}: {{ count }} token{{ 's' if count != 1 else '' }}</li>
{% if token_list %}
<div class="token-list">
{% for token in token_list %}
<div class="token-item" data-token-id="{{ token.id }}">
<span class="token-provider">{{ token.provider }}</span>
<span class="token-masked">{{ token.masked_token }}</span>
<button type="button" class="remove-token-btn" onclick="deleteSingleToken({{ token.id }})">Delete</button>
</div>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
{% endif %}
Expand Down Expand Up @@ -198,6 +239,7 @@ <h3>Current Tokens</h3>

<script>
let tokenCount = 1;
let submitting = false;

document.getElementById('provider').addEventListener('change', function () {
const customProviderDiv = document.getElementById('custom-provider');
Expand Down Expand Up @@ -268,6 +310,8 @@ <h3>Current Tokens</h3>

document.getElementById('token-form').addEventListener('submit', async function (e) {
e.preventDefault();
if (submitting) return;
submitting = true;

const submitBtn = document.getElementById('submit-btn');
submitBtn.disabled = true;
Expand Down Expand Up @@ -325,6 +369,7 @@ <h3>Current Tokens</h3>
} catch (error) {
showAlert(error.message, 'danger');
} finally {
submitting = false;
submitBtn.disabled = false;
submitBtn.textContent = 'Save Tokens';
}
Expand Down Expand Up @@ -366,6 +411,47 @@ <h3>Current Tokens</h3>
}
}

async function deleteSingleToken(tokenId) {
if (!confirm('Are you sure you want to delete this API token? This action cannot be undone.')) {
return;
}

const tokenItem = document.querySelector(`.token-item[data-token-id="${tokenId}"]`);
const deleteBtn = tokenItem ? tokenItem.querySelector('.remove-token-btn') : null;
if (deleteBtn) {
deleteBtn.disabled = true;
deleteBtn.textContent = 'Deleting...';
}

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');
// Reload the page after a short delay to update the token list
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
throw new Error(data.detail || 'Failed to delete token');
}

} catch (error) {
showAlert(error.message, 'danger');
if (deleteBtn) {
deleteBtn.disabled = false;
deleteBtn.textContent = 'Delete';
}
}
}

// Show/hide password functionality
document.addEventListener('click', function (e) {
if (e.target.classList.contains('toggle-password')) {
Expand Down
Loading