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
7 changes: 6 additions & 1 deletion ifcbdb/assets/js/bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,12 @@ function updateBinDownloadLinks(data) {
$("#download-adc").attr("href", infix + _bin + ".adc");
$("#download-hdr").attr("href", infix + _bin + ".hdr");
$("#download-roi").attr("href", infix + _bin + ".roi");
$("#download-zip").attr("href", infix + _bin + ".zip");
$("#download-zip-form").attr("action", infix + _bin + ".zip");
$("#download-zip").click(function(e) {
e.preventDefault();
$("#download-zip-form").submit();
});

$("#download-blobs").attr("href", infix + _bin + "_blob.zip");
$("#download-features").attr("href", infix + _bin + "_features.csv");
$("#download-class-scores").attr("href", infix + _bin + "_class_scores.csv");
Expand Down
22 changes: 22 additions & 0 deletions ifcbdb/dashboard/migrations/0038_apiaccount.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 4.2.17 on 2025-01-13 19:11

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('dashboard', '0037_appsettings'),
]

operations = [
migrations.CreateModel(
name='ApiAccount',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('api_key', models.CharField(max_length=128)),
('is_active', models.BooleanField(default=True)),
],
),
]
7 changes: 7 additions & 0 deletions ifcbdb/dashboard/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -917,3 +917,10 @@ class AppSettings(models.Model):
default_latitude = models.FloatField(blank=False, null=False, default=DEFAULT_LATITUDE)
default_longitude = models.FloatField(blank=False, null=False, default=DEFAULT_LONGITUDE)
default_zoom_level = models.IntegerField(blank=False, null=False, default=DEFAULT_ZOOM_LEVEL)

# api

class ApiAccount(models.Model):
name = models.CharField(max_length=50)
api_key = models.CharField(max_length=128)
is_active = models.BooleanField(default=True, blank=False, null=False)
3 changes: 3 additions & 0 deletions ifcbdb/dashboard/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ def to_url(self, value):
path('data/<slug:bin_id>_<int:target>.png', views.image_png, name='image_png'),
path('data/<slug:bin_id>_<int:target>.jpg', views.image_jpg, name='image_jpg'),

# zip access (requires API key)
path('download/<slug:bin_id>.zip', views.download_zip, name='download_zip'),

##################################################################
# Legacy URLs
# Urls below must remain in this specific order (by granularity)
Expand Down
39 changes: 33 additions & 6 deletions ifcbdb/dashboard/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import re
import hashlib
from io import BytesIO

import numpy as np
Expand All @@ -11,17 +12,17 @@
from django.shortcuts import render, get_object_or_404, reverse
from django.http import \
HttpResponse, FileResponse, Http404, HttpResponseBadRequest, JsonResponse, \
HttpResponseRedirect, HttpResponseNotFound, StreamingHttpResponse
HttpResponseRedirect, HttpResponseNotFound, StreamingHttpResponse, HttpResponseForbidden
from django.views.decorators.cache import cache_control
from django.views.decorators.http import require_POST
from django.views.decorators.http import require_POST, require_GET

from django.core.cache import cache
from celery.result import AsyncResult

from ifcb.data.imageio import format_image
from ifcb.data.adc import schema_names

from .models import Dataset, Bin, Instrument, Timeline, bin_query, Tag, Comment, normalize_tag_name
from .models import Dataset, Bin, Instrument, Timeline, bin_query, Tag, Comment, normalize_tag_name, ApiAccount
from .forms import DatasetSearchForm
from common.utilities import *

Expand Down Expand Up @@ -717,15 +718,41 @@ def class_scores_csv(request, dataset_name, bin_id):
resp['Content-Disposition'] = 'attachment; filename={}'.format(filename)
return resp

def zip(request, bin_id, **kw):
from django.views.decorators.csrf import csrf_protect

@require_POST
def zip(request, bin_id, **kwargs):
return _build_zip(request, bin_id, **kwargs)

@require_GET
def download_zip(request, bin_id, **kwargs):
# Check the header first for an api key, and inot present, check the query string
api_key = request.headers.get("ApiKey") or request.GET.get("ApiKey")
if not api_key:
return HttpResponseForbidden()


hash = hashlib.sha512(api_key.encode("utf8")).hexdigest()

api_account = ApiAccount.objects.filter(api_key=hash, is_active=True).first()
if not api_account:
return HttpResponseForbidden()

return _build_zip(request, bin_id, **kwargs)

def _build_zip(request, bin_id, **kwargs):
b = get_object_or_404(Bin, pid=bin_id)
if 'dataset_name' in kw:
bin_in_dataset_or_404(b, kw['dataset_name'])

