Compare commits

..

64 Commits

Author SHA1 Message Date
Cayo Puigdefabregas 33cc462a4f resolve conflict 2024-10-21 18:41:43 +02:00
Cayo Puigdefabregas efcb53e062 repair bad jsons 2024-10-21 18:39:31 +02:00
Cayo Puigdefabregas e8d8c96700 add anchor in tabs of details of devices 2024-10-21 16:48:36 +02:00
Cayo Puigdefabregas ff91bc9ad9 change TIME_ZONE for env variable 2024-10-21 13:50:01 +02:00
Cayo Puigdefabregas 8d509e31c1 add date in list of events 2024-10-21 13:28:20 +02:00
Cayo Puigdefabregas 4bb71adf00 add hours of use 2024-10-21 13:27:45 +02:00
Cayo Puigdefabregas a0548b38c7 change EreuseWorkbench for workbench-script 2024-10-21 11:24:09 +02:00
cayop a9e39dcd3d Merge pull request 'lot: add a button to show/hide closed lots' (#2) from tau_showclosed-lot-sbtn into main
Reviewed-on: #2
2024-10-18 13:42:45 +00:00
Cayo Puigdefabregas 9badf494a6 resolve confict2 2024-10-18 15:30:56 +02:00
Cayo Puigdefabregas d98e8e2d48 resolve conficts 2024-10-18 15:27:39 +02:00
Cayo Puigdefabregas c60a9dd89d commit in login page 2024-10-18 15:25:27 +02:00
pedro 4cf164d4bc add commit footer for docker 2024-10-18 15:25:27 +02:00
pedro 9f05dee6a7 docker-compose: bugfix DOMAIN vs ALLOWED_HOSTS 2024-10-18 15:25:27 +02:00
pedro 1d9d100fae settings: bugfix format assert error msg on DOMAIN 2024-10-18 15:25:27 +02:00
pedro 096b83da81 settings: bugfix assert error msg on DOMAIN 2024-10-18 15:25:27 +02:00
pedro d995db8181 settings: improve assert error msg on DOMAIN 2024-10-18 15:25:27 +02:00
pedro 3d2cb7d184 docker: improve debug 2024-10-18 15:25:27 +02:00
Cayo Puigdefabregas b7c4926c39 add commit_id in settings 2024-10-18 15:25:27 +02:00
Cayo Puigdefabregas e6141bcc7f more debug in the error answer from api 2024-10-18 15:25:27 +02:00
Cayo Puigdefabregas 0b55d34d74 locale in settings 2024-10-18 15:25:27 +02:00
pedro c49644ffbd add ALLOWED_HOSTS in docker compose 2024-10-18 15:25:27 +02:00
pedro 425b032273 docker: use demo true/false instead of y/n 2024-10-18 15:25:27 +02:00
pedro bf47d4bc5d docker: bugfix wrong detection of demo 2024-10-18 15:25:27 +02:00
pedro 67869bc6f5 add predefined_token
as an alternative to the randomly generated, which is also possible
when no predefined_token is defined

also update .env.example vars
2024-10-18 15:25:27 +02:00
pedro f757c58356 snapshot processing: improve warning msg
add comment TODO that system annotation should happen
2024-10-18 15:25:27 +02:00
pedro 0e8607d93e docker: make default user admin 2024-10-18 15:25:27 +02:00
pedro 3d10217599 docker-reset: remove better the db
now that in db dir there are snapshots stored
2024-10-18 15:25:27 +02:00
Cayo Puigdefabregas d8b6d3ded6 fix hid in details of device 2024-10-18 15:25:27 +02:00
Cayo Puigdefabregas c422eaddeb clean code comment 2024-10-18 15:25:27 +02:00
Cayo Puigdefabregas 090ecc275f fix bug of tag 2024-10-18 15:25:27 +02:00
Cayo Puigdefabregas 2f9b61667d remove pdbs 2024-10-18 15:25:27 +02:00
Cayo Puigdefabregas 003d224c3e add save snapshots for imports and uploads 2024-10-18 15:25:27 +02:00
Cayo Puigdefabregas fae269eb8d save placeholders 2024-10-18 15:25:27 +02:00
Cayo Puigdefabregas 69a54b92fb save snapshots on disk from api 2024-10-18 15:25:27 +02:00
Cayo Puigdefabregas a08428c90b commit in login page 2024-10-18 12:03:31 +02:00
pedro f87897de85 add commit footer for docker 2024-10-18 10:45:08 +02:00
pedro 03b9c5fea8 docker-compose: bugfix DOMAIN vs ALLOWED_HOSTS 2024-10-18 10:28:02 +02:00
pedro a65d78e72f settings: bugfix format assert error msg on DOMAIN 2024-10-18 10:27:27 +02:00
pedro b0754d8e97 settings: bugfix assert error msg on DOMAIN 2024-10-18 10:26:38 +02:00
pedro 460fb678e9 settings: improve assert error msg on DOMAIN 2024-10-18 10:24:40 +02:00
pedro 8e8130fc34 docker: improve debug 2024-10-18 10:20:33 +02:00
Cayo Puigdefabregas 182f854570 add commit_id in settings 2024-10-18 09:52:09 +02:00
Cayo Puigdefabregas 1bb3b12636 more debug in the error answer from api 2024-10-17 19:08:00 +02:00
Cayo Puigdefabregas d94b12d1a0 locale in settings 2024-10-17 18:39:10 +02:00
pedro 2e638e40a9 add ALLOWED_HOSTS in docker compose 2024-10-16 22:48:42 +02:00
pedro 1751dc98c2 docker: use demo true/false instead of y/n 2024-10-16 22:07:40 +02:00
pedro ce9a69d6f7 docker: bugfix wrong detection of demo 2024-10-16 22:05:09 +02:00
pedro 71f68e2d4b add predefined_token
as an alternative to the randomly generated, which is also possible
when no predefined_token is defined

also update .env.example vars
2024-10-16 21:54:54 +02:00
pedro 11f4b647c2 snapshot processing: improve warning msg
add comment TODO that system annotation should happen
2024-10-16 21:14:22 +02:00
pedro 7b95ce0541 docker: make default user admin 2024-10-16 21:09:16 +02:00
pedro 94fd1d685a docker-reset: remove better the db
now that in db dir there are snapshots stored
2024-10-16 20:39:57 +02:00
Cayo Puigdefabregas cd3cc62004 fix hid in details of device 2024-10-16 17:40:00 +02:00
Cayo Puigdefabregas ccda270de0 clean code comment 2024-10-16 14:19:09 +02:00
Cayo Puigdefabregas 2d7b74c556 fix bug of tag 2024-10-16 14:17:35 +02:00
Cayo Puigdefabregas d1cd44f8f6 remove pdbs 2024-10-15 17:57:45 +02:00
Cayo Puigdefabregas 69bd81289f add save snapshots for imports and uploads 2024-10-15 17:56:54 +02:00
Cayo Puigdefabregas 5bf5bc2360 save placeholders 2024-10-15 17:56:54 +02:00
Cayo Puigdefabregas 92d9837a91 save snapshots on disk from api 2024-10-15 17:56:54 +02:00
pedro d04216ad79 api snapshot public_url bugfix
with the following code the URL is more appropriate (it basically
mirrors the user's request)

before:

    http://localhost/device/{shortid}

after

    http://localhost:8000/device/{shortid}
2024-10-15 13:16:02 +02:00
Cayo Puigdefabregas b4c4ed2689 email up 2024-10-15 11:06:08 +02:00
pedro 4cad3eb063 relevant note for current snapshot api 2024-10-14 20:08:50 +02:00
Cayo Puigdefabregas e47a7d80f2 fix dhid 6 characters 2024-10-14 17:54:58 +02:00
Cayo Puigdefabregas 2b40104180 change response from api/snapshots 2024-10-14 17:45:54 +02:00
Cayo Puigdefabregas 1bf2c1558d email system and reset password 2024-10-14 17:45:54 +02:00
29 changed files with 480 additions and 74 deletions

View File

@ -1,2 +1,20 @@
DOMAIN=localhost DOMAIN=localhost
DEMO=false DEMO=false
STATIC_ROOT=/tmp/static/
MEDIA_ROOT=/tmp/media/
ALLOWED_HOSTS=localhost,localhost:8000,127.0.0.1,
DOMAIN=localhost
DEBUG=True
EMAIL_HOST="mail.example.org"
EMAIL_HOST_USER="fillme_noreply"
EMAIL_HOST_PASSWORD="fillme_passwd"
EMAIL_PORT=587
EMAIL_USE_TLS=True
EMAIL_BACKEND="django.core.mail.backends.smtp.EmailBackend"
EMAIL_FILE_PATH="/tmp/app-messages"
ENABLE_EMAIL=false
PREDEFINED_TOKEN='5018dd65-9abd-4a62-8896-80f34ac66150'
# TODO review these vars
#SNAPSHOTS_DIR=/path/to/TODO
#EVIDENCES_DIR=/path/to/TODO

69
admin/email.py Normal file
View File

@ -0,0 +1,69 @@
import logging
from django.conf import settings
from django.template import loader
from django.core.mail import EmailMultiAlternatives
from django.contrib.auth.tokens import default_token_generator
from django.contrib.sites.shortcuts import get_current_site
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
logger = logging.getLogger(__name__)
class NotifyActivateUserByEmail:
subject_template_name = 'activate_user_subject.txt'
email_template_name = 'activate_user_email.txt'
html_email_template_name = 'activate_user_email.html'
def get_email_context(self, user, token):
"""
Define a new context with a token for put in a email
when send a email for add a new password
"""
protocol = 'https' if self.request.is_secure() else 'http'
current_site = get_current_site(self.request)
site_name = current_site.name
domain = current_site.domain
if not token:
token = default_token_generator.make_token(user)
context = {
'email': user.email,
'domain': domain,
'site_name': site_name,
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
'user': user,
'token': token,
'protocol': protocol,
}
return context
def send_email(self, user, token=None):
"""
Send a email when a user is activated.
"""
context = self.get_email_context(user, token)
subject = loader.render_to_string(self.subject_template_name, context)
# Email subject *must not* contain newlines
subject = ''.join(subject.splitlines())
body = loader.render_to_string(self.email_template_name, context)
from_email = settings.DEFAULT_FROM_EMAIL
to_email = user.email
email_message = EmailMultiAlternatives(
subject, body, from_email, [to_email])
html_email = loader.render_to_string(self.html_email_template_name, context)
email_message.attach_alternative(html_email, 'text/html')
try:
if settings.ENABLE_EMAIL:
email_message.send()
return
logger.warning(to_email)
logger.warning(body)
except Exception as err:
logger.error(err)
return

View File

@ -1,3 +1,4 @@
from smtplib import SMTPException
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -9,6 +10,7 @@ from django.views.generic.edit import (
) )
from dashboard.mixins import DashboardView, Http403 from dashboard.mixins import DashboardView, Http403
from user.models import User, Institution from user.models import User, Institution
from admin.email import NotifyActivateUserByEmail
class AdminView(DashboardView): class AdminView(DashboardView):
@ -42,7 +44,7 @@ class UsersView(AdminView, TemplateView):
return context return context
class CreateUserView(AdminView, CreateView): class CreateUserView(AdminView, NotifyActivateUserByEmail, CreateView):
template_name = "user.html" template_name = "user.html"
title = _("User") title = _("User")
breadcrumb = _("admin / User") + " /" breadcrumb = _("admin / User") + " /"
@ -58,6 +60,12 @@ class CreateUserView(AdminView, CreateView):
form.instance.institution = self.request.user.institution form.instance.institution = self.request.user.institution
form.instance.set_password(form.instance.password) form.instance.set_password(form.instance.password)
response = super().form_valid(form) response = super().form_valid(form)
try:
self.send_email(form.instance)
except SMTPException as e:
messages.error(self.request, e)
return response return response

View File

@ -1,32 +1,27 @@
import json import json
from uuid import uuid4
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.core.exceptions import ValidationError
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from django.views.generic.base import View
from django.views.generic.edit import ( from django.views.generic.edit import (
CreateView, CreateView,
DeleteView, DeleteView,
UpdateView, UpdateView,
) )
from django.http import JsonResponse
from uuid import uuid4
from utils.save_snapshots import move_json, save_in_disk
from dashboard.mixins import DashboardView from dashboard.mixins import DashboardView
from evidence.models import Annotation from evidence.models import Annotation
from evidence.parse import Build from evidence.parse import Build
from user.models import User
from api.models import Token from api.models import Token
from api.tables import TokensTable from api.tables import TokensTable
def save_in_disk(data, user):
pass
@csrf_exempt @csrf_exempt
def NewSnapshot(request): def NewSnapshot(request):
# Accept only posts # Accept only posts
@ -59,19 +54,42 @@ def NewSnapshot(request):
).first() ).first()
if exist_annotation: if exist_annotation:
raise ValidationError("error: the snapshot {} exist".format(data['uuid'])) txt = "error: the snapshot {} exist".format(data['uuid'])
return JsonResponse({'status': txt}, status=500)
# Process snapshot # Process snapshot
# save_in_disk(data, tk.user) path_name = save_in_disk(data, tk.owner.institution.name)
try: try:
Build(data, tk.owner) Build(data, tk.owner)
except Exception: except Exception as err:
return JsonResponse({'status': 'fail'}, status=200) return JsonResponse({'status': f"fail: {err}"}, status=500)
return JsonResponse({'status': 'success'}, status=200) annotation = Annotation.objects.filter(
uuid=data['uuid'],
type=Annotation.Type.SYSTEM,
# TODO this is hardcoded, it should select the user preferred algorithm
key="hidalgo1",
owner=tk.owner.institution
).first()
if not annotation:
return JsonResponse({'status': 'fail'}, status=500)
url_args = reverse_lazy("device:details", args=(annotation.value,))
url = request.build_absolute_uri(url_args)
response = {
"status": "success",
"dhid": annotation.value[:6].upper(),
"url": url,
# TODO replace with public_url when available
"public_url": url
}
move_json(path_name, tk.owner.institution.name)
return JsonResponse(response, status=200)
class TokenView(DashboardView, SingleTableView): class TokenView(DashboardView, SingleTableView):

