Refactor authentication for DICOM endpoints to use TokenAuth and enhance error handling in upload process
This commit is contained in:
+46
-42
@@ -129,7 +129,7 @@ class CaseSchema(ModelSchema):
|
|||||||
fields = ["id", "title"]
|
fields = ["id", "title"]
|
||||||
|
|
||||||
|
|
||||||
@router.post("/upload_dicom", auth=BearerAuth())
|
@router.post("/upload_dicom", auth=TokenAuth())
|
||||||
def upload_dicom(request, files: List[UploadedFile] = File(...)):
|
def upload_dicom(request, files: List[UploadedFile] = File(...)):
|
||||||
uploaded = []
|
uploaded = []
|
||||||
duplicate = []
|
duplicate = []
|
||||||
@@ -162,7 +162,7 @@ def upload_dicom(request, files: List[UploadedFile] = File(...)):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/generate_image_hash", auth=BearerAuth())
|
@router.post("/generate_image_hash", auth=TokenAuth())
|
||||||
def generate_image_hash(request, id: int):
|
def generate_image_hash(request, id: int):
|
||||||
s = SeriesImage.objects.get(pk=id)
|
s = SeriesImage.objects.get(pk=id)
|
||||||
s.generate_hashes()
|
s.generate_hashes()
|
||||||
@@ -174,7 +174,7 @@ def generate_image_hash(request, id: int):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/clear_dicoms", auth=BearerAuth())
|
@router.post("/clear_dicoms", auth=TokenAuth())
|
||||||
def clear_dicoms(request):
|
def clear_dicoms(request):
|
||||||
if "selection" in request.POST:
|
if "selection" in request.POST:
|
||||||
dicoms = UncategorisedDicom.objects.filter(
|
dicoms = UncategorisedDicom.objects.filter(
|
||||||
@@ -189,7 +189,7 @@ def clear_dicoms(request):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@router.get("/uncategorised_dicoms", auth=BearerAuth())
|
@router.get("/uncategorised_dicoms", auth=TokenAuth())
|
||||||
def uncategorised_dicoms(request):
|
def uncategorised_dicoms(request):
|
||||||
dicoms = UncategorisedDicom.objects.filter(user=request.user)
|
dicoms = UncategorisedDicom.objects.filter(user=request.user)
|
||||||
|
|
||||||
@@ -291,7 +291,7 @@ def import_dicoms_helper(request, case_id: int | None = None, dicoms=None):
|
|||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/import_dicoms", auth=BearerAuth(), response=List[Tuple[SeriesSchema, str]]
|
"/import_dicoms", auth=TokenAuth(), response=List[Tuple[SeriesSchema, str]]
|
||||||
)
|
)
|
||||||
def import_dicoms(request):
|
def import_dicoms(request):
|
||||||
return import_dicoms_helper(request)
|
return import_dicoms_helper(request)
|
||||||
@@ -299,14 +299,14 @@ def import_dicoms(request):
|
|||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/import_dicoms/{case_id}",
|
"/import_dicoms/{case_id}",
|
||||||
auth=BearerAuth(),
|
auth=TokenAuth(),
|
||||||
response=List[Tuple[SeriesSchema, str]],
|
response=List[Tuple[SeriesSchema, str]],
|
||||||
)
|
)
|
||||||
def import_dicoms_case(request, case_id: int):
|
def import_dicoms_case(request, case_id: int):
|
||||||
return import_dicoms_helper(request, case_id=case_id)
|
return import_dicoms_helper(request, case_id=case_id)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/upload_dicom_case/{case_id}", auth=BearerAuth())
|
@router.post("/upload_dicom_case/{case_id}", auth=TokenAuth())
|
||||||
def upload_dicom_case(request, case_id: int, files: List[UploadedFile] = File(...)):
|
def upload_dicom_case(request, case_id: int, files: List[UploadedFile] = File(...)):
|
||||||
"""Upload DICOM files and immediately import them into the given case.
|
"""Upload DICOM files and immediately import them into the given case.
|
||||||
|
|
||||||
@@ -360,12 +360,12 @@ def upload_dicom_case(request, case_id: int, files: List[UploadedFile] = File(..
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/orphan_series", auth=BearerAuth(), response=List[SeriesSchema])
|
@router.get("/orphan_series", auth=TokenAuth(), response=List[SeriesSchema])
|
||||||
def orphan_series(request):
|
def orphan_series(request):
|
||||||
return request.user.series.filter(case=None)
|
return request.user.series.filter(case=None)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/series_remove_duplicate_images", auth=BearerAuth())
|
@router.get("/series_remove_duplicate_images", auth=TokenAuth())
|
||||||
def series_remove_duplicate_images(request, series_id: int):
|
def series_remove_duplicate_images(request, series_id: int):
|
||||||
series = get_object_or_404(Series, pk=series_id)
|
series = get_object_or_404(Series, pk=series_id)
|
||||||
|
|
||||||
@@ -382,12 +382,12 @@ def series_remove_duplicate_images(request, series_id: int):
|
|||||||
return len(dupes)
|
return len(dupes)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/get_cases_user", auth=BearerAuth(), response=List[CaseSchema])
|
@router.get("/get_cases_user", auth=TokenAuth(), response=List[CaseSchema])
|
||||||
def get_cases_user(request):
|
def get_cases_user(request):
|
||||||
return Case.objects.filter(author=request.user)
|
return Case.objects.filter(author=request.user)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/get_cases_available", auth=BearerAuth(), response=List[CaseSchema])
|
@router.get("/get_cases_available", auth=TokenAuth(), response=List[CaseSchema])
|
||||||
def get_cases_available(request):
|
def get_cases_available(request):
|
||||||
"""Return cases available to the authenticated user (via get_cases_available_to_user)."""
|
"""Return cases available to the authenticated user (via get_cases_available_to_user)."""
|
||||||
logger.error(f"Getting cases available to user {request.user}")
|
logger.error(f"Getting cases available to user {request.user}")
|
||||||
@@ -405,7 +405,7 @@ class APITokenOut(Schema):
|
|||||||
last_used: str | None = None
|
last_used: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api_tokens", auth=BearerAuth(), response=List[APITokenOut])
|
@router.get("/api_tokens", auth=TokenAuth(), response=List[APITokenOut])
|
||||||
def list_api_tokens(request):
|
def list_api_tokens(request):
|
||||||
tokens = APIToken.objects.filter(user=request.user).order_by("-created")
|
tokens = APIToken.objects.filter(user=request.user).order_by("-created")
|
||||||
out = []
|
out = []
|
||||||
@@ -552,7 +552,7 @@ def token_check(request):
|
|||||||
# return {"token": token, "id": obj.id}
|
# return {"token": token, "id": obj.id}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api_tokens/{token_id}/revoke", auth=BearerAuth())
|
@router.post("/api_tokens/{token_id}/revoke", auth=TokenAuth())
|
||||||
def revoke_api_token(request, token_id: int):
|
def revoke_api_token(request, token_id: int):
|
||||||
try:
|
try:
|
||||||
t = APIToken.objects.get(pk=token_id, user=request.user)
|
t = APIToken.objects.get(pk=token_id, user=request.user)
|
||||||
@@ -563,7 +563,7 @@ def revoke_api_token(request, token_id: int):
|
|||||||
return {"status": "revoked"}
|
return {"status": "revoked"}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/check_image_hash/{hash}", auth=BearerAuth())
|
@router.get("/check_image_hash/{hash}", auth=TokenAuth())
|
||||||
def check_image_hash(request, hash: str):
|
def check_image_hash(request, hash: str):
|
||||||
try:
|
try:
|
||||||
series_image = SeriesImage.objects.get(image_blake3_hash=hash)
|
series_image = SeriesImage.objects.get(image_blake3_hash=hash)
|
||||||
@@ -578,7 +578,7 @@ def check_image_hash(request, hash: str):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@router.post("/check_image_hashes/", auth=BearerAuth())
|
@router.post("/check_image_hashes/", auth=TokenAuth())
|
||||||
def check_images_hashes(request, hashes: List[str]):
|
def check_images_hashes(request, hashes: List[str]):
|
||||||
"""Checks a list of image hashes and returns the series id / url if found
|
"""Checks a list of image hashes and returns the series id / url if found
|
||||||
|
|
||||||
@@ -586,33 +586,37 @@ def check_images_hashes(request, hashes: List[str]):
|
|||||||
{ "hash_id": {"id": "series_id|false", "url": "series_url|false"}, ...}
|
{ "hash_id": {"id": "series_id|false", "url": "series_url|false"}, ...}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
hash_status = {}
|
try:
|
||||||
|
hash_status = {}
|
||||||
|
|
||||||
for hash in hashes:
|
for hash in hashes:
|
||||||
try:
|
# Prefer filter().first() to avoid MultipleObjectsReturned
|
||||||
# TOOD also check against uncategorised dicoms?
|
series_image = SeriesImage.objects.filter(image_blake3_hash=hash).select_related("series").first()
|
||||||
series_image = SeriesImage.objects.get(image_blake3_hash=hash)
|
if series_image:
|
||||||
data = {
|
try:
|
||||||
"id": series_image.pk,
|
url = series_image.series.get_absolute_url()
|
||||||
"url": series_image.series.get_absolute_url(),
|
except Exception:
|
||||||
"type": "series",
|
url = None
|
||||||
}
|
data = {"id": series_image.pk, "url": url, "type": "series"}
|
||||||
except SeriesImage.DoesNotExist:
|
hash_status[hash] = data
|
||||||
try:
|
continue
|
||||||
uncategorised_dicom = UncategorisedDicom.objects.get(
|
|
||||||
image_blake3_hash=hash
|
|
||||||
)
|
|
||||||
data = {
|
|
||||||
"id": uncategorised_dicom.pk,
|
|
||||||
"url": reverse("atlas:user_uploads"),
|
|
||||||
"type": "uncategorised",
|
|
||||||
}
|
|
||||||
except UncategorisedDicom.DoesNotExist:
|
|
||||||
data = {"id": False, "url": False}
|
|
||||||
|
|
||||||
hash_status[hash] = data
|
uncategorised = UncategorisedDicom.objects.filter(image_blake3_hash=hash).first()
|
||||||
|
if uncategorised:
|
||||||
|
try:
|
||||||
|
uploads_url = reverse("atlas:user_uploads")
|
||||||
|
except Exception:
|
||||||
|
uploads_url = None
|
||||||
|
data = {"id": uncategorised.pk, "url": uploads_url, "type": "uncategorised"}
|
||||||
|
hash_status[hash] = data
|
||||||
|
continue
|
||||||
|
|
||||||
return hash_status
|
hash_status[hash] = {"id": False, "url": False}
|
||||||
|
|
||||||
|
return hash_status
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("check_images_hashes failed")
|
||||||
|
return JsonResponse({"detail": "Internal server error"}, status=500)
|
||||||
|
|
||||||
|
|
||||||
# @router.get("/generate_image_hash/{id}", auth=django_auth)
|
# @router.get("/generate_image_hash/{id}", auth=django_auth)
|
||||||
@@ -626,13 +630,13 @@ def check_images_hashes(request, hashes: List[str]):
|
|||||||
# print(series_image)
|
# print(series_image)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/view_dicom_tags/{hash}", auth=BearerAuth())
|
@router.get("/view_dicom_tags/{hash}", auth=TokenAuth())
|
||||||
def view_dicom_tags(request, hash: str):
|
def view_dicom_tags(request, hash: str):
|
||||||
item = SeriesImage.objects.get(image_blake3_hash=hash)
|
item = SeriesImage.objects.get(image_blake3_hash=hash)
|
||||||
return item.get_dicom_json()
|
return item.get_dicom_json()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/series_split_by_dicom_tag/{series_id}/{dicom_tag}", auth=BearerAuth())
|
@router.get("/series_split_by_dicom_tag/{series_id}/{dicom_tag}", auth=TokenAuth())
|
||||||
def series_split_by_tag(request, series_id: int, dicom_tag: str):
|
def series_split_by_tag(request, series_id: int, dicom_tag: str):
|
||||||
series = get_object_or_404(Series, pk=series_id)
|
series = get_object_or_404(Series, pk=series_id)
|
||||||
if not series.check_user_can_edit(request.user):
|
if not series.check_user_can_edit(request.user):
|
||||||
@@ -679,7 +683,7 @@ def series_split_by_tag(request, series_id: int, dicom_tag: str):
|
|||||||
return new_series
|
return new_series
|
||||||
|
|
||||||
|
|
||||||
@router.get("/split_order_by_dicom_tag/{series_id}/{dicom_tag}", auth=BearerAuth())
|
@router.get("/split_order_by_dicom_tag/{series_id}/{dicom_tag}", auth=TokenAuth())
|
||||||
def series_order_by_tag(request, series_id: int, dicom_tag: str):
|
def series_order_by_tag(request, series_id: int, dicom_tag: str):
|
||||||
series = get_object_or_404(Series, pk=series_id)
|
series = get_object_or_404(Series, pk=series_id)
|
||||||
|
|
||||||
|
|||||||
@@ -128,14 +128,31 @@
|
|||||||
file._blake3_hash = hash; // Store for later use if needed
|
file._blake3_hash = hash; // Store for later use if needed
|
||||||
}
|
}
|
||||||
// Call the API
|
// Call the API
|
||||||
return fetch("{% url 'api-1:check_images_hashes' %}", {
|
try {
|
||||||
method: "POST",
|
const res = await fetch("{% url 'api-1:check_images_hashes' %}", {
|
||||||
headers: {
|
method: "POST",
|
||||||
"Content-Type": "application/json",
|
headers: {
|
||||||
"X-CSRFToken": "{{ csrf_token }}"
|
"Content-Type": "application/json",
|
||||||
},
|
"X-CSRFToken": "{{ csrf_token }}"
|
||||||
body: JSON.stringify(hashes)
|
},
|
||||||
}).then(res => res.json());
|
body: JSON.stringify(hashes)
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
console.error('Hash check failed', res.status, text);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await res.json();
|
||||||
|
} catch (e) {
|
||||||
|
const text = await res.text();
|
||||||
|
console.error('Non-JSON response from hash check', text);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Network error while checking hashes', e);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -161,14 +178,35 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$dupProgress.html('<span style="color:#0d6efd;">Checking hashes with server...</span>');
|
$dupProgress.html('<span style="color:#0d6efd;">Checking hashes with server...</span>');
|
||||||
const result = await fetch("{% url 'api-1:check_images_hashes' %}", {
|
let result = {};
|
||||||
method: "POST",
|
try {
|
||||||
headers: {
|
const res = await fetch("{% url 'api-1:check_images_hashes' %}", {
|
||||||
"Content-Type": "application/json",
|
method: "POST",
|
||||||
"X-CSRFToken": "{{ csrf_token }}"
|
headers: {
|
||||||
},
|
"Content-Type": "application/json",
|
||||||
body: JSON.stringify(hashes)
|
"X-CSRFToken": "{{ csrf_token }}"
|
||||||
}).then(res => res.json());
|
},
|
||||||
|
body: JSON.stringify(hashes)
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
console.error('Hash check failed', res.status, text);
|
||||||
|
toastr.error('Error checking duplicates with server');
|
||||||
|
result = {};
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
result = await res.json();
|
||||||
|
} catch (e) {
|
||||||
|
const text = await res.text();
|
||||||
|
console.error('Non-JSON response from hash check', text);
|
||||||
|
result = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Network error while checking hashes', e);
|
||||||
|
toastr.error('Network error while checking duplicates');
|
||||||
|
result = {};
|
||||||
|
}
|
||||||
|
|
||||||
// Build a map from hash to file for quick lookup
|
// Build a map from hash to file for quick lookup
|
||||||
const hashToFile = {};
|
const hashToFile = {};
|
||||||
@@ -249,7 +287,8 @@
|
|||||||
window.upload_results = {
|
window.upload_results = {
|
||||||
"uploaded": [],
|
"uploaded": [],
|
||||||
"duplicates": [],
|
"duplicates": [],
|
||||||
"failed": []
|
"failed": [],
|
||||||
|
"duplicate_series": []
|
||||||
};
|
};
|
||||||
|
|
||||||
chunked_files = [...chunks(window.to_upload, 10)];
|
chunked_files = [...chunks(window.to_upload, 10)];
|
||||||
@@ -279,27 +318,67 @@
|
|||||||
window.upload_count += 1;
|
window.upload_count += 1;
|
||||||
if (xhr.status === 200) {
|
if (xhr.status === 200) {
|
||||||
// Handle successful response from the server
|
// Handle successful response from the server
|
||||||
window.upload_results["uploaded"].push(...JSON.parse(xhr.response)["uploaded"]);
|
let res = null;
|
||||||
window.upload_results["duplicates"].push(...JSON.parse(xhr.response)["duplicates"]);
|
try {
|
||||||
window.upload_results["failed"].push(...JSON.parse(xhr.response)["failed"]);
|
res = JSON.parse(xhr.responseText || xhr.response);
|
||||||
|
} catch (e) {
|
||||||
for (let i = 0; i < JSON.parse(xhr.response)["uploaded"].length; i++) {
|
const text = xhr.responseText || xhr.response;
|
||||||
let item = document.createElement("li");
|
console.error('Upload response not JSON', text);
|
||||||
item.textContent = JSON.parse(xhr.response)["uploaded"][i];
|
alert('Upload failed: server returned non-JSON response');
|
||||||
$("#uploaded-files").append(item);
|
res = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < JSON.parse(xhr.response)["duplicates"].length; i++) {
|
if (res) {
|
||||||
let item = document.createElement("li");
|
// Append arrays of tuples [filename, hash]
|
||||||
item.textContent = JSON.parse(xhr.response)["duplicates"][i];
|
if (res.uploaded && res.uploaded.length) {
|
||||||
$("#duplicate-files").append(item);
|
window.upload_results.uploaded.push(...res.uploaded);
|
||||||
}
|
res.uploaded.forEach(u => {
|
||||||
window.duplicate_series.add(...JSON.parse(xhr.response)["duplicate_series"]);
|
const item = document.createElement('li');
|
||||||
|
if (Array.isArray(u) && u.length >= 1) {
|
||||||
|
const name = u[0];
|
||||||
|
const hash = u[1] || '';
|
||||||
|
item.innerHTML = `${name}` + (hash ? ` <small class="text-muted">(${hash})</small>` : '');
|
||||||
|
} else {
|
||||||
|
item.textContent = u;
|
||||||
|
}
|
||||||
|
$('#uploaded-files').append(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < JSON.parse(xhr.response)["failed"].length; i++) {
|
if (res.duplicates && res.duplicates.length) {
|
||||||
let item = document.createElement("li");
|
window.upload_results.duplicates.push(...res.duplicates);
|
||||||
item.textContent = JSON.parse(xhr.response)["failed"][i];
|
res.duplicates.forEach(d => {
|
||||||
$("#failed-files").append(item);
|
const item = document.createElement('li');
|
||||||
|
if (Array.isArray(d) && d.length >= 1) {
|
||||||
|
const name = d[0];
|
||||||
|
const hash = d[1] || '';
|
||||||
|
item.innerHTML = `${name}` + (hash ? ` <small class="text-muted">(${hash})</small>` : '');
|
||||||
|
} else {
|
||||||
|
item.textContent = d;
|
||||||
|
}
|
||||||
|
$('#duplicate-files').append(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.failed && res.failed.length) {
|
||||||
|
window.upload_results.failed.push(...res.failed);
|
||||||
|
res.failed.forEach(f => {
|
||||||
|
const item = document.createElement('li');
|
||||||
|
item.textContent = f;
|
||||||
|
$('#failed-files').append(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// duplicate_series may be an array of urls
|
||||||
|
if (res.duplicate_series && res.duplicate_series.length) {
|
||||||
|
res.duplicate_series.forEach(url => {
|
||||||
|
if (window.duplicate_series && typeof window.duplicate_series.add === 'function') {
|
||||||
|
window.duplicate_series.add(url);
|
||||||
|
} else {
|
||||||
|
window.upload_results.duplicate_series.push(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle error response from the server
|
// Handle error response from the server
|
||||||
|
|||||||
Reference in New Issue
Block a user