if 'dataset_name' in kwargs:
bin_in_dataset_or_404(b, kwargs['dataset_name'])

try:
zip_buf = b.zip()
except KeyError:
raise Http404("raw data not found")

filename = '{}.zip'.format(bin_id)

return FileResponse(zip_buf, as_attachment=True, filename=filename, content_type='application/zip')


Expand Down
22 changes: 20 additions & 2 deletions ifcbdb/secure/forms.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import re, os
import re, os, uuid
from django import forms

from dashboard.models import Dataset, Instrument, DataDirectory, AppSettings, \
from dashboard.models import Dataset, Instrument, DataDirectory, AppSettings, ApiAccount, \
DEFAULT_LATITUDE, DEFAULT_LONGITUDE, DEFAULT_ZOOM_LEVEL


Expand Down Expand Up @@ -178,6 +178,24 @@ class Meta:
"timeout": forms.TextInput(attrs={"class": "form-control form-control-sm", "placeholder": "Timeout"}),
}

class ApiAccountForm(forms.ModelForm):
replace_api_key = forms.BooleanField(widget=forms.HiddenInput(), required=False)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.fields["api_key"].required = False

class Meta:
model = ApiAccount
fields = ["id", "name", "api_key", "is_active"]

widgets = {
"name": forms.TextInput(attrs={"class": "form-control form-control-sm", "placeholder": "Name"}),
"api_key": forms.TextInput(attrs={"class": "form-control form-control-sm", "placeholder": "Api Key"}),
"is_active": forms.CheckboxInput(attrs={"class": "custom-control-input"}),
}


class MetadataUploadForm(forms.Form):
file = forms.FileField(label="Choose file", widget=forms.ClearableFileInput(attrs={"class": "custom-file-input"}))
Expand Down
4 changes: 4 additions & 0 deletions ifcbdb/secure/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
path('edit-dataset/<int:id>', views.edit_dataset, name='edit-dataset'),
path('instrument-management', views.instrument_management, name='instrument-management'),
path('edit-instrument/<int:id>', views.edit_instrument, name='edit-instrument'),
path('api-account-management', views.api_account_management, name='api-account-management'),
path('edit-api-account/<int:id>', views.edit_api_account, name='edit-api-account'),
path('upload-metadata', views.upload_metadata, name='upload-metadata'),
path('directory-management/<int:dataset_id>', views.directory_management, name='directory-management'),
path('edit-directory/<int:dataset_id>/<int:id>', views.edit_directory, name='edit-directory'),
Expand All @@ -20,8 +22,10 @@
# Paths used for AJAX requests specifically for returning data formatted for DataTables
path('api/dt/datasets', views.dt_datasets, name='datasets_dt'),
path('api/dt/instruments', views.dt_instruments, name='instruments_dt'),
path('api/dt/api-accounts', views.dt_api_accounts, name='api_accounts_dt'),
path('api/dt/directories/<int:dataset_id>', views.dt_directories, name='directories_dt'),
path('api/delete-directory/<int:dataset_id>/<int:id>', views.delete_directory, name='delete-directory'),
path('api/delete-api-account/<int:id>', views.delete_api_account, name='delete-api-account'),
path('api/add-tag/<slug:bin_id>', views.add_tag, name='add_tag'),
path('api/remove-tag/<slug:bin_id>', views.remove_tag, name='remove_tag'),
path('api/add-comment/<slug:bin_id>', views.add_comment, name='add_comment'),
Expand Down
75 changes: 72 additions & 3 deletions ifcbdb/secure/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import uuid, hashlib
from django.contrib.auth.decorators import login_required
from django import forms
from django.views.decorators.http import require_POST, require_GET
Expand All @@ -6,8 +7,8 @@

import pandas as pd

from dashboard.models import Dataset, Instrument, DataDirectory, Tag, TagEvent, Bin, Comment, AppSettings
from .forms import DatasetForm, InstrumentForm, DirectoryForm, MetadataUploadForm, AppSettingsForm
from dashboard.models import Dataset, Instrument, DataDirectory, Tag, TagEvent, Bin, Comment, AppSettings, ApiAccount
from .forms import DatasetForm, InstrumentForm, DirectoryForm, MetadataUploadForm, AppSettingsForm, ApiAccountForm

from django.core.cache import cache
from celery.result import AsyncResult
Expand Down Expand Up @@ -132,7 +133,6 @@ def delete_directory(request, dataset_id, id):
directory.delete()
return JsonResponse({})


def dt_instruments(request):
if not request.user.is_authenticated:
return HttpResponseForbidden()
Expand Down Expand Up @@ -172,6 +172,75 @@ def edit_instrument(request, id):
"form": form,
})