View File

@ -1,4 +1,5 @@
from django.urls import resolve from django.urls import resolve
from django.conf import settings
from django.shortcuts import get_object_or_404, redirect, Http404 from django.shortcuts import get_object_or_404, redirect, Http404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
@ -32,6 +33,7 @@ class DashboardView(LoginRequiredMixin):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context.update({ context.update({
"commit_id": settings.COMMIT,
'title': self.title, 'title': self.title,
'subtitle': self.subtitle, 'subtitle': self.subtitle,
'breadcrumb': self.breadcrumb, 'breadcrumb': self.breadcrumb,

View File

@ -15,7 +15,7 @@
{% trans 'Documents' %} {% trans 'Documents' %}
</a> </a>
{% endif %} {% endif %}
<a href="{# url 'idhub:admin_people_activate' object.id #}" type="button" class="btn btn-green-admin"> <a href="{# url 'dashboard:exports' object.id #}" type="button" class="btn btn-green-admin">
<i class="bi bi-reply"></i> <i class="bi bi-reply"></i>
{% trans 'Exports' %} {% trans 'Exports' %}
</a> </a>

View File

@ -69,23 +69,17 @@ class SearchView(InventaryMixin):
if not matches.size(): if not matches.size():
return self.search_hids(query, offset, limit) return self.search_hids(query, offset, limit)
annotations = [] devices = []
for x in matches: for x in matches:
annotations.extend(self.get_annotations(x)) devices.append(self.get_annotations(x))
devices = [Device(id=x) for x in set(annotations)]
count = matches.size() count = matches.size()
return devices, count return devices, count
def get_annotations(self, xp): def get_annotations(self, xp):
snap = xp.document.get_data() snap = xp.document.get_data()
uuid = json.loads(snap).get('uuid') uuid = json.loads(snap).get('uuid')
return Device.get_annotation_from_uuid(uuid, self.request.user.institution)
return Annotation.objects.filter(
type=Annotation.Type.SYSTEM,
owner=self.request.user.institution,
uuid=uuid
).values_list("value", flat=True).distinct()
def search_hids(self, query, offset, limit): def search_hids(self, query, offset, limit):
qry = Q() qry = Q()

View File

@ -1,5 +1,6 @@
from django import forms from django import forms
from utils.device import create_annotation, create_doc, create_index from utils.device import create_annotation, create_doc, create_index
from utils.save_snapshots import move_json, save_in_disk
DEVICE_TYPES = [ DEVICE_TYPES = [
@ -55,9 +56,12 @@ class BaseDeviceFormSet(forms.BaseFormSet):
doc = create_doc(row) doc = create_doc(row)
if not commit: if not commit:
return doc return doc
path_name = save_in_disk(doc, self.user.institution.name, place="placeholder")
create_index(doc, self.user) create_index(doc, self.user)
create_annotation(doc, user, commit=commit) create_annotation(doc, user, commit=commit)
move_json(path_name, self.user.institution.name, place="placeholder")
return doc return doc

View File

@ -27,7 +27,7 @@ class Device:
# the id is the chid of the device # the id is the chid of the device
self.id = kwargs["id"] self.id = kwargs["id"]
self.pk = self.id self.pk = self.id
self.shortid = self.pk[:6] self.shortid = self.pk[:6].upper()
self.algorithm = None self.algorithm = None
self.owner = None self.owner = None
self.annotations = [] self.annotations = []
@ -90,9 +90,11 @@ class Device:
def get_hids(self): def get_hids(self):
annotations = self.get_annotations() annotations = self.get_annotations()
algos = list(ALGOS.keys())
algos.append('CUSTOM_ID')
self.hids = list(set(annotations.filter( self.hids = list(set(annotations.filter(
type=Annotation.Type.SYSTEM, type=Annotation.Type.SYSTEM,
key__in=ALGOS.keys(), key__in=algos,
).values_list("value", flat=True))) ).values_list("value", flat=True)))
def get_evidences(self): def get_evidences(self):
@ -118,9 +120,32 @@ class Device:
def get_unassigned(cls, institution, offset=0, limit=None): def get_unassigned(cls, institution, offset=0, limit=None):
sql = """ sql = """
SELECT DISTINCT t1.value from evidence_annotation as t1 WITH RankedAnnotations AS (
left join lot_devicelot as t2 on t1.value = t2.device_id SELECT
where t2.device_id is null and owner_id=={institution} and type=={type} t1.value,
t1.key,
ROW_NUMBER() OVER (
PARTITION BY t1.uuid
ORDER BY
CASE
WHEN t1.key = 'CUSTOM_ID' THEN 1
WHEN t1.key = 'hidalgo1' THEN 2
ELSE 3
END,
t1.created DESC
) AS row_num
FROM evidence_annotation AS t1
LEFT JOIN lot_devicelot AS t2 ON t1.value = t2.device_id
WHERE t2.device_id IS NULL
AND t1.owner_id = {institution}
AND t1.type = {type}
)
SELECT DISTINCT
value
FROM
RankedAnnotations
WHERE
row_num = 1
""".format( """.format(
institution=institution.id, institution=institution.id,
type=Annotation.Type.SYSTEM, type=Annotation.Type.SYSTEM,
@ -144,18 +169,83 @@ class Device:
def get_unassigned_count(cls, institution): def get_unassigned_count(cls, institution):
sql = """ sql = """
SELECT count(DISTINCT t1.value) from evidence_annotation as t1 WITH RankedAnnotations AS (
left join lot_devicelot as t2 on t1.value = t2.device_id SELECT
where t2.device_id is null and owner_id=={institution} and type=={type}; t1.value,
t1.key,
ROW_NUMBER() OVER (
PARTITION BY t1.uuid
ORDER BY
CASE
WHEN t1.key = 'CUSTOM_ID' THEN 1
WHEN t1.key = 'hidalgo1' THEN 2
ELSE 3
END,
t1.created DESC
) AS row_num
FROM evidence_annotation AS t1
LEFT JOIN lot_devicelot AS t2 ON t1.value = t2.device_id
WHERE t2.device_id IS NULL
AND t1.owner_id = {institution}
AND t1.type = {type}
)
SELECT
COUNT(DISTINCT value)
FROM
RankedAnnotations
WHERE
row_num = 1
""".format( """.format(
institution=institution.id, institution=institution.id,
type=Annotation.Type.SYSTEM, type=Annotation.Type.SYSTEM,
) )
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute(sql) cursor.execute(sql)
return cursor.fetchall()[0][0] return cursor.fetchall()[0][0]
@classmethod
def get_annotation_from_uuid(cls, uuid, institution):
sql = """
WITH RankedAnnotations AS (
SELECT
t1.value,
t1.key,
ROW_NUMBER() OVER (
PARTITION BY t1.uuid
ORDER BY
CASE
WHEN t1.key = 'CUSTOM_ID' THEN 1
WHEN t1.key = 'hidalgo1' THEN 2
ELSE 3
END,
t1.created DESC
) AS row_num
FROM evidence_annotation AS t1
LEFT JOIN lot_devicelot AS t2 ON t1.value = t2.device_id
WHERE t2.device_id IS NULL
AND t1.owner_id = {institution}
AND t1.type = {type}
AND t1.uuid = '{uuid}'
)
SELECT DISTINCT
value
FROM
RankedAnnotations
WHERE
row_num = 1;
""".format(
uuid=uuid.replace("-", ""),
institution=institution.id,
type=Annotation.Type.SYSTEM,
)
annotations = []
with connection.cursor() as cursor:
cursor.execute(sql)
annotations = cursor.fetchall()
return cls(id=annotations[0][0])
@property @property
def is_websnapshot(self): def is_websnapshot(self):
if not self.last_evidence: if not self.last_evidence:

View File

@ -12,25 +12,25 @@
<div class="col"> <div class="col">
<ul class="nav nav-tabs nav-tabs-bordered"> <ul class="nav nav-tabs nav-tabs-bordered">
<li class="nav-items"> <li class="nav-items">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#details">General details</button> <a href="#details" class="nav-link active" data-bs-toggle="tab" data-bs-target="#details">General details</a>
</li> </li>
<li class="nav-items"> <li class="nav-items">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#annotations">User annotations</button> <a href="#annotations" class="nav-link" data-bs-toggle="tab" data-bs-target="#annotations">User annotations</a>
</li> </li>
<li class="nav-items"> <li class="nav-items">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#documents">Documents</button> <a href="#documents" class="nav-link" data-bs-toggle="tab" data-bs-target="#documents">Documents</a>
</li> </li>
<li class="nav-items"> <li class="nav-items">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#lots">Lots</button> <a href="#lots" class="nav-link" data-bs-toggle="tab" data-bs-target="#lots">Lots</a>
</li> </li>
<li class="nav-items"> <li class="nav-items">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#components">Components</button> <a href="#components" class="nav-link" data-bs-toggle="tab" data-bs-target="#components">Components</a>
</li> </li>
<li class="nav-items"> <li class="nav-items">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#evidences">Evidences</button> <a href="#evidences" class="nav-link" data-bs-toggle="tab" data-bs-target="#evidences">Evidences</a>
</li> </li>
<li class="nav-items"> <li class="nav-items">
<a class="nav-link" href="">Web</a> <a href="#web" class="nav-link" href="">Web</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -214,3 +214,25 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block extrascript %}
<script>
document.addEventListener("DOMContentLoaded", function() {
// Obtener el hash de la URL (ejemplo: #components)
const hash = window.location.hash;
// Verificar si hay un hash en la URL
if (hash) {
// Buscar el botón o enlace que corresponde al hash y activarlo
const tabTrigger = document.querySelector(`[data-bs-target="${hash}"]`);
if (tabTrigger) {
// Crear una instancia de tab de Bootstrap para activar el tab
const tab = new bootstrap.Tab(tabTrigger);
tab.show();
}
}
});
</script>
{% endblock %}

View File

@ -10,6 +10,7 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/ https://docs.djangoproject.com/en/5.0/ref/settings/
""" """
import os
import xapian import xapian
from pathlib import Path from pathlib import Path
@ -35,10 +36,35 @@ assert DOMAIN not in [None, ''], "DOMAIN var is MANDATORY"
print("DOMAIN: " + DOMAIN) print("DOMAIN: " + DOMAIN)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=DOMAIN, cast=Csv()) ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=DOMAIN, cast=Csv())
assert DOMAIN in ALLOWED_HOSTS, "DOMAIN is not ALLOWED_HOST" assert DOMAIN in ALLOWED_HOSTS, f"DOMAIN {DOMAIN} is not in ALLOWED_HOSTS {ALLOWED_HOSTS}"
CSRF_TRUSTED_ORIGINS = config('CSRF_TRUSTED_ORIGINS', default=f'https://{DOMAIN}', cast=Csv()) CSRF_TRUSTED_ORIGINS = config('CSRF_TRUSTED_ORIGINS', default=f'https://{DOMAIN}', cast=Csv())
INITIAL_ADMIN_EMAIL = config("INITIAL_ADMIN_EMAIL", default='admin@example.org')
INITIAL_ADMIN_PASSWORD = config("INITIAL_ADMIN_PASSWORD", default='1234')
DEFAULT_FROM_EMAIL = config(
'DEFAULT_FROM_EMAIL', default='webmaster@localhost')
EMAIL_HOST = config('EMAIL_HOST', default='localhost')
EMAIL_HOST_USER = config('EMAIL_HOST_USER', default='')
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='')
EMAIL_PORT = config('EMAIL_PORT', default=25, cast=int)
EMAIL_USE_TLS = config('EMAIL_USE_TLS', default=False, cast=bool)
EMAIL_BACKEND = config('EMAIL_BACKEND', default='django.core.mail.backends.smtp.EmailBackend')
EMAIL_FILE_PATH = config('EMAIL_FILE_PATH', default='/tmp/app-messages')
ENABLE_EMAIL = config("ENABLE_EMAIL", default=True, cast=bool)
EVIDENCES_DIR = config("EVIDENCES_DIR", default=os.path.join(BASE_DIR, "db"))
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
@ -132,12 +158,20 @@ AUTH_PASSWORD_VALIDATORS = [
LANGUAGE_CODE = "en-us" LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC" TIME_ZONE = config("TIME_ZONE", default="UTC")
USE_I18N = True USE_I18N = True
USE_TZ = True USE_TZ = False
if TIME_ZONE == "UTC":
USE_TZ = True
USE_L10N = True
LANGUAGES = [
('es', 'Spanish'),
('en', 'English'),
]
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/ # https://docs.djangoproject.com/en/5.0/howto/static-files/
@ -182,3 +216,4 @@ LOGGING = {
SNAPSHOT_PATH="/tmp/" SNAPSHOT_PATH="/tmp/"
DATA_UPLOAD_MAX_NUMBER_FILES = 1000 DATA_UPLOAD_MAX_NUMBER_FILES = 1000
COMMIT = config('COMMIT', default='')

View File

@ -6,7 +6,9 @@ services:
environment: environment:
- DEBUG=true - DEBUG=true
- DOMAIN=${DOMAIN:-localhost} - DOMAIN=${DOMAIN:-localhost}
- DEMO=${DEMO:-n} - ALLOWED_HOSTS=${ALLOWED_HOSTS:-$DOMAIN}
- DEMO=${DEMO:-false}
- PREDEFINED_TOKEN=${PREDEFINED_TOKEN:-}
volumes: volumes:
- .:/opt/devicehub-django - .:/opt/devicehub-django
ports: ports:

View File

@ -9,11 +9,13 @@ set -u
set -x set -x
main() { main() {
cd "$(dirname "${0}")"
if [ "${DETACH:-}" ]; then if [ "${DETACH:-}" ]; then
detach_arg='-d' detach_arg='-d'
fi fi
# remove old database # remove old database
sudo rm -vf db/* sudo rm -vfr ./db/*
docker compose down -v docker compose down -v
docker compose build docker compose build
docker compose up ${detach_arg:-} docker compose up ${detach_arg:-}

View File

@ -12,6 +12,14 @@ check_app_is_there() {
} }
deploy() { deploy() {
# TODO this is weird, find better workaround
git config --global --add safe.directory /opt/devicehub-django
export COMMIT=$(git log --format="%H %ad" --date=iso -n 1)
if [ "${DEBUG:-}" = 'true' ]; then
./manage.py print_settings
fi
# detect if existing deployment (TODO only works with sqlite) # detect if existing deployment (TODO only works with sqlite)
if [ -f "${program_dir}/db/db.sqlite3" ]; then if [ -f "${program_dir}/db/db.sqlite3" ]; then
echo "INFO: detected EXISTING deployment" echo "INFO: detected EXISTING deployment"
@ -24,11 +32,13 @@ deploy() {
INIT_ORG="${INIT_ORG:-example-org}" INIT_ORG="${INIT_ORG:-example-org}"
INIT_USER="${INIT_USER:-user@example.org}" INIT_USER="${INIT_USER:-user@example.org}"
INIT_PASSWD="${INIT_PASSWD:-1234}" INIT_PASSWD="${INIT_PASSWD:-1234}"
ADMIN='True'
PREDEFINED_TOKEN="${PREDEFINED_TOKEN:-}"
./manage.py add_institution "${INIT_ORG}" ./manage.py add_institution "${INIT_ORG}"
# TODO: one error on add_user, and you don't add user anymore # TODO: one error on add_user, and you don't add user anymore
./manage.py add_user "${INIT_ORG}" "${INIT_USER}" "${INIT_PASSWD}" ./manage.py add_user "${INIT_ORG}" "${INIT_USER}" "${INIT_PASSWD}" "${ADMIN}" "${PREDEFINED_TOKEN}"
if [ "${DEMO:-}" ]; then if [ "${DEMO:-}" = 'true' ]; then
./manage.py up_snapshots example/snapshots/ "${INIT_USER}" ./manage.py up_snapshots example/snapshots/ "${INIT_USER}"
fi fi
fi fi
@ -36,7 +46,7 @@ deploy() {
runserver() { runserver() {
PORT="${PORT:-8000}" PORT="${PORT:-8000}"
if [ "${DEBUG:-}" ]; then if [ "${DEBUG:-}" = 'true' ]; then
./manage.py runserver 0.0.0.0:${PORT} ./manage.py runserver 0.0.0.0:${PORT}
else else
# TODO # TODO

View File

@ -9,6 +9,7 @@ from utils.forms import MultipleFileField
from device.models import Device from device.models import Device
from evidence.parse import Build from evidence.parse import Build
from evidence.models import Annotation from evidence.models import Annotation
from utils.save_snapshots import move_json, save_in_disk
class UploadForm(forms.Form): class UploadForm(forms.Form):
@ -48,7 +49,9 @@ class UploadForm(forms.Form):
return return
for ev in self.evidences: for ev in self.evidences:
path_name = save_in_disk(ev[1], user.institution.name)
Build(ev[1], user) Build(ev[1], user)
move_json(path_name, user.institution.name)
class UserTagForm(forms.Form): class UserTagForm(forms.Form):
@ -151,8 +154,11 @@ class ImportForm(forms.Form):
if commit: if commit:
for doc, cred in table: for doc, cred in table:
cred.save() path_name = save_in_disk(doc, self.user.institution.name, place="placeholder")
create_index(doc, self.user)
return table cred.save()
create_index(doc, self.user)
move_json(path_name, self.user.institution.name, place="placeholder")
return table
return return

View File

@ -67,7 +67,7 @@ class Evidence:
for xa in matches: for xa in matches:
self.doc = json.loads(xa.document.get_data()) self.doc = json.loads(xa.document.get_data())
if self.doc.get("software") == "EreuseWorkbench": if self.doc.get("software") == "workbench-script":
dmidecode_raw = self.doc["data"]["dmidecode"] dmidecode_raw = self.doc["data"]["dmidecode"]
self.dmi = DMIParse(dmidecode_raw) self.dmi = DMIParse(dmidecode_raw)
@ -80,7 +80,7 @@ class Evidence:
self.created = self.annotations.last().created self.created = self.annotations.last().created
def get_components(self): def get_components(self):
if self.doc.get("software") != "EreuseWorkbench": if self.doc.get("software") != "workbench-script":
return self.doc.get('components', []) return self.doc.get('components', [])
self.set_components() self.set_components()
return self.components return self.components
@ -92,7 +92,7 @@ class Evidence:
return "" return ""
return list(self.doc.get('kv').values())[0] return list(self.doc.get('kv').values())[0]
if self.doc.get("software") != "EreuseWorkbench": if self.doc.get("software") != "workbench-script":
return self.doc['device']['manufacturer'] return self.doc['device']['manufacturer']
return self.dmi.manufacturer().strip() return self.dmi.manufacturer().strip()
@ -104,13 +104,13 @@ class Evidence:
return "" return ""
return list(self.doc.get('kv').values())[1] return list(self.doc.get('kv').values())[1]
if self.doc.get("software") != "EreuseWorkbench": if self.doc.get("software") != "workbench-script":
return self.doc['device']['model'] return self.doc['device']['model']
return self.dmi.model().strip() return self.dmi.model().strip()
def get_chassis(self): def get_chassis(self):
if self.doc.get("software") != "EreuseWorkbench": if self.doc.get("software") != "workbench-script":
return self.doc['device']['model'] return self.doc['device']['model']
chassis = self.dmi.get("Chassis")[0].get("Type", '_virtual') chassis = self.dmi.get("Chassis")[0].get("Type", '_virtual')
@ -126,7 +126,8 @@ class Evidence:
return Annotation.objects.filter( return Annotation.objects.filter(
owner=user.institution, owner=user.institution,
type=Annotation.Type.SYSTEM, type=Annotation.Type.SYSTEM,
).order_by("-created").values_list("uuid", flat=True).distinct() key="hidalgo1",
).order_by("-created").values_list("uuid", "created").distinct()
def set_components(self): def set_components(self):
snapshot = ParseSnapshot(self.doc).snapshot_json snapshot = ParseSnapshot(self.doc).snapshot_json

View File

@ -5,6 +5,8 @@ import hashlib
from datetime import datetime from datetime import datetime
from dmidecode import DMIParse from dmidecode import DMIParse
from json_repair import repair_json
from evidence.models import Annotation from evidence.models import Annotation
from evidence.xapian import index from evidence.xapian import index
from utils.constants import ALGOS, CHASSIS_DH from utils.constants import ALGOS, CHASSIS_DH
@ -20,7 +22,12 @@ def get_network_cards(child, nets):
def get_mac(lshw): def get_mac(lshw):
nets = [] nets = []
try: try:
get_network_cards(json.loads(lshw), nets) hw = json.loads(lshw)
except json.decoder.JSONDecodeError:
hw = json.loads(repair_json(lshw))
try:
get_network_cards(hw, nets)
except Exception as ss: except Exception as ss:
print("WARNING!! {}".format(ss)) print("WARNING!! {}".format(ss))
return return
@ -57,7 +64,7 @@ class Build:
} }
def get_hid_14(self): def get_hid_14(self):
if self.json.get("software") == "EreuseWorkbench": if self.json.get("software") == "workbench-script":
hid = self.get_hid(self.json) hid = self.get_hid(self.json)
else: else:
device = self.json['device'] device = self.json['device']
@ -113,7 +120,8 @@ class Build:
# mac = get_mac2(hwinfo_raw) or "" # mac = get_mac2(hwinfo_raw) or ""
mac = get_mac(lshw) or "" mac = get_mac(lshw) or ""
if not mac: if not mac:
print("WARNING!! No there are MAC address") print(f"WARNING: Could not retrieve MAC address in snapshot {snapshot['uuid']}" )
# TODO generate system annotation for that snapshot
else: else:
print(f"{manufacturer}{model}{chassis}{serial_number}{sku}{mac}") print(f"{manufacturer}{model}{chassis}{serial_number}{sku}{mac}")

View File

@ -3,6 +3,8 @@ import numpy as np
from datetime import datetime from datetime import datetime
from dmidecode import DMIParse from dmidecode import DMIParse
from json_repair import repair_json
from utils.constants import CHASSIS_DH, DATASTORAGEINTERFACE from utils.constants import CHASSIS_DH, DATASTORAGEINTERFACE
@ -160,6 +162,7 @@ class ParseSnapshot:
continue continue
model = sm.get('model_name') model = sm.get('model_name')
manufacturer = None manufacturer = None
hours = sm.get("power_on_time", {}).get("hours", 0)
if model and len(model.split(" ")) > 1: if model and len(model.split(" ")) > 1:
mm = model.split(" ") mm = model.split(" ")
model = mm[-1] model = mm[-1]
@ -175,6 +178,7 @@ class ParseSnapshot:
"size": self.get_data_storage_size(sm), "size": self.get_data_storage_size(sm),
"variant": sm.get("firmware_version"), "variant": sm.get("firmware_version"),
"interface": self.get_data_storage_interface(sm), "interface": self.get_data_storage_interface(sm),
"hours": hours,
} }
) )
@ -478,7 +482,11 @@ class ParseSnapshot:
def loads(self, x): def loads(self, x):
if isinstance(x, str): if isinstance(x, str):
try: try:
return json.loads(x) try:
hw = json.loads(lshw)
except json.decoder.JSONDecodeError:
hw = json.loads(repair_json(lshw))
return hw
except Exception as ss: except Exception as ss:
print("WARNING!! {}".format(ss)) print("WARNING!! {}".format(ss))
return {} return {}

View File

@ -14,10 +14,13 @@
{% for ev in evidences %} {% for ev in evidences %}
<tr> <tr>
<td> <td>
<a href="{% url 'evidence:details' ev %}">{{ ev }}</a> <a href="{% url 'evidence:details' ev.0 %}">{{ ev.0 }}</a>
</td> </td>
<td> <td>
<a href="{# url 'evidence:delete' ev #}"><i class="bi bi-trash text-danger"></i></a> <small class="text-muted">{{ ev.1 }}</small>
</td>
<td>
<a href="{# url 'evidence:delete' ev.0 #}"><i class="bi bi-trash text-danger"></i></a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -0,0 +1,31 @@
{% load i18n %}{% autoescape off %}
{% trans "DeviceHub" as site %}
<p>
{% blocktrans %}You're receiving this email because your user account at {{site}} has been activated.{% endblocktrans %}
</p>
<p>
{% trans "Your username is:" %} {{ user.username }}
</p>
<p>
{% trans "Please go to the following page and choose a password:" %}
</p>
<p>
{% block reset_link %}
<a href="{{ protocol }}://{{ domain }}{% url 'login:password_reset_confirm' uidb64=uid token=token %}">
{{ protocol }}://{{ domain }}{% url 'login:password_reset_confirm' uidb64=uid token=token %}
</a>
{% endblock %}
</p>
<p>
{% trans "Thanks for using our site!" %}
</p>
<p>
{% blocktrans %}The {{site}} team{% endblocktrans %}
</p>
{% endautoescape %}

View File

@ -0,0 +1,19 @@
{% load i18n %}{% autoescape off %}
{% trans "DeviceHub" as site %}
{% blocktrans %}You're receiving this email because your user account at {{site}} has been activated.{% endblocktrans %}
{% trans "Your username is:" %} {{ user.username }}
{% trans "Please go to the following page and choose a password:" %}
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'login:password_reset_confirm' uidb64=uid token=token %}
{% endblock %}
{% trans "Thanks for using our site!" %}
{% blocktrans %}The {{site}} team{% endblocktrans %}
{% endautoescape %}

View File

@ -0,0 +1,4 @@
{% load i18n %}{% autoescape off %}
{% trans "IdHub" as site %}
{% blocktrans %}User activation on {{site}}{% endblocktrans %}
{% endautoescape %}

View File

@ -42,4 +42,5 @@
<div id="login-footer" class="mt-3"> <div id="login-footer" class="mt-3">
<a href="{% url 'login:password_reset' %}" data-toggle="modal" data-target="#forgotPasswordModal">{% trans "Forgot your password? Click here to recover" %}</a> <a href="{% url 'login:password_reset' %}" data-toggle="modal" data-target="#forgotPasswordModal">{% trans "Forgot your password? Click here to recover" %}</a>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -9,8 +9,8 @@
<p> <p>
{% block reset_link %} {% block reset_link %}
<a href="{{ protocol }}://{{ domain }}{% url 'idhub:password_reset_confirm' uidb64=uid token=token %}"> <a href="{{ protocol }}://{{ domain }}{% url 'login:password_reset_confirm' uidb64=uid token=token %}">
{{ protocol }}://{{ domain }}{% url 'idhub:password_reset_confirm' uidb64=uid token=token %} {{ protocol }}://{{ domain }}{% url 'login:password_reset_confirm' uidb64=uid token=token %}
</a> </a>
{% endblock %} {% endblock %}
</p> </p>

View File

@ -3,7 +3,7 @@
{% trans "Please go to the following page and choose a new password:" %} {% trans "Please go to the following page and choose a new password:" %}
{% block reset_link %} {% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'idhub:password_reset_confirm' uidb64=uid token=token %} {{ protocol }}://{{ domain }}{% url 'login:password_reset_confirm' uidb64=uid token=token %}
{% endblock %} {% endblock %}
{% trans "Your username, in case you've forgotten:" %} {{ user.username }} {% trans "Your username, in case you've forgotten:" %} {{ user.username }}

View File

@ -1,5 +1,6 @@
import logging import logging
from django.conf import settings
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.contrib.auth import login as auth_login from django.contrib.auth import login as auth_login
@ -17,7 +18,7 @@ class LoginView(auth_views.LoginView):
extra_context = { extra_context = {
'title': _('Login'), 'title': _('Login'),
'success_url': reverse_lazy('dashboard:unassigned_devices'), 'success_url': reverse_lazy('dashboard:unassigned_devices'),
# 'commit_id': settings.COMMIT, 'commit_id': settings.COMMIT,
} }
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@ -66,7 +67,8 @@ class PasswordResetView(auth_views.PasswordResetView):
def form_valid(self, form): def form_valid(self, form):
try: try:
return super().form_valid(form) response = super().form_valid(form)
return response
except Exception as err: except Exception as err:
logger.error(err) logger.error(err)
return HttpResponseRedirect(self.success_url) return HttpResponseRedirect(self.success_url)

View File

@ -10,4 +10,4 @@ pandas==2.2.2
xlrd==2.0.1 xlrd==2.0.1
odfpy==1.4.1 odfpy==1.4.1
pytz==2024.2 pytz==2024.2
json-repair==0.30.0

View File

@ -17,15 +17,17 @@ class Command(BaseCommand):
parser.add_argument('email', type=str, help='email') parser.add_argument('email', type=str, help='email')
parser.add_argument('password', type=str, help='password') parser.add_argument('password', type=str, help='password')
parser.add_argument('is_admin', nargs='?', default=False, type=str, help='is admin') parser.add_argument('is_admin', nargs='?', default=False, type=str, help='is admin')
parser.add_argument('predefined_token', nargs='?', default='', type=str, help='predefined token')
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
email = kwargs['email'] email = kwargs['email']
password = kwargs['password'] password = kwargs['password']
is_admin = kwargs['is_admin'] is_admin = kwargs['is_admin']
predefined_token = kwargs['predefined_token']
institution = Institution.objects.get(name=kwargs['institution']) institution = Institution.objects.get(name=kwargs['institution'])
self.create_user(institution, email, password, is_admin) self.create_user(institution, email, password, is_admin, predefined_token)
def create_user(self, institution, email, password, is_admin): def create_user(self, institution, email, password, is_admin, predefined_token):
self.u = User.objects.create( self.u = User.objects.create(
institution=institution, institution=institution,
email=email, email=email,
@ -34,6 +36,10 @@ class Command(BaseCommand):
) )
self.u.set_password(password) self.u.set_password(password)
self.u.save() self.u.save()
token = uuid4() if predefined_token:
token = predefined_token
else:
token = uuid4()
Token.objects.create(token=token, owner=self.u) Token.objects.create(token=token, owner=self.u)
print(f"TOKEN: {token}") print(f"TOKEN: {token}")

43
utils/save_snapshots.py Normal file
View File

@ -0,0 +1,43 @@
import os
import json
import shutil
from datetime import datetime
from django.conf import settings
def move_json(path_name, user, place="snapshots"):
if place != "snapshots":
place = "placeholders"
tmp_snapshots = settings.EVIDENCES_DIR
path_dir = os.path.join(tmp_snapshots, user, place)
if os.path.isfile(path_name):
shutil.copy(path_name, path_dir)
os.remove(path_name)
def save_in_disk(data, user, place="snapshots"):
uuid = data.get('uuid', '')
now = datetime.now()
year = now.year
month = now.month
day = now.day
hour = now.hour
minutes = now.minute
tmp_snapshots = settings.EVIDENCES_DIR
if place != "snapshots":
place = "placeholders"
name_file = f"{year}-{month}-{day}-{hour}-{minutes}_{uuid}.json"
path_dir = os.path.join(tmp_snapshots, user, place, "errors")
path_name = os.path.join(path_dir, name_file)
if not os.path.isdir(path_dir):
os.system(f'mkdir -p {path_dir}')
with open(path_name, 'w') as snapshot_file:
snapshot_file.write(json.dumps(data))
return path_name