diff --git a/bats_ai/core/views/nabat/nabat_recording.py b/bats_ai/core/views/nabat/nabat_recording.py index dd6d2353..c25a11d6 100644 --- a/bats_ai/core/views/nabat/nabat_recording.py +++ b/bats_ai/core/views/nabat/nabat_recording.py @@ -231,8 +231,6 @@ def generate_nabat_recording( @router.get('/{id}/spectrogram', auth=admin_auth) def get_spectrogram(request: HttpRequest, id: int): - if not request.user.is_authenticated and not request.user.is_superuser: - return JsonResponse({'error': 'Permission denied'}, status=403) try: nabat_recording = NABatRecording.objects.get(pk=id) except NABatRecording.DoesNotExist: @@ -432,7 +430,9 @@ def get_recording_annotation_details(request: HttpRequest, id: int, apiToken: st @router.put('recording-annotation', auth=None, response={200: str}) def create_recording_annotation(request: HttpRequest, data: NABatCreateRecordingAnnotationSchema): - email_or_response = get_email_if_authorized(request, data.apiToken, recording_pk=id) + email_or_response = get_email_if_authorized( + request, data.apiToken, recording_pk=data.recordingId + ) if isinstance(email_or_response, JsonResponse): return email_or_response user_email = email_or_response # safe to use @@ -487,11 +487,12 @@ def create_recording_annotation(request: HttpRequest, data: NABatCreateRecording def update_recording_annotation( request: HttpRequest, id: int, data: NABatCreateRecordingAnnotationSchema ): - email_or_response = get_email_if_authorized(request, data.apiToken, recording_pk=id) + email_or_response = get_email_if_authorized( + request, data.apiToken, recording_pk=data.recordingId + ) if isinstance(email_or_response, JsonResponse): return email_or_response user_email = email_or_response # safe to use - try: annotation = NABatRecordingAnnotation.objects.get(pk=id, user_email=user_email) # Check permission @@ -539,9 +540,10 @@ def update_recording_annotation( return JsonResponse({'error': 'One or more species IDs not found.'}, 404) +# TODO: Determine if this will be implemented for NABat @router.delete('recording-annotation/{id}', auth=None, response={200: str}) -def delete_recording_annotation(request: HttpRequest, id: int, apiToken: str): - email_or_response = get_email_if_authorized(request, apiToken, recording_pk=id) +def delete_recording_annotation(request: HttpRequest, id: int, apiToken: str, recordingId: str): + email_or_response = get_email_if_authorized(request, apiToken, recording_pk=recordingId) if isinstance(email_or_response, JsonResponse): return email_or_response user_email = email_or_response # safe to use diff --git a/client/src/api/NABatApi.ts b/client/src/api/NABatApi.ts index 0d82beaa..7359a525 100644 --- a/client/src/api/NABatApi.ts +++ b/client/src/api/NABatApi.ts @@ -68,8 +68,8 @@ async function patchNABatFileAnnotation(fileAnnotationId: number, fileAnnotation return axiosInstance.patch<{ message: string, id: number }>(`nabat/recording/recording-annotation/${fileAnnotationId}`, { ...fileAnnotation }); } -async function deleteNABatFileAnnotation(fileAnnotationId: number, apiToken?: string) { - return axiosInstance.delete<{ message: string, id: number }>(`nabat/recording/recording-annotation/${fileAnnotationId}`, { params: { apiToken } }); +async function deleteNABatFileAnnotation(fileAnnotationId: number, apiToken?: string, recordingId?: number) { + return axiosInstance.delete<{ message: string, id: number }>(`nabat/recording/recording-annotation/${fileAnnotationId}`, { params: { apiToken, recordingId } }); } export interface RecordingListItem { diff --git a/client/src/components/ColorSchemeSelect.vue b/client/src/components/ColorSchemeSelect.vue index 67f501a8..9f0c54f8 100644 --- a/client/src/components/ColorSchemeSelect.vue +++ b/client/src/components/ColorSchemeSelect.vue @@ -14,6 +14,10 @@ defineProps({ type: Number, default: 150, }, + maxWidth: { + type: Number, + default: 150, + }, returnObject: { type: Boolean, default: true, diff --git a/client/src/components/RecordingAnnotationEditor.vue b/client/src/components/RecordingAnnotationEditor.vue index dfc8f999..df86eb2d 100644 --- a/client/src/components/RecordingAnnotationEditor.vue +++ b/client/src/components/RecordingAnnotationEditor.vue @@ -95,7 +95,7 @@ export default defineComponent({ const deleteAnnotation = async () => { if (props.annotation && props.recordingId) { - props.type === 'nabat' ? await deleteNABatFileAnnotation(props.annotation.id, props.apiToken) : await deleteFileAnnotation(props.annotation.id,); + props.type === 'nabat' ? await deleteNABatFileAnnotation(props.annotation.id, props.apiToken, props.recordingId) : await deleteFileAnnotation(props.annotation.id,); emit('delete:annotation'); } }; @@ -119,7 +119,7 @@ export default defineComponent({ Edit Annotations = ref(null); + + + const loadFileAnnotations = async () => { if (props.type === 'nabat') { annotations.value = (await getNABatRecordingFileAnnotations(props.recordingId, props.apiToken)).data; @@ -51,7 +56,20 @@ export default defineComponent({ } }; - onMounted(() => loadFileAnnotations()); + onMounted(async () => { + await loadFileAnnotations(); + if (props.type === 'nabat') { + const decoded = decodeJWT(props.apiToken); + if (decoded['email']) { + currentNaBatUser.value = decoded['email']; + const foundItem = annotations.value.find((item) => item.owner === currentNaBatUser.value); + if (foundItem) { + setSelectedId(foundItem); + } + } + } + + }); const addAnnotation = async () => { const newAnnotation: UpdateFileAnnotation & { apiToken?: string } = { @@ -91,11 +109,11 @@ export default defineComponent({ const isAdmin = computed(() => configuration.value.is_admin); const disableNaBatAnnotations = computed(() => { - const nonAIAnnotations = annotations.value.filter((item) => item.owner); + const currentUserAnnotations = annotations.value.filter((item) => item.owner === currentNaBatUser.value); if (isAdmin.value && props.type === 'nabat' && !props.apiToken) { return true; } - return (nonAIAnnotations.length > 0 && props.type === 'nabat'); + return ( currentUserAnnotations.length > 0 && props.type === 'nabat'); }); return { @@ -109,6 +127,7 @@ export default defineComponent({ detailsDialog, detailRecordingId, disableNaBatAnnotations, + currentNaBatUser, }; }, }); @@ -137,6 +156,7 @@ export default defineComponent({ :id="`annotation-${annotation.id}`" :key="annotation.id" :class="{ selected: annotation.id === selectedAnnotation?.id }" + :disabled="type === 'nabat' && disableNaBatAnnotations && annotation.owner !== currentNaBatUser" class="annotation-item" @click="setSelectedId(annotation)" > diff --git a/client/src/use/useJWTToken.ts b/client/src/use/useJWTToken.ts index 7a854af8..cd31c677 100644 --- a/client/src/use/useJWTToken.ts +++ b/client/src/use/useJWTToken.ts @@ -1,29 +1,29 @@ -import { ref, watch } from 'vue'; +import { ref, watch } from "vue"; interface UseJWTTokenOptions { token: string; warningSeconds: number; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function decodeJWT(token: string): any | null { + try { + const payload = token.split(".")[1]; + const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/")); + return JSON.parse(decoded); + } catch (error) { + console.error("Failed to decode JWT:", error); + return null; + } +} + export function useJWTToken(options: UseJWTTokenOptions) { const { token, warningSeconds } = options; - const storageKey = 'jwt-expiration'; + const storageKey = "jwt-expiration"; const exp = ref(null); const shouldWarn = ref(false); let warningTimeout: ReturnType | null = null; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - function decodeJWT(token: string): any | null { - try { - const payload = token.split('.')[1]; - const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/')); - return JSON.parse(decoded); - } catch (error) { - console.error('Failed to decode JWT:', error); - return null; - } - } - function setupWarning(expiration: number) { const now = Math.floor(Date.now() / 1000); const secondsUntilWarning = expiration - now - warningSeconds; @@ -51,11 +51,11 @@ export function useJWTToken(options: UseJWTTokenOptions) { if (stored) { try { const data = JSON.parse(stored); - if (typeof data.expiration === 'number') { + if (typeof data.expiration === "number") { return data.expiration; } } catch (error) { - console.error('Failed to parse stored expiration:', error); + console.error("Failed to parse stored expiration:", error); } } return null; @@ -66,12 +66,12 @@ export function useJWTToken(options: UseJWTTokenOptions) { return; } const decoded = decodeJWT(token); - if (decoded && typeof decoded.exp === 'number') { + if (decoded && typeof decoded.exp === "number") { exp.value = decoded.exp; persistExpiration(decoded.exp); setupWarning(decoded.exp); } else { - console.warn('Token does not have a valid exp field.'); + console.warn("Token does not have a valid exp field."); const persisted = loadPersistedExpiration(); if (persisted) { exp.value = persisted; @@ -101,4 +101,4 @@ export function useJWTToken(options: UseJWTTokenOptions) { shouldWarn, clear, }; -} \ No newline at end of file +}