@login_required
def api_account_management(request):
return render(request, 'secure/api-account-management.html', {

})

def dt_api_accounts(request):
if not request.user.is_authenticated:
return HttpResponseForbidden()

api_accounts = ApiAccount.objects.all()

return JsonResponse({
"data": [
{
"id": item.id,
"name": item.name,
"is_active": item.is_active,
}
for item in api_accounts
]
})

@login_required
def edit_api_account(request, id):
if int(id) > 0:
api_account = get_object_or_404(ApiAccount, pk=id)
else:
api_account = ApiAccount(api_key=str(uuid.uuid4()).lower())

if request.POST:
existing_api_key = api_account.api_key

form = ApiAccountForm(request.POST, instance=api_account)
if form.is_valid():
replace_api_key = form.cleaned_data.get("replace_api_key")

instance = form.save(commit=False)

if int(id) == 0 or replace_api_key:
api_key = form.cleaned_data.get("api_key").encode("utf8")
instance.api_key = hashlib.sha512(api_key).hexdigest()
else:
instance.api_key = existing_api_key

instance.save()

return redirect(reverse("secure:api-account-management"))
else:
# Clear out the api key so it doesn't render back on the form
if int(id) > 0:
api_account.api_key = ""

form = ApiAccountForm(instance=api_account)

return render(request, "secure/edit-api-account.html", {
"api_account": api_account,
"form": form,
})

@require_POST
def delete_api_account(request, id):
if not request.user.is_authenticated:
return HttpResponseForbidden()

api_account = get_object_or_404(ApiAccount, pk=id)
api_account.delete()

return JsonResponse({})

@login_required
def app_settings(request):
Expand Down
3 changes: 3 additions & 0 deletions ifcbdb/templates/dashboard/bin.html
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,9 @@
<li><a id="download-hdr" href="#">HDR</a></li>
<li><a id="download-roi" href="#">ROI</a></li>
<li><a id="download-zip" href="#">ZIP</a></li>
<form id="download-zip-form" method="post" target="_blank">
{% csrf_token %}
</form>
</ul>
</div>
<div class="flex-column flex-fill">
Expand Down
88 changes: 88 additions & 0 deletions ifcbdb/templates/secure/api-account-management.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
{% extends 'base.html' %} {% block content %}
<div class="flex-row d-flex justify-content-between">
<div class="flex-column">
<span class="h3-responsive">Api Account Management</span>
</div>
<div class="flex-column">
<a class="btn btn-sm btn-mdb-color" href="{% url 'secure:edit-api-account' 0 %}"><i class="fas fa-plus magic mr-1"></i> Add New Api Account</a>
</div>
</div>
<hr class="my-2">
<div class="row py-2 px-3">
<div class="col">
<table id="api-accounts" class="table table-sm table-striped table-bordered" style="width:100%">
<thead>
<tr>
<th>Number</th>
<th>Nickname</th>
<th></th>
</tr>
</thead>
</table>
</div>
</div>

<div>
<hr />
<a href="{% url 'secure:index' %}" class="btn btn-sm btn-mdb-color">
<i class='fas fa-arrow-left magic mr-1'></i>
Back to Settings
</a>
</div>

{% endblock %} {% block scripts %}
<script>
$(function(){
const apiAccountsTable = $('#api-accounts').DataTable({
searching: false,
lengthChange: false,
ajax: {
url: "{% url 'secure:api_accounts_dt' %}"
},
columns: [
{
title: "Name",
data: "name"
},
{
title: "Active?",
data: "is_active",
render: function (data) { return data ? "Yes" : "No" }
},
{
orderable: false,
data: "id",
render: function ( data, type, row ) {
return (
"<a class='btn btn-sm btn-mdb-color' href='/secure/edit-api-account/" + data + "'>" +
"<i class='fas fa-edit magic mr-1'></i>Edit" +
"</a>" +
"<a class='btn btn-sm btn-mdb-color ml-2 delete-api-account' data-id='" + data + "' href='#'>" +
"<i class='fas fa-trash magic mr-1'></i>Delete" +
"</a>"
);
}
}
]
});

$("#api-accounts").click(function(e) {
if (e.target.matches(".delete-api-account") || e.target.parentNode.matches(".delete-api-account")) {
if (!confirm("Are you sure you want to delete this Api Account?")) {
return;
}

const id = e.target.closest("[data-id]").dataset.id;

var payload = {
csrfmiddlewaretoken: "{{ csrf_token }}",
}

$.post("/secure/api/delete-api-account/" + id, payload, function(data){
apiAccountsTable.ajax.reload();
});
}
});
});
</script>
{% endblock %}
Loading