diff --git a/.bashrc b/.bashrc new file mode 100644 index 0000000..3d299ab --- /dev/null +++ b/.bashrc @@ -0,0 +1,8 @@ +# START_FEATURE ecs +# If we're on prod, make the terminal red +if [ "$DEPLOY_ENVIRONMENT" == "prod" ]; then + export PS1="\[\e[31m\]\u@\h:\w\$ \[\e[0m\]" +fi + +alias djm="uv run --no-sync manage.py" +# END_FEATURE ecs diff --git a/.gitignore b/.gitignore index 1441253..4021544 100644 --- a/.gitignore +++ b/.gitignore @@ -53,7 +53,12 @@ venv.bak/ # Installed packages node_modules/ +# ECS deploy script +.deploy/ +.git-copy/ + # Compiled Files +static/js/dist* staticfiles/ staticfiles/* # START_FEATURE sass_bootstrap @@ -64,3 +69,8 @@ staticfiles/* static/webpack_bundles/ webpack-stats.json # END_FEATURE django_react + +.terraform + +# Locally uploaded files +uploads/ diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Dockerfile b/Dockerfile index 0fe8706..4810e8c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,36 @@ -# START_FEATURE docker -FROM python:3.11.4-slim-buster +# START_FEATURE ecs +# START_FEATURE vue +# ----------------------------------- NPM ------------------------------------ # +FROM node:24-slim AS node-deps + +WORKDIR /app + +# Install required system dependencies +RUN apt-get update \ + && apt-get install -y --no-install-recommends bzip2 + +# Add only files needed for dependency installation +COPY package.json ./ + +# Install Node dependencies with caching +RUN npm install && npm cache clean --force + +# Build vue dist +COPY . /app/ +RUN npm run vue-build + +# END_FEATURE vue +# ---------------------------------- Python ---------------------------------- # + +FROM python:3.12.13-slim-trixie + +# Set working directory WORKDIR /app ADD requirements.txt /app/requirements.txt +# Install system and python dependencies RUN set -ex \ && buildDeps=" \ build-essential \ @@ -12,8 +38,15 @@ RUN set -ex \ " \ && deps=" \ postgresql-client \ + git \ + # (editing) + vim \ + # (debug) + curl \ + htop \ " \ - && apt-get update && apt-get install -y $buildDeps $deps --no-install-recommends \ + && apt-get update \ + && apt-get install -y $buildDeps $deps --no-install-recommends \ && pip install --no-cache-dir -r /app/requirements.txt \ && apt-get purge -y --auto-remove $buildDeps \ $(! command -v gpg > /dev/null || echo 'gnupg dirmngr') \ @@ -22,29 +55,25 @@ RUN set -ex \ ENV VIRTUAL_ENV /env ENV PATH /env/bin:$PATH -# START_FEATURE django_react -COPY ./nwb.config.js /app/nwb.config.js -COPY ./package.json /app/package.json -COPY ./package-lock.json /app/package-lock.json -RUN npm install -# END_FEATURE django_react +# START_FEATURE vue +# Copy Node dependencies from node-deps stage +COPY --from=node-deps /app/node_modules /app/node_modules +COPY --from=node-deps /app/package*.json /app/ +COPY --from=node-deps /app/static/js/dist/ /app/static/js/dist/ +# END_FEATURE vue +# Copy application files and .env.example COPY . /app/ COPY ./config/.env.example /app/config/.env +COPY .bashrc /root/ -# START_FEATURE django_react -RUN ./node_modules/.bin/nwb build --no-vendor -# END_FEATURE django_react - +# Compile static assets # START_FEATURE sass_bootstrap RUN python manage.py compilescss # END_FEATURE sass_bootstrap - RUN python manage.py collectstatic --noinput - RUN rm /app/config/.env -EXPOSE 8000 - -CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "config.wsgi:application"] -# END_FEATURE docker +EXPOSE 8080 +CMD ["gunicorn", "--bind", ":8080", "--workers", "15", "config.wsgi:application"] +# END_FEATURE ecs diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/admin.py b/app/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/app/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/app/apps.py b/app/apps.py new file mode 100644 index 0000000..ed327d2 --- /dev/null +++ b/app/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AppConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'app' diff --git a/app/constants.py b/app/constants.py new file mode 100644 index 0000000..a2e43ab --- /dev/null +++ b/app/constants.py @@ -0,0 +1,5 @@ +SAMPLE_OBJECT_PK_URL_KWARG = "sample_object_id" + +# START_FEATURE direct_upload +ATTACHMENT_PK_URL_KWARG = "attachment_id" +# END_FEATURE direct_upload diff --git a/app/forms.py b/app/forms.py new file mode 100644 index 0000000..6c6e4e6 --- /dev/null +++ b/app/forms.py @@ -0,0 +1,46 @@ +from crispy_forms.helper import Layout +from crispy_forms.layout import Fieldset +from django import forms +from django.http import HttpRequest +from app.models import Attachment, SampleObject +from common.fields import DirectUploadFileField +from common.forms import ActionFormMixin, CrispyFormMixin + + +class SampleObjectBaseForm(CrispyFormMixin, ActionFormMixin, forms.ModelForm): + request: HttpRequest + + # START_FEATURE direct_upload + attachments = DirectUploadFileField(queryset=Attachment.objects.filter(deleted_on=None), required=False) + # END_FEATURE direct_upload + + class Meta: + model = SampleObject + exclude = ['created_by'] + + layout = Layout( + Fieldset( + "Details", + "name", + "description" + ), + # START_FEATURE direct_upload + "attachments" + # END_FEATURE direct_upload + ) + + def __init__(self, request: HttpRequest, *args, **kwargs): + super().__init__(*args, **kwargs) + self.request = request + + +class SampleObjectCreateForm(SampleObjectBaseForm): + action_title = "Create Sample Object" + + def save(self, commit=True): + self.instance.created_by = self.request.user + return super().save(commit) + + +class SampleObjectEditForm(SampleObjectBaseForm): + action_title = "Edit {instance}" diff --git a/app/migrations/0001_initial.py b/app/migrations/0001_initial.py new file mode 100644 index 0000000..3a8d0b6 --- /dev/null +++ b/app/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# Generated by Django 5.2.12 on 2026-03-16 18:26 + +import common.models +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Attachment', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('created_on', models.DateTimeField(auto_now_add=True)), + ('updated_on', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=512)), + ('file', models.FileField(max_length=1024, upload_to=common.models.get_upload_prefix)), + ('upload_completed_on', models.DateTimeField(null=True)), + ('deleted_on', models.DateTimeField(null=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SampleObject', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('created_on', models.DateTimeField(auto_now_add=True)), + ('updated_on', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=512, unique=True)), + ('description', models.TextField(blank=True, default='')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/app/migrations/0002_initial.py b/app/migrations/0002_initial.py new file mode 100644 index 0000000..d35abd5 --- /dev/null +++ b/app/migrations/0002_initial.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.12 on 2026-03-16 18:26 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('app', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='attachment', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='files', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='sampleobject', + name='attachments', + field=models.ManyToManyField(related_name='sample_objects', to='app.attachment'), + ), + migrations.AddField( + model_name='sampleobject', + name='created_by', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sample_objects', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/app/migrations/__init__.py b/app/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..abab0c7 --- /dev/null +++ b/app/models.py @@ -0,0 +1,26 @@ +from django.db import models + +from common.models import TimestampedModel, UploadFile, User + + +class SampleObject(TimestampedModel): + created_by = models.ForeignKey(User, related_name="sample_objects", on_delete=models.PROTECT) + + # START_FEATURE direct_upload + attachments = models.ManyToManyField("Attachment", related_name="sample_objects") + # END_FEATURE direct_upload + + name = models.CharField(max_length=512, unique=True) + description = models.TextField(default="", blank=True) + + def __str__(self) -> str: + return f'Sample Object {self.name}' + + def get_attachments(self): + return self.attachments.filter(deleted_on=None) + + +# START_FEATURE direct_upload +class Attachment(UploadFile): + pass +# END_FEATURE direct_upload diff --git a/app/serializers.py b/app/serializers.py new file mode 100644 index 0000000..7c854a7 --- /dev/null +++ b/app/serializers.py @@ -0,0 +1,35 @@ +# START_FEATURE direct_upload +from app.constants import ATTACHMENT_PK_URL_KWARG +from django.utils.formats import date_format +from common.serializers import UserSerializer +from django.urls import reverse +from rest_framework import serializers + +from app.models import Attachment + + +class AttachmentSerializer(serializers.ModelSerializer): + user = UserSerializer() + + class Meta: + model = Attachment + exclude = [] + + def to_representation(self, instance): + rep = super().to_representation(instance) + rep["view_url"] = reverse('attachment_open', kwargs={ + ATTACHMENT_PK_URL_KWARG: instance.id, + }) + rep["download_url"] = reverse('attachment_download', kwargs={ + ATTACHMENT_PK_URL_KWARG: instance.id, + }) + rep["delete_url"] = reverse('attachment_delete', kwargs={ + ATTACHMENT_PK_URL_KWARG: instance.id, + }) + rep["created_on"] = date_format(instance.created_on, format="DATETIME_FORMAT") + rep["upload_completed_on"] = date_format(instance.upload_completed_on, format="DATETIME_FORMAT") + if instance.file.storage.exists(instance.file.name): + rep["size"] = instance.file.size + rep["path"] = instance.file.name + return rep +# END_FEATURE direct_upload diff --git a/app/templates/app/dashboard.html b/app/templates/app/dashboard.html new file mode 100644 index 0000000..ba73e5a --- /dev/null +++ b/app/templates/app/dashboard.html @@ -0,0 +1,99 @@ +{% extends "base_templates/base.html" %} + + +{# START_FEATURE sass_bootstrap #} +{% load sass_tags %} +{# END_FEATURE sass_bootstrap #} + +{% block title %}Dashboard{% endblock %} + +{% block head %} + +{# START_FEATURE sass_bootstrap #} + +{# END_FEATURE sass_bootstrap #} +{% endblock %} + +{% block body %} +
+

Dashboard

+

Hello {{ user.email }}!

+ +
+
+
+

Sample Objects

+ + + +
+
+ {% if sample_objects %} + + + + + + + {# START_FEATURE direct_upload #} + + {# END_FEATURE direct_upload #} + + + + + {% for sample_object in sample_objects %} + + + + + {# START_FEATURE direct_upload #} + + + {# END_FEATURE direct_upload #} + + {% endfor %} + +
NameCreated ByCreated OnAttachmentsActions
{{ sample_object.name }}{{ sample_object.created_by }}{{ sample_object.created_on }} + {% for attachment in sample_object.get_attachments %} + {% if not attachment.deleted_on %} + {{ attachment.name }} + {% endif %} + {% endfor %} + + +
+ {% else %} +
+ + No Objects +
+ {% endif %} +
+
+
+ + {# START_FEATURE direct_upload #} +
+
+

All Attachments

+ +
+
+ {# END_FEATURE direct_upload #} +
+{% endblock %} diff --git a/app/templates/app/sample_object_detail.html b/app/templates/app/sample_object_detail.html new file mode 100644 index 0000000..43073d8 --- /dev/null +++ b/app/templates/app/sample_object_detail.html @@ -0,0 +1,54 @@ +{% extends "base_templates/base.html" %} + +{% block title %}Sample Object {{sample_object.name }}{% endblock %} + +{% block body %} +
+
+
+

Sample Object {{ sample_object.name }}

+ Created by {{ sample_object.created_by }} on {{ sample_object.created_on }} +
+ + +
+ +
+
+

Description

+

{{ sample_object.description }}

+
+
+ + {# START_FEATURE direct_upload #} +
+
+

Attachments

+ +
+
+
+{# END_FEATURE direct_upload #} +{% endblock %} diff --git a/app/templates/app/sample_object_form.html b/app/templates/app/sample_object_form.html new file mode 100644 index 0000000..f3e546b --- /dev/null +++ b/app/templates/app/sample_object_form.html @@ -0,0 +1,12 @@ +{% extends "base_templates/base.html" %} + +{% load crispy_forms_tags %} + +{% block title %}{{ form.action_title_formatted }}{% endblock %} + +{% block body %} +
+

{{ form.action_title_formatted }}

+ {% crispy form %} +
+{% endblock %} diff --git a/app/templates/widgets/direct_upload_file_input.html b/app/templates/widgets/direct_upload_file_input.html new file mode 100644 index 0000000..53dc993 --- /dev/null +++ b/app/templates/widgets/direct_upload_file_input.html @@ -0,0 +1,18 @@ +{# START_FEATURE direct_upload #} +
+
+ +
+
+{# END_FEATURE direct_upload #} diff --git a/app/tests.py b/app/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/app/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/app/urls.py b/app/urls.py new file mode 100644 index 0000000..63068fb --- /dev/null +++ b/app/urls.py @@ -0,0 +1,60 @@ +from django.urls import path + +from app import views +from app.constants import SAMPLE_OBJECT_PK_URL_KWARG, ATTACHMENT_PK_URL_KWARG + +urlpatterns = [ + path( + f"dashboard/", + views.DashboardView.as_view(), + name='dashboard' + ), + path( + f"sample-objects/create/", + views.SampleObjectCreateView.as_view(), + name='sample-object-create' + ), + path( + f"sample-objects//", + views.SampleObjectDetailView.as_view(), + name='sample-object' + ), + path( + f"sample-objects//edit/", + views.SampleObjectEditView.as_view(), + name='sample-object-edit' + ), + + # START_FEATURE direct_upload + path( + f"attachments/upload-start/", + views.FileUploadStartView.as_view(), + name='attachment_upload_start' + ), + path( + f"attachments//upload-stream/", + views.FileUploadStreamView.as_view(), + name='attachment_upload_stream' + ), + path( + f"attachments//upload-complete/", + views.FileUploadCompleteView.as_view(), + name='attachment_upload_complete' + ), + path( + f"attachments//delete/", + views.FileDeleteView.as_view(), + name='attachment_delete' + ), + path( + f"attachments//download/", + views.FileDownloadView.as_view(), + name='attachment_download' + ), + path( + f"attachments//open/", + views.FileOpenView.as_view(), + name='attachment_open' + ), + # END_FEATURE direct_upload +] diff --git a/app/views.py b/app/views.py new file mode 100644 index 0000000..6c7c37a --- /dev/null +++ b/app/views.py @@ -0,0 +1,189 @@ +import json +import re + +from common.mixins import PermissionRequiredMixin, RequestMixin +from common.permissions import PermissionType +from common.s3 import create_presigned_upload_url +from django.conf import settings +from django.core.files.storage import default_storage +from django.core.files.storage.filesystem import FileSystemStorage +from django.db import transaction +from django.http import JsonResponse +from django.http.response import HttpResponse +from django.shortcuts import get_object_or_404 +from django.urls import reverse, reverse_lazy +from django.utils import timezone +from django.views.generic.base import TemplateView, View +from django.views.generic.detail import DetailView, SingleObjectMixin +from django.views.generic.edit import CreateView, UpdateView + +from app.constants import SAMPLE_OBJECT_PK_URL_KWARG +from app.forms import SampleObjectCreateForm, SampleObjectEditForm + +# START_FEATURE direct_upload +from app.constants import ATTACHMENT_PK_URL_KWARG +from app.models import Attachment, SampleObject +from app.serializers import AttachmentSerializer +# END_FEATURE direct_upload + + +class DashboardView(PermissionRequiredMixin, TemplateView): + permission_required = PermissionType.dashboard + template_name = "app/dashboard.html" + + # START_FEATURE direct_upload + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['storage_backend'] = "s3" if settings.AWS_STORAGE_BUCKET_NAME else "local" + context['attachments'] = json.dumps([ + AttachmentSerializer(attachment).data + for attachment in Attachment.objects.filter(deleted_on=None) + ]) + context['sample_objects'] = SampleObject.objects.prefetch_related('attachments') + return context + # END_FEATURE direct_upload + + +class SampleObjectCreateView(PermissionRequiredMixin, RequestMixin, CreateView): + permission_required = PermissionType.dashboard + template_name = "app/sample_object_form.html" + form_class = SampleObjectCreateForm + model = SampleObject + success_url = reverse_lazy('dashboard') + + +class SampleObjectDetailView(PermissionRequiredMixin, DetailView): + permission_required = PermissionType.dashboard + template_name = "app/sample_object_detail.html" + model = SampleObject + pk_url_kwarg = SAMPLE_OBJECT_PK_URL_KWARG + context_object_name = "sample_object" + + # START_FEATURE direct_upload + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['storage_backend'] = "s3" if settings.AWS_STORAGE_BUCKET_NAME else "local" + context['attachments'] = json.dumps([ + AttachmentSerializer(attachment).data + for attachment in self.get_object().attachments.filter(deleted_on=None) + ]) + return context + # END_FEATURE direct_upload + + +class SampleObjectEditView(PermissionRequiredMixin, RequestMixin, UpdateView): + permission_required = PermissionType.dashboard + template_name = "app/sample_object_form.html" + form_class = SampleObjectEditForm + model = SampleObject + pk_url_kwarg = SAMPLE_OBJECT_PK_URL_KWARG + context_object_name = "sample_object" + + def get_success_url(self): + return reverse('sample-object', kwargs={ + SAMPLE_OBJECT_PK_URL_KWARG: self.get_object().id, + }) + + +# START_FEATURE direct_upload +class FileUploadStartView(PermissionRequiredMixin, View): + permission_required = PermissionType.dashboard + + def link_objects(self, attachment: Attachment, request): + for key, value in request.GET.items(): + + # Expect a query parameter key in this form: '__' + # The value should be a UUID primary key + if re.match(r"(\w+)__(\w+)", key): + link_type, obj_accessor = key.split("__") + if link_type == "mtm": + if (accessor := getattr(attachment, obj_accessor)): + accessor.add(get_object_or_404(accessor.model, pk=value)) + + @transaction.atomic + def post(self, request, *args, **kwargs): + attachment_name = request.POST['name'] + + # Create `Attachment` instance + attachment = Attachment.objects.create( + user=request.user, + name=attachment_name, + ) + self.link_objects(attachment, request) + + # Generate S3 path for file + storage_path = Attachment._meta.get_field('file').generate_filename(attachment, attachment_name) + attachment.file = storage_path + attachment.save() + + # Set the presigned upload and completion URLs on response paylod + serialized_data = {} + url_kwargs = {ATTACHMENT_PK_URL_KWARG: str(attachment.id)} + if isinstance(default_storage, FileSystemStorage): + serialized_data['upload_presigned_url'] = reverse("attachment_upload_stream", kwargs=url_kwargs) + serialized_data['upload_complete_url'] = reverse("attachment_upload_complete", kwargs=url_kwargs) + else: + serialized_data['upload_presigned_url'] = create_presigned_upload_url(object_name=storage_path) + serialized_data['upload_complete_url'] = reverse("attachment_upload_complete", kwargs=url_kwargs) + + return JsonResponse(serialized_data) + + +class FileUploadStreamView(PermissionRequiredMixin, SingleObjectMixin, View): + permission_required = PermissionType.dashboard + + model = Attachment + pk_url_kwarg = ATTACHMENT_PK_URL_KWARG + + def post(self, request, *args, **kwargs): + instance = self.get_object() + + file = request.FILES.get('file') + if not file: + return HttpResponse(status=400) + + instance.update(file=file) + + return JsonResponse({ + "id": instance.id, + "name": instance.name, + "url": instance.file.url, + }) + + +class FileUploadCompleteView(FileUploadStreamView): + permission_required = PermissionType.dashboard + + def post(self, request, *args, **kwargs): + instance = self.get_object() + instance.update(upload_completed_on=timezone.now()) + return JsonResponse(AttachmentSerializer(instance).data) + + +class FileDownloadView(PermissionRequiredMixin, SingleObjectMixin, View): + permission_required = PermissionType.dashboard + + model = Attachment + pk_url_kwarg = ATTACHMENT_PK_URL_KWARG + + def get(self, request, *args, **kwargs): + instance = self.get_object() + return instance.download_file() + + +class FileOpenView(FileDownloadView): + permission_required = PermissionType.dashboard + + def get(self, request, *args, **kwargs): + instance = self.get_object() + return instance.view_file() + + +class FileDeleteView(FileUploadStreamView): + permission_required = PermissionType.dashboard + + def post(self, request, *args, **kwargs): + instance = self.get_object() + instance.update(deleted_on=timezone.now()) + return HttpResponse(status=200) +# END_FEATURE direct_upload diff --git a/common/fields.py b/common/fields.py new file mode 100644 index 0000000..1f21f7a --- /dev/null +++ b/common/fields.py @@ -0,0 +1,60 @@ +# START_FEATURE direct_upload +import json +from uuid import uuid4 +from django import forms +from django.core.files.storage import default_storage +from django.core.files.storage.filesystem import FileSystemStorage +from django.db.models import QuerySet +from django.urls import reverse + +from app.serializers import AttachmentSerializer + + +class DirectUploadFileInput(forms.SelectMultiple): + queryset: QuerySet + template_name = "widgets/direct_upload_file_input.html" + + def get_context(self, name, value, attrs): + context: dict = super().get_context(name, value, attrs) + context["upload_start_url"] = reverse("attachment_upload_start") + context["storage_backend"] = ("filesystem" if isinstance(default_storage, FileSystemStorage) else "s3") + context["queryset_json"] = json.dumps([AttachmentSerializer(f).data for f in self.queryset.all()]) + return context + + +class DirectUploadFileField(forms.ModelMultipleChoiceField): + """ + A field that allows for direct file uploads to S3 in form submissions. + + In order for the `DirectUploadFileField` to work properly in `ModelForm` instances, + make sure that ForeignKey relationships between objects and attachments originate + from the object and point to the attachment, not the other way around. + """ + + widget = DirectUploadFileInput + + def __init__( + self, + queryset: QuerySet, + allowed_file_types: list[str] = [], + multiple: bool = True, + max_number_of_files: int | None = None, + **kwargs, + ): + self.allowed_file_types = [ft if ft.startswith(".") else "." + ft for ft in allowed_file_types] + self.multiple = multiple + self.max_number_of_files = max_number_of_files + super().__init__(queryset=queryset, **kwargs) + self.widget.queryset = self.queryset + + if allowed_file_types: + self.help_text = "Only the following file types are allowed: " + ", ".join(self.allowed_file_types) + + def widget_attrs(self, widget): + attrs = super().widget_attrs(widget) + attrs['multiple'] = self.multiple + attrs['required'] = self.required + attrs['max_number_of_files'] = self.max_number_of_files + attrs['allowed_file_types'] = self.allowed_file_types + return attrs +# END_FEATURE direct_upload diff --git a/common/forms.py b/common/forms.py index 16a2750..95b35e2 100644 --- a/common/forms.py +++ b/common/forms.py @@ -1,39 +1,35 @@ # START_FEATURE crispy_forms -from django import forms +from crispy_forms.helper import FormHelper, Layout +from crispy_forms.layout import Button, Submit +from django.db.models import Model -from crispy_forms.helper import FormHelper -from crispy_forms.layout import Submit - -class CrispyFormMixin(object): - submit_label = "Save" - form_action = "" +class CrispyFormMixin: + submit_label: str = "Save" + cancel_label: str = "Cancel" + form_action: str = "" + form_tag: bool = True + default_inputs: bool = True + layout: Layout | None = None def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_method = "POST" self.helper.form_action = self.form_action - self.helper.add_input(Submit("submit", self.submit_label)) + if self.layout is not None: + self.helper.layout = self.layout + if self.default_inputs: + self.helper.add_input(Submit("submit", self.submit_label)) + self.helper.add_input(Button("cancel", self.cancel_label, + css_class="btn-secondary", onclick="history.back()")) # END_FEATURE crispy_forms -# START_FEATURE crispy_forms -class SampleForm(CrispyFormMixin, forms.Form): - # TODO: delete me; this is just a reference example - is_company = forms.CharField(label="company", required=False, widget=forms.CheckboxInput()) - email = forms.EmailField( - label="email", max_length=30, required=True, widget=forms.TextInput(), help_text="Insert your email" - ) - first_name = forms.CharField(label="first name", max_length=5, required=True, widget=forms.TextInput()) - last_name = forms.CharField(label="last name", max_length=5, required=True, widget=forms.TextInput()) - datetime_field = forms.SplitDateTimeField(label="date time", widget=forms.SplitDateTimeWidget()) +class ActionFormMixin: + instance: type[Model] + action_title: str = "Perform Action" - def clean(self): - super().clean() - password1 = self.cleaned_data.get("password1", None) - password2 = self.cleaned_data.get("password2", None) - if not password1 and not password2 or password1 != password2: - raise forms.ValidationError("Passwords dont match") - return self.cleaned_data -# END_FEATURE crispy_forms + @property + def action_title_formatted(self): + return self.action_title.format(instance=self.instance) diff --git a/common/helpers.py b/common/helpers.py new file mode 100644 index 0000000..26f2e7a --- /dev/null +++ b/common/helpers.py @@ -0,0 +1,13 @@ +# START_FEATURE direct_upload +import os + + +def get_attachment_extension(filename: str): + _, extension = os.path.splitext(filename) + extension = extension.lower().replace('.', '') + return extension + +def remove_attachment_extension(filename: str): + name, _ = os.path.splitext(filename) + return name +# END_FEATURE direct_upload diff --git a/common/migrations/0001_initial.py b/common/migrations/0001_initial.py index d26f799..56c7b29 100644 --- a/common/migrations/0001_initial.py +++ b/common/migrations/0001_initial.py @@ -1,11 +1,10 @@ -# Generated by Django 3.2.6 on 2021-10-04 19:37 +# Generated by Django 5.2.12 on 2026-03-16 18:26 -import common.models -from django.conf import settings -from django.db import migrations, models import django.db.models.deletion import django.utils.timezone import uuid +from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): @@ -32,8 +31,9 @@ class Migration(migrations.Migration): ('created_on', models.DateTimeField(auto_now_add=True)), ('updated_on', models.DateTimeField(auto_now=True)), ('email', models.EmailField(max_length=254, unique=True)), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ('role', models.CharField(choices=[('guest', 'Guest'), ('standard', 'Standard')], default='standard', max_length=128)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), ], options={ 'verbose_name': 'user', diff --git a/common/mixins.py b/common/mixins.py new file mode 100644 index 0000000..ac2a2bc --- /dev/null +++ b/common/mixins.py @@ -0,0 +1,32 @@ +from django.contrib.auth.mixins import AccessMixin +from django.core.exceptions import ImproperlyConfigured +from django.views.generic.edit import FormMixin + +from common.permissions import PermissionType + + +class PermissionRequiredMixin(AccessMixin): + permission_required = None + + def dispatch(self, request, *args, **kwargs): + if not self.permission_required: + raise ImproperlyConfigured("Permission for view not specified.") + if not request.user.is_authenticated: + return self.handle_no_permission() + + # Check that user has the required permission. + if ( + not request.user.has_permission(self.permission_required) + and self.permission_required != PermissionType.none + ): + return self.handle_no_permission() + + return super().dispatch(request, *args, **kwargs) + + +class RequestMixin(FormMixin): + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['request'] = self.request + return kwargs diff --git a/common/models.py b/common/models.py index 9d87842..253d9a8 100644 --- a/common/models.py +++ b/common/models.py @@ -1,10 +1,27 @@ +import mimetypes import uuid from django.contrib.auth.models import AbstractUser from django.db import models +from django.http import FileResponse, Http404, HttpResponse, HttpResponseRedirect +from django.conf import settings +import boto3 + +# START_FEATURE direct_upload +from django.urls import reverse +from django.template.defaultfilters import filesizeformat +from common.helpers import get_attachment_extension, remove_attachment_extension +from app.constants import ATTACHMENT_PK_URL_KWARG +# END_FEATURE direct_upload from common.managers import UserManager +# START_FEATURE sentry +from sentry_sdk import capture_message + +from common.permissions import ROLE_PERMISSIONS, UserRole +# END_FEATURE sentry + class TimestampedModel(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) @@ -29,6 +46,7 @@ class Meta: # Create your models here. class User(AbstractUser, TimestampedModel): email = models.EmailField(unique=True) + # START_FEATURE django_social username = None # disable the AbstractUser.username field USERNAME_FIELD = "email" @@ -37,10 +55,19 @@ class User(AbstractUser, TimestampedModel): objects = UserManager() # END_FEATURE django_social + role = models.CharField(max_length=128, default=UserRole.standard, choices=UserRole.choices) + + @property + def permissions(self): + return ROLE_PERMISSIONS[self.role] + + def has_permission(self, permission): + return permission in self.permissions + # START_FEATURE django_storages # TODO: delete me; this is just a reference example -def get_s3_path(instance, filename): +def get_upload_prefix(instance, filename): return "%s/%s/%s" % ( "uploads", instance.user_id, @@ -49,14 +76,79 @@ def get_s3_path(instance, filename): class UploadFile(TimestampedModel): - user = models.ForeignKey(User, related_name="files", on_delete=models.PROTECT) - file = models.FileField( - max_length=1024, - upload_to=get_s3_path - ) class Meta: abstract = True + + user = models.ForeignKey(User, related_name="files", on_delete=models.PROTECT) + name = models.CharField(max_length=512) + file = models.FileField(max_length=1024, upload_to=get_upload_prefix) + + # START_FEATURE direct_upload + upload_completed_on = models.DateTimeField(null=True) + deleted_on = models.DateTimeField(null=True) + + def get_download_url(self, download_on_open: bool = True): + extension = get_attachment_extension(self.file.name) + filename = f"{remove_attachment_extension(self.name)}.{extension}".replace('"', '') + content_type, _ = mimetypes.guess_type(self.file.name) + s3_filename = self.file.name + + content_disposition = "attachment" if download_on_open else "inline" + + try: + + # Download file directly from S3 + if (s3_bucket_name := settings.AWS_STORAGE_BUCKET_NAME): + s3 = boto3.client('s3') + url = s3.generate_presigned_url( + 'get_object', + Params={ + "Bucket": str(s3_bucket_name), + "Key": s3_filename, + "ResponseContentDisposition": f'{content_disposition}; filename="{filename}"', + "ResponseContentType": content_type or "application/octet-stream", + }, + ExpiresIn=900 + ) + return HttpResponseRedirect(url) + else: + return FileResponse( + self.file.open(), + as_attachment=download_on_open, + filename=filename + ) + + except Exception: + capture_message(f"Failed to get object URL from S3 for ({self}) with path ({s3_filename})") + raise Http404() + + def download_file(self) -> FileResponse | HttpResponse: + return self.get_download_url(download_on_open=True) + + def view_file(self) -> FileResponse | HttpResponse: + return self.get_download_url(download_on_open=False) + + # TODO: Should replace this with a DRF serializer + def get_context_data(self): + context = { + "id": self.id, + "user": self.user.email, + "name": self.name, + "upload_completed_on": self.upload_completed_on, + "view_url": reverse('attachment_open', kwargs={ + ATTACHMENT_PK_URL_KWARG: self.id, + }), + "download_url": reverse('attachment_download', kwargs={ + ATTACHMENT_PK_URL_KWARG: self.id, + }) + } + if self.file.storage.exists(self.file.name): + context["size"] = filesizeformat(self.file.size) + context["path"] = self.file.name + return context + # END_FEATURE direct_upload + # END_FEATURE django_storages diff --git a/common/permissions.py b/common/permissions.py new file mode 100644 index 0000000..25ee4d4 --- /dev/null +++ b/common/permissions.py @@ -0,0 +1,20 @@ +from django.db.models import TextChoices + + +class UserOrganization(TextChoices): + organization = ("organization", "Organization") + + +class UserRole(TextChoices): + guest = ("guest", "Guest") + standard = ("standard", "Standard") + + +class PermissionType(TextChoices): + dashboard = ("dashboard", "View Dashboard") + none = ("none", "No permission required") + + +ROLE_PERMISSIONS = {} +ROLE_PERMISSIONS[UserRole.guest] = [] +ROLE_PERMISSIONS[UserRole.standard] = list(PermissionType) diff --git a/common/s3.py b/common/s3.py new file mode 100644 index 0000000..4ccb009 --- /dev/null +++ b/common/s3.py @@ -0,0 +1,42 @@ +# START_FEATURE direct_upload +import boto3 + +from django.conf import settings + +from django.core.files.storage import default_storage +from storages.backends.s3boto3 import S3Boto3Storage + + +def create_presigned_upload_url(object_name: str, expiration: int = 3600): + """ + Generate a presigned URL to upload an S3 object + (see https://boto3.amazonaws.com/v1/documentation/api/latest/guide/s3-presigned-urls.html) + + :param bucket_name: string + :param object_name: string + :param fields: Dictionary of prefilled form fields + :param conditions: List of conditions to include in the policy + :param expiration: Time in seconds for the presigned URL to remain valid + :return: Dictionary with the following keys: + url: URL to post to + fields: Dictionary of form fields and values to submit with the POST + :return: None if error. + """ + + # Generate a presigned S3 POST URL + s3_client = boto3.client('s3') + response = None + if isinstance(default_storage, S3Boto3Storage): + response = s3_client.generate_presigned_post( + settings.AWS_STORAGE_BUCKET_NAME, + object_name, + ExpiresIn=expiration + ) + + # The response contains the presigned URL and required fields + return response + + else: + raise Exception(f"Cannot create a presigned upload URL for {type(default_storage)} storage") + +# END_FEATURE direct_upload diff --git a/common/serializers.py b/common/serializers.py new file mode 100644 index 0000000..634eafa --- /dev/null +++ b/common/serializers.py @@ -0,0 +1,13 @@ +from rest_framework import serializers +from common.models import User + + +class UserSerializer(serializers.ModelSerializer): + + class Meta: + model = User + fields = [ + "first_name", + "last_name", + "email", + ] diff --git a/common/templates/base_templates/base.html b/common/templates/base_templates/base.html index f8a2cc3..7e7b4e5 100644 --- a/common/templates/base_templates/base.html +++ b/common/templates/base_templates/base.html @@ -1,6 +1,9 @@ {# START_FEATURE sass_bootstrap #} {% load sass_tags %} {# END_FEATURE sass_bootstrap #} + +{% load static %} + @@ -11,42 +14,12 @@ {# END_FEATURE sass_bootstrap #} + + {% block head %}{% endblock %} - + {% include "base_templates/navbar.html" %} {% if not PRODUCTION and not LOCALHOST %}
@@ -69,9 +42,22 @@

Warning

{{ message }}
{% endfor %} - {% block body %}{% endblock %} +
+ {% block body %} + {% endblock %} +
{# END_FEATURE bootstrap_messages #} - + + {# START_FEATURE direct_upload #} + + {# END_FEATURE direct_upload #} + + {% block bottom_javascript %} + + {% endblock %} + diff --git a/common/templates/base_templates/navbar.html b/common/templates/base_templates/navbar.html new file mode 100644 index 0000000..cad932e --- /dev/null +++ b/common/templates/base_templates/navbar.html @@ -0,0 +1,58 @@ + diff --git a/common/templates/common/index.html b/common/templates/common/index.html index a6a6242..37b54d1 100644 --- a/common/templates/common/index.html +++ b/common/templates/common/index.html @@ -15,12 +15,10 @@ {% endblock %} {% block body %} -
-

Welcome to [PROJECT]. Please log in to continue.

- - {# START_FEATURE sass_bootstrap #} - {# TODO: delete me; this is just a reference example #} - - {# END_FEATURE sass_bootstrap #} -
+

Welcome to [PROJECT]. Please log in to continue.

+ + {# START_FEATURE sass_bootstrap #} + {# TODO: delete me; this is just a reference example #} + + {# END_FEATURE sass_bootstrap #} {% endblock %} diff --git a/common/templatetags/vue.py b/common/templatetags/vue.py new file mode 100644 index 0000000..822c339 --- /dev/null +++ b/common/templatetags/vue.py @@ -0,0 +1,16 @@ +# START_FEATURE vue +import re +from django.template.defaultfilters import register + + +@register.filter +def to_v_init_arg(model_name): + """ + Converts a v-model field key to the value expected for a v-init arg + """ + v_init_arg = ":".join(model_name.split(".")) + # https://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-snake-case + camel_case_re = re.compile(r'(?=6.9.0" } @@ -372,11 +382,11 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", - "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -1601,17 +1611,407 @@ } }, "node_modules/@babel/types": { - "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", - "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@insin/npm-install-webpack-plugin": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@insin/npm-install-webpack-plugin/-/npm-install-webpack-plugin-5.0.0.tgz", @@ -1743,10 +2143,21 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -1812,193 +2223,936 @@ "url": "https://opencollective.com/popperjs" } }, - "node_modules/@sentry-internal/tracing": { - "version": "7.120.3", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.120.3.tgz", - "integrity": "sha512-Ausx+Jw1pAMbIBHStoQ6ZqDZR60PsCByvHdw/jdH9AqPrNE9xlBSf9EwcycvmrzwyKspSLaB52grlje2cRIUMg==", + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "license": "MIT", "dependencies": { - "@sentry/core": "7.120.3", - "@sentry/types": "7.120.3", - "@sentry/utils": "7.120.3" + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" }, "engines": { - "node": ">=8" + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } } }, - "node_modules/@sentry/core": { - "version": "7.120.3", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.120.3.tgz", - "integrity": "sha512-vyy11fCGpkGK3qI5DSXOjgIboBZTriw0YDx/0KyX5CjIjDDNgp5AGgpgFkfZyiYiaU2Ww3iFuKo4wHmBusz1uA==", - "dependencies": { - "@sentry/types": "7.120.3", - "@sentry/utils": "7.120.3" - }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/@sentry/integrations": { - "version": "7.120.3", - "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.120.3.tgz", - "integrity": "sha512-6i/lYp0BubHPDTg91/uxHvNui427df9r17SsIEXa2eKDwQ9gW2qRx5IWgvnxs2GV/GfSbwcx4swUB3RfEWrXrQ==", - "dependencies": { - "@sentry/core": "7.120.3", - "@sentry/types": "7.120.3", - "@sentry/utils": "7.120.3", - "localforage": "^1.8.1" - }, - "engines": { - "node": ">=8" + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz", + "integrity": "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.0.tgz", + "integrity": "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz", + "integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.0.tgz", + "integrity": "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.0.tgz", + "integrity": "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.0.tgz", + "integrity": "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.0.tgz", + "integrity": "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.0.tgz", + "integrity": "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.0.tgz", + "integrity": "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.0.tgz", + "integrity": "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.0.tgz", + "integrity": "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.0.tgz", + "integrity": "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.0.tgz", + "integrity": "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.0.tgz", + "integrity": "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.0.tgz", + "integrity": "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.0.tgz", + "integrity": "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.0.tgz", + "integrity": "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.0.tgz", + "integrity": "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.0.tgz", + "integrity": "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.0.tgz", + "integrity": "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.0.tgz", + "integrity": "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.0.tgz", + "integrity": "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.0.tgz", + "integrity": "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.0.tgz", + "integrity": "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.0.tgz", + "integrity": "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sentry-internal/tracing": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.120.3.tgz", + "integrity": "sha512-Ausx+Jw1pAMbIBHStoQ6ZqDZR60PsCByvHdw/jdH9AqPrNE9xlBSf9EwcycvmrzwyKspSLaB52grlje2cRIUMg==", + "dependencies": { + "@sentry/core": "7.120.3", + "@sentry/types": "7.120.3", + "@sentry/utils": "7.120.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/core": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.120.3.tgz", + "integrity": "sha512-vyy11fCGpkGK3qI5DSXOjgIboBZTriw0YDx/0KyX5CjIjDDNgp5AGgpgFkfZyiYiaU2Ww3iFuKo4wHmBusz1uA==", + "dependencies": { + "@sentry/types": "7.120.3", + "@sentry/utils": "7.120.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/integrations": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.120.3.tgz", + "integrity": "sha512-6i/lYp0BubHPDTg91/uxHvNui427df9r17SsIEXa2eKDwQ9gW2qRx5IWgvnxs2GV/GfSbwcx4swUB3RfEWrXrQ==", + "dependencies": { + "@sentry/core": "7.120.3", + "@sentry/types": "7.120.3", + "@sentry/utils": "7.120.3", + "localforage": "^1.8.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/node": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.120.3.tgz", + "integrity": "sha512-t+QtekZedEfiZjbkRAk1QWJPnJlFBH/ti96tQhEq7wmlk3VszDXraZvLWZA0P2vXyglKzbWRGkT31aD3/kX+5Q==", + "dependencies": { + "@sentry-internal/tracing": "7.120.3", + "@sentry/core": "7.120.3", + "@sentry/integrations": "7.120.3", + "@sentry/types": "7.120.3", + "@sentry/utils": "7.120.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/types": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.120.3.tgz", + "integrity": "sha512-C4z+3kGWNFJ303FC+FxAd4KkHvxpNFYAFN8iMIgBwJdpIl25KZ8Q/VdGn0MLLUEHNLvjob0+wvwlcRBBNLXOow==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/utils": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.120.3.tgz", + "integrity": "sha512-UDAOQJtJDxZHQ5Nm1olycBIsz2wdGX8SdzyGVHmD8EOQYAeDZQyIlQYohDe9nazdIOQLZCIc3fU0G9gqVLkaGQ==", + "dependencies": { + "@sentry/types": "7.120.3" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@transloadit/prettier-bytes": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@transloadit/prettier-bytes/-/prettier-bytes-0.3.5.tgz", + "integrity": "sha512-xF4A3d/ZyX2LJWeQZREZQw+qFX4TGQ8bGVP97OLRt6sPO6T0TNHBFTuRHOJh7RNmYOBmQ9MHxpolD9bXihpuVA==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/html-minifier-terser": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", + "integrity": "sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==" + }, + "node_modules/@types/node": { + "version": "22.15.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz", + "integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/q": { + "version": "1.5.8", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz", + "integrity": "sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw==" + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==" + }, + "node_modules/@types/source-list-map": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.6.tgz", + "integrity": "sha512-5JcVt1u5HDmlXkwOD2nslZVllBBc7HDuOICfiZah2Z0is8M8g+ddAEawbmd3VjedfDHBzxCaXLs07QEmb7y54g==" + }, + "node_modules/@types/tapable": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.12.tgz", + "integrity": "sha512-bTHG8fcxEqv1M9+TD14P8ok8hjxoOCkfKc8XXLaaD05kI7ohpeI956jtDOD3XHKBQrlyPughUtzm1jtVhHpA5Q==" + }, + "node_modules/@types/uglify-js": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.5.tgz", + "integrity": "sha512-TU+fZFBTBcXj/GpDpDaBmgWk/gn96kMZ+uocaFUlV2f8a6WdMzzI44QBCmGcCiYR0Y6ZlNRiyUyKKt5nl/lbzQ==", + "dependencies": { + "source-map": "^0.6.1" + } + }, + "node_modules/@types/uglify-js/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@types/webpack": { + "version": "4.41.40", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.40.tgz", + "integrity": "sha512-u6kMFSBM9HcoTpUXnL6mt2HSzftqb3JgYV6oxIgL2dl6sX6aCa5k6SOkzv5DuZjBTPUE/dJltKtwwuqrkZHpfw==", + "dependencies": { + "@types/node": "*", + "@types/tapable": "^1", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "anymatch": "^3.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/@types/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-4nZOdMwSPHZ4pTEZzSp0AsTM4K7Qmu40UKW4tJDiOVs20UzYF9l+qUe4s0ftfN0pin06n+5cWWDJXH+sbhAiDw==", + "dependencies": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.7.3" + } + }, + "node_modules/@types/webpack-sources/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/webpack/node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/webpack/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@uppy/aws-s3": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@uppy/aws-s3/-/aws-s3-5.1.0.tgz", + "integrity": "sha512-UBz+shrtDbnOf11AboDrkc9Fq2Cdf8HbFftE+gqfxig6fkv5rHpHhBCLkl8wCGAq+X/CxdqvvNhm/OM23Uzw2w==", + "dependencies": { + "@uppy/companion-client": "^5.1.1", + "@uppy/utils": "^7.1.4" + }, + "peerDependencies": { + "@uppy/core": "^5.2.0" + } + }, + "node_modules/@uppy/companion-client": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@uppy/companion-client/-/companion-client-5.1.1.tgz", + "integrity": "sha512-DzrOWTbIZHvtgAFXBMYHk2wD27NjpBSVhY2tEiEIUhPd2CxbFRZjHM/N3HOt3VwZEAP471QWFLlJRWPcIY3A2Q==", + "dependencies": { + "@uppy/utils": "^7.1.1", + "namespace-emitter": "^2.0.1", + "p-retry": "^6.1.0" + }, + "peerDependencies": { + "@uppy/core": "^5.1.1" + } + }, + "node_modules/@uppy/companion-client/node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@uppy/companion-client/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@uppy/components": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@uppy/components/-/components-1.1.0.tgz", + "integrity": "sha512-omiNBzJn49FQznkSwOIGn3TKz+3r4T+y8sxWIBDMO6De2genzywRk1drAWO9GbSAF3htlVuvamNojQ2pSLeh3w==", + "dependencies": { + "clsx": "^2.1.1", + "dequal": "^2.0.3", + "preact": "^10.5.13", + "pretty-bytes": "^6.1.1" + }, + "peerDependencies": { + "@uppy/core": "^5.1.1", + "@uppy/image-editor": "^4.0.2", + "@uppy/screen-capture": "^5.0.1", + "@uppy/webcam": "^5.0.2" + }, + "peerDependenciesMeta": { + "@uppy/image-editor": { + "optional": true + }, + "@uppy/screen-capture": { + "optional": true + }, + "@uppy/webcam": { + "optional": true + } + } + }, + "node_modules/@uppy/core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@uppy/core/-/core-5.2.0.tgz", + "integrity": "sha512-uvfNyz4cnaplt7LYJmEZHuqOuav0tKp4a9WKJIaH6iIj7XiqYvS2J5SEByexAlUFlzefOAyjzj4Ja2dd/8aMrw==", + "dependencies": { + "@transloadit/prettier-bytes": "^0.3.4", + "@uppy/store-default": "^5.0.0", + "@uppy/utils": "^7.1.4", + "lodash": "^4.17.21", + "mime-match": "^1.0.2", + "namespace-emitter": "^2.0.1", + "nanoid": "^5.0.9", + "preact": "^10.5.13" + } + }, + "node_modules/@uppy/core/node_modules/nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@uppy/dashboard": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@uppy/dashboard/-/dashboard-5.1.0.tgz", + "integrity": "sha512-TCuDbsAWokPO2LzubDoXyBskY/DHFB9tyDeHJe1FGBHrm0sY2zd+yEW7c4HNMDiIYCs/AdFJWAjQosCT07rD6Q==", + "dependencies": { + "@transloadit/prettier-bytes": "^0.3.4", + "@uppy/provider-views": "^5.2.0", + "@uppy/thumbnail-generator": "^5.1.0", + "@uppy/utils": "^7.1.4", + "classnames": "^2.2.6", + "lodash": "^4.17.21", + "nanoid": "^5.0.9", + "preact": "^10.5.13", + "shallow-equal": "^3.0.0" + }, + "peerDependencies": { + "@uppy/core": "^5.2.0" + } + }, + "node_modules/@uppy/dashboard/node_modules/nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@uppy/provider-views": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@uppy/provider-views/-/provider-views-5.2.2.tgz", + "integrity": "sha512-NAazIJ5sjrAc6++CeJ/u9dB5gDaaAOLHrYeEmWs/HqLlftlIinRZOybnyzJRXwI8jWI/FK5moluzt2HXu6dPQQ==", + "dependencies": { + "@uppy/utils": "^7.1.5", + "classnames": "^2.2.6", + "lodash": "^4.17.21", + "nanoid": "^5.0.9", + "p-queue": "^8.0.0", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^5.2.0" + } + }, + "node_modules/@uppy/provider-views/node_modules/nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/@uppy/store-default": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@uppy/store-default/-/store-default-5.0.0.tgz", + "integrity": "sha512-hQtCSQ1yGiaval/wVYUWquYGDJ+bpQ7e4FhUUAsRQz1x1K+o7NBtjfp63O9I4Ks1WRoKunpkarZ+as09l02cPw==", + "license": "MIT" + }, + "node_modules/@uppy/thumbnail-generator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@uppy/thumbnail-generator/-/thumbnail-generator-5.1.0.tgz", + "integrity": "sha512-QAKJHHkMrD/30GOyUb5U9HyJ7Ie3jiMLp4pVdw27PoA4pNV7fDQz0tyDeRPj2H+BWPEB1NsTSSfHI2pjHNI+OQ==", + "dependencies": { + "@uppy/utils": "^7.1.4", + "exifr": "^7.0.0" + }, + "peerDependencies": { + "@uppy/core": "^5.2.0" + } + }, + "node_modules/@uppy/utils": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/@uppy/utils/-/utils-7.1.5.tgz", + "integrity": "sha512-Vz4WGTjef6WebECGur4clWjpkET4o3bdvPMj1m2sD5cL+dTt69m+FIE5h5JD3HBMLEPTXPVkrXGMIFcbOYC12Q==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "preact": "^10.5.13" + } + }, + "node_modules/@uppy/vue": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@uppy/vue/-/vue-3.1.0.tgz", + "integrity": "sha512-V007Zg/GPS0OJ6F9PkhOhZAMVtSfsq6VO9cq3q1qMdfvknxgoxhcLbEz8I2zJNa2pvF7NeOn46gcGQMyrEvxTQ==", + "dependencies": { + "@uppy/components": "^1.0.3", + "preact": "^10.5.13", + "shallow-equal": "^3.0.0" + }, + "peerDependencies": { + "@uppy/core": "^5.0.2", + "@uppy/dashboard": "^5.0.2", + "vue": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@uppy/dashboard": { + "optional": true + }, + "@uppy/status-bar": { + "optional": true + } } }, - "node_modules/@sentry/node": { - "version": "7.120.3", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.120.3.tgz", - "integrity": "sha512-t+QtekZedEfiZjbkRAk1QWJPnJlFBH/ti96tQhEq7wmlk3VszDXraZvLWZA0P2vXyglKzbWRGkT31aD3/kX+5Q==", + "node_modules/@uppy/xhr-upload": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@uppy/xhr-upload/-/xhr-upload-5.1.1.tgz", + "integrity": "sha512-Vp0HWVA8o+niC2uISxPt0pZ+95bHHkk9HzNaUTrff/vq+20Ln68BS2auJhc9ecJzI6SKAlGZ342dcTQ/onw0nA==", "dependencies": { - "@sentry-internal/tracing": "7.120.3", - "@sentry/core": "7.120.3", - "@sentry/integrations": "7.120.3", - "@sentry/types": "7.120.3", - "@sentry/utils": "7.120.3" + "@uppy/companion-client": "^5.1.1", + "@uppy/utils": "^7.1.5" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/types": { - "version": "7.120.3", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.120.3.tgz", - "integrity": "sha512-C4z+3kGWNFJ303FC+FxAd4KkHvxpNFYAFN8iMIgBwJdpIl25KZ8Q/VdGn0MLLUEHNLvjob0+wvwlcRBBNLXOow==", - "engines": { - "node": ">=8" + "peerDependencies": { + "@uppy/core": "^5.2.0" } }, - "node_modules/@sentry/utils": { - "version": "7.120.3", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.120.3.tgz", - "integrity": "sha512-UDAOQJtJDxZHQ5Nm1olycBIsz2wdGX8SdzyGVHmD8EOQYAeDZQyIlQYohDe9nazdIOQLZCIc3fU0G9gqVLkaGQ==", + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz", + "integrity": "sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==", + "license": "MIT", "dependencies": { - "@sentry/types": "7.120.3" + "@rolldown/pluginutils": "1.0.0-beta.53" }, "engines": { - "node": ">=8" + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", + "vue": "^3.2.25" } }, - "node_modules/@types/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "node_modules/@vue/compiler-core": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz", + "integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==", "dependencies": { - "@types/minimatch": "*", - "@types/node": "*" + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.27", + "entities": "^7.0.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" } }, - "node_modules/@types/html-minifier-terser": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", - "integrity": "sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" - }, - "node_modules/@types/minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==" + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } }, - "node_modules/@types/node": { - "version": "22.15.29", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz", - "integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==", + "node_modules/@vue/compiler-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz", + "integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==", "dependencies": { - "undici-types": "~6.21.0" + "@vue/compiler-core": "3.5.27", + "@vue/shared": "3.5.27" } }, - "node_modules/@types/q": { - "version": "1.5.8", - "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz", - "integrity": "sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw==" - }, - "node_modules/@types/source-list-map": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.6.tgz", - "integrity": "sha512-5JcVt1u5HDmlXkwOD2nslZVllBBc7HDuOICfiZah2Z0is8M8g+ddAEawbmd3VjedfDHBzxCaXLs07QEmb7y54g==" - }, - "node_modules/@types/tapable": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.12.tgz", - "integrity": "sha512-bTHG8fcxEqv1M9+TD14P8ok8hjxoOCkfKc8XXLaaD05kI7ohpeI956jtDOD3XHKBQrlyPughUtzm1jtVhHpA5Q==" - }, - "node_modules/@types/uglify-js": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.5.tgz", - "integrity": "sha512-TU+fZFBTBcXj/GpDpDaBmgWk/gn96kMZ+uocaFUlV2f8a6WdMzzI44QBCmGcCiYR0Y6ZlNRiyUyKKt5nl/lbzQ==", + "node_modules/@vue/compiler-sfc": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz", + "integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==", "dependencies": { - "source-map": "^0.6.1" + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.27", + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" } }, - "node_modules/@types/uglify-js/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/@vue/compiler-sfc/node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, "engines": { - "node": ">=0.10.0" + "node": "^10 || ^12 || >=14" } }, - "node_modules/@types/webpack": { - "version": "4.41.40", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.40.tgz", - "integrity": "sha512-u6kMFSBM9HcoTpUXnL6mt2HSzftqb3JgYV6oxIgL2dl6sX6aCa5k6SOkzv5DuZjBTPUE/dJltKtwwuqrkZHpfw==", + "node_modules/@vue/compiler-ssr": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz", + "integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==", "dependencies": { - "@types/node": "*", - "@types/tapable": "^1", - "@types/uglify-js": "*", - "@types/webpack-sources": "*", - "anymatch": "^3.0.0", - "source-map": "^0.6.0" + "@vue/compiler-dom": "3.5.27", + "@vue/shared": "3.5.27" } }, - "node_modules/@types/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-4nZOdMwSPHZ4pTEZzSp0AsTM4K7Qmu40UKW4tJDiOVs20UzYF9l+qUe4s0ftfN0pin06n+5cWWDJXH+sbhAiDw==", + "node_modules/@vue/reactivity": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.27.tgz", + "integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==", "dependencies": { - "@types/node": "*", - "@types/source-list-map": "*", - "source-map": "^0.7.3" + "@vue/shared": "3.5.27" } }, - "node_modules/@types/webpack-sources/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "engines": { - "node": ">= 8" + "node_modules/@vue/runtime-core": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.27.tgz", + "integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/shared": "3.5.27" } }, - "node_modules/@types/webpack/node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/@vue/runtime-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz", + "integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==", "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" + "@vue/reactivity": "3.5.27", + "@vue/runtime-core": "3.5.27", + "@vue/shared": "3.5.27", + "csstype": "^3.2.3" } }, - "node_modules/@types/webpack/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" + "node_modules/@vue/server-renderer": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.27.tgz", + "integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==", + "dependencies": { + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "vue": "3.5.27" } }, + "node_modules/@vue/shared": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.27.tgz", + "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==" + }, "node_modules/@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -2272,6 +3426,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-html": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", @@ -3226,6 +4391,22 @@ "@popperjs/core": "^2.11.8" } }, + "node_modules/bootstrap-icons": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz", + "integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3724,6 +4905,11 @@ "node": ">= 0.4" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/clean-css": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", @@ -3862,6 +5048,14 @@ "node": ">=0.10.0" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/coa": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", @@ -4623,10 +5817,9 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "peer": true + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" }, "node_modules/custom-event": { "version": "1.0.1", @@ -4887,6 +6080,14 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, "node_modules/des.js": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", @@ -5545,6 +6746,46 @@ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -5617,6 +6858,11 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -5746,6 +6992,11 @@ "which": "bin/which" } }, + "node_modules/exifr": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.3.tgz", + "integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==" + }, "node_modules/expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -7007,6 +8258,35 @@ "node": ">=6" } }, + "node_modules/html-minifier-terser/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/html-minifier-terser/node_modules/terser": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", + "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", + "dependencies": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/html-minifier-terser/node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/html-webpack-plugin": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.3.0.tgz", @@ -7747,6 +9027,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-network-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", + "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", @@ -7830,6 +9121,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -8859,6 +10159,14 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -9087,6 +10395,15 @@ "node": ">= 0.6" } }, + "node_modules/mime-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mime-match/-/mime-match-1.0.2.tgz", + "integrity": "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==", + "license": "ISC", + "dependencies": { + "wildcard": "^1.1.0" + } + }, "node_modules/mime-types": { "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", @@ -9691,12 +11008,35 @@ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, + "node_modules/namespace-emitter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/namespace-emitter/-/namespace-emitter-2.0.1.tgz", + "integrity": "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==", + "license": "MIT" + }, "node_modules/nan": { "version": "2.22.2", "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", "integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==", "optional": true }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -10634,6 +11974,26 @@ "node": ">=6" } }, + "node_modules/p-queue": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.1.tgz", + "integrity": "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==", + "dependencies": { + "eventemitter3": "^5.0.1", + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==" + }, "node_modules/p-retry": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", @@ -10645,6 +12005,17 @@ "node": ">=6" } }, + "node_modules/p-timeout": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", + "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -11623,6 +12994,16 @@ "node": ">=0.10.0" } }, + "node_modules/preact": { + "version": "10.28.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz", + "integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prepend-http": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", @@ -11631,6 +13012,17 @@ "node": ">=0.10.0" } }, + "node_modules/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pretty-error": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz", @@ -12407,6 +13799,86 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" }, + "node_modules/rollup": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz", + "integrity": "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.0", + "@rollup/rollup-android-arm64": "4.57.0", + "@rollup/rollup-darwin-arm64": "4.57.0", + "@rollup/rollup-darwin-x64": "4.57.0", + "@rollup/rollup-freebsd-arm64": "4.57.0", + "@rollup/rollup-freebsd-x64": "4.57.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", + "@rollup/rollup-linux-arm-musleabihf": "4.57.0", + "@rollup/rollup-linux-arm64-gnu": "4.57.0", + "@rollup/rollup-linux-arm64-musl": "4.57.0", + "@rollup/rollup-linux-loong64-gnu": "4.57.0", + "@rollup/rollup-linux-loong64-musl": "4.57.0", + "@rollup/rollup-linux-ppc64-gnu": "4.57.0", + "@rollup/rollup-linux-ppc64-musl": "4.57.0", + "@rollup/rollup-linux-riscv64-gnu": "4.57.0", + "@rollup/rollup-linux-riscv64-musl": "4.57.0", + "@rollup/rollup-linux-s390x-gnu": "4.57.0", + "@rollup/rollup-linux-x64-gnu": "4.57.0", + "@rollup/rollup-linux-x64-musl": "4.57.0", + "@rollup/rollup-openbsd-x64": "4.57.0", + "@rollup/rollup-openharmony-arm64": "4.57.0", + "@rollup/rollup-win32-arm64-msvc": "4.57.0", + "@rollup/rollup-win32-ia32-msvc": "4.57.0", + "@rollup/rollup-win32-x64-gnu": "4.57.0", + "@rollup/rollup-win32-x64-msvc": "4.57.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-external-globals": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-external-globals/-/rollup-plugin-external-globals-0.13.0.tgz", + "integrity": "sha512-wBS3hmoF0OtEnA0lWsmTC6Nhnkk2zjZbfhaX2gLo8VnfNGFdGhiYKwMpIPQPrYbAw+mAYUYmoHYktAl1eZHgVw==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "estree-walker": "^3.0.3", + "is-reference": "^3.0.2", + "magic-string": "^0.30.10" + }, + "peerDependencies": { + "rollup": "^2.25.0 || ^3.3.0 || ^4.1.4" + } + }, + "node_modules/rollup-plugin-external-globals/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/rollup/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -12673,17 +14145,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/serialize-error/node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/serialize-javascript": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", @@ -12876,6 +14337,11 @@ "node": ">=0.10.0" } }, + "node_modules/shallow-equal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-3.1.0.tgz", + "integrity": "sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -13263,6 +14729,14 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", @@ -13933,19 +15407,22 @@ } }, "node_modules/terser": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", - "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "optional": true, + "peer": true, "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" + "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" }, "engines": { - "node": ">=6.0.0" + "node": ">=10" } }, "node_modules/terser-webpack-plugin": { @@ -14002,6 +15479,11 @@ "node": ">= 8" } }, + "node_modules/terser-webpack-plugin/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/terser-webpack-plugin/node_modules/find-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", @@ -14082,19 +15564,42 @@ "node": ">= 8" } }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "node_modules/terser-webpack-plugin/node_modules/terser": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", + "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", + "dependencies": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=6.0.0" + } }, - "node_modules/terser/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/terser/node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "optional": true, + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, "engines": { - "node": ">=0.10.0" + "node": ">=0.4.0" } }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "optional": true, + "peer": true + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -14151,6 +15656,48 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==" }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tmatch": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-2.0.1.tgz", @@ -14300,9 +15847,9 @@ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "engines": { "node": ">=10" }, @@ -14788,6 +16335,146 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", @@ -14801,6 +16488,26 @@ "node": ">=0.10.0" } }, + "node_modules/vue": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", + "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-sfc": "3.5.27", + "@vue/runtime-dom": "3.5.27", + "@vue/server-renderer": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/watchpack": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", @@ -15257,6 +16964,11 @@ "node": ">=0.10.0" } }, + "node_modules/webpack/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/webpack/node_modules/is-wsl": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", @@ -15294,6 +17006,22 @@ "node": ">=0.10.0" } }, + "node_modules/webpack/node_modules/terser": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", + "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", + "dependencies": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/webpack/node_modules/terser-webpack-plugin": { "version": "1.4.6", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.6.tgz", @@ -15487,6 +17215,12 @@ "node": ">=4" } }, + "node_modules/wildcard": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-1.1.2.tgz", + "integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==", + "license": "MIT" + }, "node_modules/wordwrap": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", diff --git a/package.json b/package.json index f756dc1..dda9631 100644 --- a/package.json +++ b/package.json @@ -3,26 +3,41 @@ "version": "0.1.0", "private": true, "dependencies": { + "@babel/plugin-transform-react-jsx": "~7.16.7", + "@uppy/aws-s3": "^5.1.0", + "@uppy/core": "^5.2.0", + "@uppy/dashboard": "^5.1.0", + "@uppy/vue": "^3.1.0", + "@uppy/xhr-upload": "^5.1.1", + "@vitejs/plugin-vue": "^6.0.3", "bootstrap": "^5.1.1", + "bootstrap-icons": "^1.13.1", "django-react-loader": "^0.1.7", + "humanize-plus": "^1.8.2", "nwb": "^0.24.7", "react": "^16.13.1", "react-dom": "^16.13.1", - "webpack-bundle-tracker": "^0.4.3", - "@babel/plugin-transform-react-jsx": "~7.16.7" + "rollup-plugin-external-globals": "^0.13.0", + "vite": "^7.3.1", + "vue": "^3.5.27", + "webpack-bundle-tracker": "^0.4.3" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", - "eject": "react-scripts eject" + "eject": "react-scripts eject", + "vue-dev": "vite build --watch --mode development", + "vue-build": "vite build", + "vue-preview": "vite preview" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] - },"browserslist": { + }, + "browserslist": { "production": [ ">0.2%", "not dead", @@ -33,5 +48,10 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "@vue/language-server": "^3.2.5", + "@vue/typescript-plugin": "^3.2.5", + "typescript-language-server": "^5.1.3" } } diff --git a/requirements.in b/requirements.in index 1292355..627001d 100644 --- a/requirements.in +++ b/requirements.in @@ -3,6 +3,7 @@ django-environ django-extensions>=3.2 requests psycopg2 +whitenoise ####### OPTIONAL FEATURES ####### @@ -28,6 +29,10 @@ sentry-sdk django-storages # END_FEATURE django_storages +# START_FEATURE direct_upload +boto3 +# END_FEATURE + # START_FEATURE docker gunicorn # END_FEATURE docker diff --git a/src/components/FileUploadDashboard.vue b/src/components/FileUploadDashboard.vue new file mode 100644 index 0000000..625135c --- /dev/null +++ b/src/components/FileUploadDashboard.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/src/components/FileUploadDirect.vue b/src/components/FileUploadDirect.vue new file mode 100644 index 0000000..71f1465 --- /dev/null +++ b/src/components/FileUploadDirect.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/src/Components/Hello.js b/src/components/Hello.jsx similarity index 100% rename from src/Components/Hello.js rename to src/components/Hello.jsx diff --git a/src/components/index.js b/src/components/index.js new file mode 100644 index 0000000..f9138db --- /dev/null +++ b/src/components/index.js @@ -0,0 +1,12 @@ +// START_FEATURE vue +const modules = import.meta.glob("./*.vue", { eager: true }) +const components = {} + +for (const path in modules) { + // Extract the file name without extension + const name = path.match(/\.\/(.*)\.vue$/)[1] + components[name] = modules[path].default +} + +export default components +// END_FEATURE vue diff --git a/src/composables/csrf.js b/src/composables/csrf.js new file mode 100644 index 0000000..5fa0529 --- /dev/null +++ b/src/composables/csrf.js @@ -0,0 +1,11 @@ +// START_FEATURE vue +import { onMounted, ref } from "vue" + +export function useCSRF() { + const csrf = ref(null) + onMounted(() => { + csrf.value = window.csrfmiddlewaretoken + }) + return { csrf } +} +// END_FEATURE vue diff --git a/src/composables/fetch.js b/src/composables/fetch.js new file mode 100644 index 0000000..7011f62 --- /dev/null +++ b/src/composables/fetch.js @@ -0,0 +1,64 @@ +// START_FEATURE vue +import { useCSRF } from "./csrf.js" + +export function useFetch() { + const { csrf } = useCSRF() + const post = (url, options = {}, headers = {}) => { + return fetch(url, { + headers: { + ...headers, + "X-CSRFTOKEN": csrf.value, + }, + ...{ + method: "POST", + ...options, + }, + }) + } + const get = (url, options = {}, headers = {}, queryParams = {}) => { + let searchParams = new URLSearchParams(queryParams) + return fetch(url + "?" + searchParams.toString(), { + headers, + ...{ + method: "GET", + ...options, + }, + }) + } + const poll = async ( + valueRef, + url, + handleResponse = null, + onSuccess = null, + pollRate = 2000, + options = {}, + headers = {}, + queryParams = {}, + ) => { + const doQuery = async () => { + return await get(url, options, headers, queryParams).then(async res => { + if (res.status === 202) { + return false + } else { + if (handleResponse) { + return await handleResponse(res) + } + return await res.json() + } + }) + } + const tryAgain = () => { + setTimeout(async () => { + const res = await doQuery() + if (res !== false) { + valueRef.value = res + } else { + tryAgain() + } + }, pollRate) + } + tryAgain() + } + return { post, get, poll } +} +// END_FEATURE vue diff --git a/src/directives/index.js b/src/directives/index.js new file mode 100644 index 0000000..c053f0e --- /dev/null +++ b/src/directives/index.js @@ -0,0 +1,12 @@ +// START_FEATURE vue +const modules = import.meta.glob("./*.js", { eager: true }) +const components = {} + +for (const path in modules) { + // Extract the file name without extension + const name = path.match(/\.\/(.*)\.js$/)[1] + components[name] = modules[path].default +} + +export default components +// END_FEATURE vue diff --git a/src/directives/v-init.js b/src/directives/v-init.js new file mode 100644 index 0000000..add8d75 --- /dev/null +++ b/src/directives/v-init.js @@ -0,0 +1,59 @@ +// START_FEATURE vue +import "vue" + +function set(context, path, value) { + /** + * set is a helper function which allows us to dynamically target and set values within an object, allowing + * v-init to interpret a model like 'listPullParams.housing_type' as context['listPullParams']['housing_type'] + */ + var pList = path.split(".") + if (pList.length > 2) { + throw "You cannot use v-init with a nest factor of > 2 e.g. a.b.c" + } + if (pList.length >= 2) { + // we are setting the value of pList[0][pList[1]] = value + var target = pList.shift() + if (context.$data[target] === undefined) { + context.$data[target] = {} + } + set(context[target], pList.join("."), value) + } else { + context[pList[0]] = value + } +} + +const initArgToModel = arg => { + // Convert v-init arg to v-model format + const fieldKeys = arg.split(":").map(key => { + // convert each field key from kebab-case to camelCase + const camelChunks = key.split("-").map((chunk, i) => { + if (i === 0) { + return chunk + } // + return chunk.charAt(0).toUpperCase() + chunk.substring(1) + }) + return camelChunks.join("") + }) + return fieldKeys.join(".") +} + +const VInit = [ + // v-init automatically sets the data value on page load; the expression is given by the binding argument + "init", + { + beforeMount(el, binding, vNode) { + if (!binding.arg) { + console.log(el, binding, vNode) + throw new Error("You cannot use v-init without providing an argument.") + } + const expression = initArgToModel(binding.arg) + set(binding.instance, expression, binding.value) + binding.instance.$nextTick(() => { + binding.instance.$forceUpdate() + }) + }, + }, +] + +export default VInit +// END_FEATURE vue diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..09adfcc --- /dev/null +++ b/src/main.js @@ -0,0 +1,28 @@ +// START_FEATURE vue +import components from "./components" +import directives from "./directives" + +import "bootstrap" +import "bootstrap-icons/font/bootstrap-icons.css" + +const MainVueApp = { + install: (app, options) => { + app.config.compilerOptions.whitespace = "preserve" + + for (const componentName in components) { + const component = components[componentName] + app.component(componentName, component) + } + + for (const directiveName in directives) { + const directive = directives[directiveName] + // arr[0] is name of directive, arr[1] is content + app.directive(directive[0], directive[1]) + } + }, +} + +export * from "./directives" +export * from "./components" +export default MainVueApp +// END_FEATURE vue \ No newline at end of file diff --git a/src/Pages/Home.js b/src/pages/Home.jsx similarity index 100% rename from src/Pages/Home.js rename to src/pages/Home.jsx diff --git a/src/pages/default.js b/src/pages/default.js new file mode 100644 index 0000000..79a2daf --- /dev/null +++ b/src/pages/default.js @@ -0,0 +1,6 @@ +// START_FEATURE vue +import { createApp } from "vue" +import MainVueApp from "../main" + +createApp({}).use(MainVueApp).mount("#app") +// END_FEATURE vue diff --git a/static/styles/base.scss b/static/styles/base.scss index fcea8f4..d3dee40 100644 --- a/static/styles/base.scss +++ b/static/styles/base.scss @@ -2,4 +2,5 @@ * Base app styling */ @import '_variables.scss'; + @import 'bootstrap/scss/bootstrap.scss'; diff --git a/terraform/deployments/shared/main.tf b/terraform/deployments/shared/main.tf new file mode 100644 index 0000000..8954634 --- /dev/null +++ b/terraform/deployments/shared/main.tf @@ -0,0 +1,34 @@ +# START_FEATURE ecs +terraform { + + backend "s3" { + profile = "zag-dev-cli" + bucket = "matthewzagarandev" + key = "shared/terraform.tfstate" + region = "us-east-1" + } + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.59" + } + } + + required_version = ">= 1.2.0" +} + +provider "aws" { + region = "us-east-1" + profile = "zag-dev-cli" +} + +module "shared" { + source = "../../modules/shared" + + # --------------------------------- REQUIRED --------------------------------- # + + environment = "shared" + application_url = "staging-deploy.zagaran.com" +} +# END_FEATURE ecs diff --git a/terraform/deployments/staging/main.tf b/terraform/deployments/staging/main.tf new file mode 100644 index 0000000..becefff --- /dev/null +++ b/terraform/deployments/staging/main.tf @@ -0,0 +1,108 @@ +# START_FEATURE ecs +terraform { + + backend "s3" { + profile = "zag-dev-cli" + bucket = "matthewzagarandev" + key = "staging/terraform.tfstate" + region = "us-east-1" + } + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.59" + } + } + + required_version = ">= 1.2.0" +} + +provider "aws" { + region = "us-east-1" + profile = "zag-dev-cli" +} + +module "staging" { + source = "../../modules/application" + + # --------------------------------- REQUIRED --------------------------------- # + + environment = "staging" + application_name = "deploy" + application_url = "staging-deploy.dev.zagaran.com" + remote_repo_name = "zagaran/deploy" + + # ------------------------ References other resources ------------------------ # + + data_application_domain = "dev.zagaran.com" + data_email_domain = "dev.zagaran.com" + + # --------------------------------- OPTIONAL --------------------------------- # + + rds_deletion_protection = false + load_balancer_deletion_protection = false + worker_desired_count = 0 + +} + +output "cluster_id" { + description = "The ID of the ECS cluster" + value = module.staging.cluster_id +} + +output "ecr_image_uri" { + description = "ECR URI where the environment's image is stored" + value = module.staging.ecr_image_uri +} + +output "ecr_repository_name" { + description = "The name of the ECR repository" + value = module.staging.ecr_repository_name +} + +output "web_service_name" { + description = "The name of the ECS web service. This is also the container name." + value = module.staging.web_service_name +} + +output "web_network_configuration_security_group" { + description = "The security group used by the ECS web task" + value = tolist(module.staging.web_network_configuration_security_groups)[0] +} + +output "web_network_configuration_subnet" { + description = "The ID of one the subnets used by the web task" + value = tolist(module.staging.web_network_configuration_subnets)[0] +} + +output "web_task_definition_arn" { + description = "The ARN of the ECS web service task definition" + value = module.staging.web_task_definition_arn +} + +output "web_log_group_name" { + description = "The name of the cloudwatch log group for the web service task" + value = module.staging.web_log_group_name +} + +output "worker_service_name" { + description = "The name of the ECS worker service. This is also the container name." + value = module.staging.worker_service_name +} + +output "worker_task_desired_count" { + description = "The intended number of worker tasks" + value = module.staging.worker_task_desired_count +} + +output "worker_log_group_name" { + description = "The name of the cloudwatch log group for the web service task" + value = module.staging.worker_log_group_name +} + +output "s3_bucket_name" { + description = "The name of the S3 bucket" + value = module.staging.s3_bucket_name +} +# END_FEATURE ecs diff --git a/terraform/modules/application/alb.tf b/terraform/modules/application/alb.tf new file mode 100644 index 0000000..712a358 --- /dev/null +++ b/terraform/modules/application/alb.tf @@ -0,0 +1,125 @@ +# START_FEATURE ecs +resource "aws_lb" "alb" { + name = "${local.app_env_name}-alb" + enable_deletion_protection = var.load_balancer_deletion_protection + internal = false + load_balancer_type = "application" + security_groups = [aws_security_group.load_balancer.id] + idle_timeout = var.load_balancer_idle_timeout + subnets = [ + aws_subnet.public_subnet_a.id, + aws_subnet.public_subnet_b.id, + ] +} + +resource "aws_lb_target_group" "target_group" { + name = var.environment + port = 8080 + protocol = "HTTP" + vpc_id = aws_vpc.vpc.id + target_type = "ip" + health_check { + path = "/health-check/" + protocol = "HTTP" + } + lifecycle { + create_before_destroy = false + } + stickiness { + type = "lb_cookie" + cookie_duration = 300 + } +} + + +resource "aws_security_group" "load_balancer" { + name = "${local.app_env_name}-alb" + vpc_id = aws_vpc.vpc.id + + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + ingress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + egress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + egress { + from_port = 8080 + to_port = 8080 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_lb_listener" "http_redirect" { + load_balancer_arn = aws_lb.alb.arn + port = "80" + protocol = "HTTP" + + default_action { + type = "redirect" + + redirect { + port = "443" + protocol = "HTTPS" + status_code = "HTTP_301" + } + } +} + +resource "aws_lb_listener" "https" { + load_balancer_arn = aws_lb.alb.arn + port = "443" + protocol = "HTTPS" + + ssl_policy = var.ssl_policy + certificate_arn = aws_acm_certificate.cert.arn + + default_action { + type = "fixed-response" + + fixed_response { + content_type = "text/plain" + message_body = "Resource is not available" + status_code = "400" + } + } +} + +resource "aws_lb_listener_rule" "target_group_forwarding" { + listener_arn = aws_lb_listener.https.arn + + action { + type = "forward" + target_group_arn = aws_lb_target_group.target_group.arn + } + + condition { + host_header { + values = [var.application_url] + } + } +} +# END_FEATURE ecs diff --git a/terraform/modules/application/ecr.tf b/terraform/modules/application/ecr.tf new file mode 100644 index 0000000..639c84a --- /dev/null +++ b/terraform/modules/application/ecr.tf @@ -0,0 +1,5 @@ +# START_FEATURE ecs +resource "aws_ecr_repository" "ecr" { + name = "${local.app_env_name}-ecr" +} +# END_FEATURE ecs diff --git a/terraform/modules/application/ecs.tf b/terraform/modules/application/ecs.tf new file mode 100644 index 0000000..a6b5714 --- /dev/null +++ b/terraform/modules/application/ecs.tf @@ -0,0 +1,157 @@ +# START_FEATURE ecs +# --------------------------------- Fargate ---------------------------------- # + + +resource "aws_ecs_cluster" "cluster" { + name = "${local.app_env_name}-cluster" + + setting { + name = "containerInsights" + value = "enhanced" + } +} + +resource "aws_ecs_cluster_capacity_providers" "fargate_provider" { + cluster_name = aws_ecs_cluster.cluster.name + capacity_providers = ["FARGATE"] +} + + +# -------------------------------- Cloudwatch -------------------------------- # + + +resource "aws_cloudwatch_log_group" "web_log_group" { + name = "${local.app_env_name}-web" + retention_in_days = 90 +} + +resource "aws_cloudwatch_log_group" "worker_log_group" { + name = "${local.app_env_name}-worker" + retention_in_days = 90 +} + + +# ----------------------------- Task Definitions ----------------------------- # + + +resource "aws_ecs_task_definition" "web" { + family = "${local.app_env_name}-web" + cpu = var.fargate_web_cpu + memory = var.fargate_web_memory + execution_role_arn = aws_iam_role.ecs_execution_role.arn + task_role_arn = aws_iam_role.ecs_task_role.arn + requires_compatibilities = ["FARGATE"] + network_mode = "awsvpc" + container_definitions = jsonencode([ + { + name = "${local.app_env_name}-web" + image = "${aws_ecr_repository.ecr.repository_url}:latest" + secrets = local.ecs_secrets + essential = true + portMappings = [ + { + containerPort = 8080 + hostPort = 8080 + } + ], + logConfiguration = { + logDriver = "awslogs", + options = { + awslogs-group = aws_cloudwatch_log_group.web_log_group.name, + awslogs-region = data.aws_region.current.name, + awslogs-stream-prefix = "ecs" + } + } + } + ]) +} + +resource "aws_ecs_task_definition" "worker" { + family = "${local.app_env_name}-worker" + cpu = var.fargate_worker_cpu + memory = var.fargate_worker_memory + execution_role_arn = aws_iam_role.ecs_execution_role.arn + task_role_arn = aws_iam_role.ecs_task_role.arn + requires_compatibilities = ["FARGATE"] + network_mode = "awsvpc" + container_definitions = jsonencode([ + { + name = "${local.app_env_name}-worker" + image = "${aws_ecr_repository.ecr.repository_url}:latest" + secrets = local.ecs_secrets + command = local.ecs_worker_command + essential = true + portMappings = [ + { + containerPort = 8080 + hostPort = 8080 + } + ], + logConfiguration = { + logDriver = "awslogs", + options = { + awslogs-group = aws_cloudwatch_log_group.worker_log_group.name, + awslogs-region = data.aws_region.current.name, + awslogs-stream-prefix = "ecs" + } + } + } + ]) +} + + +# ------------------------------- ECS Services ------------------------------- # + + +resource "aws_ecs_service" "web" { + name = "${local.app_env_name}-web" + cluster = aws_ecs_cluster.cluster.id + task_definition = aws_ecs_task_definition.web.arn + desired_count = var.web_desired_count + launch_type = "FARGATE" + enable_execute_command = true + + load_balancer { + target_group_arn = aws_lb_target_group.target_group.arn + container_name = "${local.app_env_name}-web" + container_port = 8080 + } + + deployment_circuit_breaker { + enable = true + rollback = true + } + + network_configuration { + subnets = [ + aws_subnet.public_subnet_a.id, + aws_subnet.public_subnet_b.id + ] + security_groups = [aws_security_group.web.id] + assign_public_ip = true + } +} + +resource "aws_ecs_service" "worker" { + name = "${local.app_env_name}-worker" + cluster = aws_ecs_cluster.cluster.id + task_definition = aws_ecs_task_definition.worker.arn + desired_count = var.worker_desired_count + launch_type = "FARGATE" + enable_execute_command = true + + deployment_circuit_breaker { + enable = true + rollback = true + } + + network_configuration { + subnets = [ + aws_subnet.public_subnet_a.id, + aws_subnet.public_subnet_b.id + ] + security_groups = [aws_security_group.worker.id] + assign_public_ip = true + } +} +# END_FEATURE ecs diff --git a/terraform/modules/application/elasticache.tf b/terraform/modules/application/elasticache.tf new file mode 100644 index 0000000..0de6fea --- /dev/null +++ b/terraform/modules/application/elasticache.tf @@ -0,0 +1,36 @@ +# START_FEATURE ecs +resource "aws_elasticache_replication_group" "redis" { + replication_group_id = "${local.app_env_name}-redis" + description = "Redis replication group" + apply_immediately = true + auto_minor_version_upgrade = true + automatic_failover_enabled = true + engine_version = var.redis_engine_version + node_type = var.redis_instance_type + num_cache_clusters = 2 + port = 6379 + preferred_cache_cluster_azs = ["us-east-1a", "us-east-1b"] + security_group_ids = [aws_security_group.redis.id] + subnet_group_name = aws_elasticache_subnet_group.redis.name + + log_delivery_configuration { + destination = aws_cloudwatch_log_group.redis_log_group.name + destination_type = "cloudwatch-logs" + log_format = "text" + log_type = "engine-log" + } +} + +resource "aws_elasticache_subnet_group" "redis" { + name = "${local.app_env_name}-redis-subnets" + subnet_ids = [ + aws_subnet.public_subnet_a.id, + aws_subnet.public_subnet_b.id + ] +} + +resource "aws_cloudwatch_log_group" "redis_log_group" { + name = "${local.app_env_name}-redis" + retention_in_days = 90 +} +# END_FEATURE ecs diff --git a/terraform/modules/application/github.tf b/terraform/modules/application/github.tf new file mode 100644 index 0000000..4619b8e --- /dev/null +++ b/terraform/modules/application/github.tf @@ -0,0 +1,14 @@ +# START_FEATURE ecs +resource "aws_iam_openid_connect_provider" "github" { + url = "https://token.actions.githubusercontent.com" + + client_id_list = [ + "sts.amazonaws.com" + ] + + thumbprint_list = [ + "6938fd4d98bab03faadb97b34396831e3780aea1" + ] +} + +# END_FEATURE ecs diff --git a/terraform/modules/application/iam.tf b/terraform/modules/application/iam.tf new file mode 100644 index 0000000..d8b748c --- /dev/null +++ b/terraform/modules/application/iam.tf @@ -0,0 +1,199 @@ +# START_FEATURE ecs +data "aws_iam_policy_document" "ecs_assume_role_policy" { + statement { + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["ecs-tasks.amazonaws.com"] + } + } +} + +data "aws_iam_policy_document" "ecs_execution_role_policy" { + statement { + effect = "Allow" + actions = [ + "ecr:GetAuthorizationToken" + ] + resources = ["*"] + } + + statement { + effect = "Allow" + actions = [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage" + ] + resources = [ + "arn:aws:ecr:*:*:repository/${aws_ecr_repository.ecr.name}", + ] + } + + statement { + effect = "Allow" + actions = [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ] + resources = [ + "${aws_cloudwatch_log_group.web_log_group.arn}:*", + "${aws_cloudwatch_log_group.worker_log_group.arn}:*", + ] + } + + statement { + effect = "Allow" + actions = [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ] + resources = [ + aws_secretsmanager_secret.web_infrastructure.arn, + data.aws_secretsmanager_secret.web_config.arn, + ] + } + + statement { + effect = "Allow" + actions = [ + "kms:Decrypt" + ] + resources = [ + "arn:aws:kms:*:*:aws/secretsmanager" + ] + } + + statement { + effect = "Allow" + actions = [ + "ecs:RunTask", + "ecs:DescribeServices" + ] + resources = ["*"] + } + + statement { + effect = "Allow" + actions = [ + "autoscaling:*" + ] + resources = ["*"] + } +} + +data "aws_iam_policy_document" "ecs_task_role_policy" { + statement { + effect = "Allow" + actions = [ + "ssmmessages:CreateControlChannel", + "ssmmessages:CreateDataChannel", + "ssmmessages:OpenControlChannel", + "ssmmessages:OpenDataChannel" + ] + resources = ["*"] + } + + statement { + effect = "Allow" + actions = [ + "s3:*" + ] + resources = [ + format("arn:aws:s3:::%s", aws_s3_bucket.bucket.id), + format("arn:aws:s3:::%s/*", aws_s3_bucket.bucket.id) + ] + } + + statement { + effect = "Allow" + actions = [ + "kms:Decrypt" + ] + resources = [ + "arn:aws:kms:*:*:aws/secretsmanager" + ] + } + + statement { + effect = "Allow" + actions = [ + "ses:*" + ] + resources = [ + "*" + ] + } + + statement { + effect = "Allow" + actions = [ + "ecs:RunTask", + "ecs:DescribeServices", + "ecs:StopTask", + "ecs:DescribeTasks", + "iam:GetRole", + "iam:PassRole" + ] + resources = ["*"] + } +} + +resource "aws_iam_role" "ecs_execution_role" { + name = "${local.app_env_name}-ecs-execution-role" + assume_role_policy = data.aws_iam_policy_document.ecs_assume_role_policy.json +} + +resource "aws_iam_role_policy" "ecs_execution_role_policy" { + name = "ecs-execution-role-policy" + role = aws_iam_role.ecs_execution_role.id + policy = data.aws_iam_policy_document.ecs_execution_role_policy.json +} + +resource "aws_iam_role" "ecs_task_role" { + name = "${local.app_env_name}-ecs-task-role" + assume_role_policy = data.aws_iam_policy_document.ecs_assume_role_policy.json +} + +resource "aws_iam_role_policy" "ecs_task_role_policy" { + name = "ecs-task-role-policy" + role = aws_iam_role.ecs_task_role.id + policy = data.aws_iam_policy_document.ecs_task_role_policy.json +} + +# ------------------------------ Github Actions ------------------------------ # + + +resource "aws_iam_role" "github_actions_deployment_role" { + name = "${local.app_env_name}-github-actions-deployment-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + { + Effect = "Allow", + Principal = { + Federated = aws_iam_openid_connect_provider.github.arn + }, + Action = "sts:AssumeRoleWithWebIdentity", + Condition = { + StringEquals = { + "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" + }, + StringLike = { + "token.actions.githubusercontent.com:sub" = [ + "repo:${var.remote_repo_name}:*" + ] + } + } + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "github_actions_admin_access" { + role = aws_iam_role.github_actions_deployment_role.id + policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess" +} +# END_FEATURE ecs diff --git a/terraform/modules/application/locals.tf b/terraform/modules/application/locals.tf new file mode 100644 index 0000000..3a50f48 --- /dev/null +++ b/terraform/modules/application/locals.tf @@ -0,0 +1,49 @@ +# START_FEATURE ecs +data "aws_caller_identity" "current" {} + +locals { + app_env_name = "${var.application_name}-${var.environment}" + + ecr_image_uri = format( + "%s.dkr.ecr.%s.amazonaws.com/%s:latest", + data.aws_caller_identity.current.account_id, + data.aws_region.current.name, + aws_ecr_repository.ecr.name, + ) + ecr_repository_arn = format( + "arn:aws:ecr:%s:%s:repository/%s", + data.aws_region.current.name, + data.aws_caller_identity.current.account_id, + aws_ecr_repository.ecr.name + ) + ecs_cluster_arn = format( + "arn:aws:ecs:%s:%s:repository/%s", + data.aws_region.current.name, + data.aws_caller_identity.current.account_id, + aws_ecs_cluster.cluster.name + ) + + ecs_infrastructure_secrets = [ + for setting in keys(jsondecode(nonsensitive( + aws_secretsmanager_secret_version.web_infrastructure.secret_string, + ))) : + { + name : setting + valueFrom : format("%s:%s::", aws_secretsmanager_secret.web_infrastructure.arn, setting) + } + ] + + ecs_config_secrets = [ + for setting in keys(jsondecode(nonsensitive( + data.aws_secretsmanager_secret_version.web_config.secret_string, + ))) : + { + name : setting + valueFrom : format("%s:%s::", data.aws_secretsmanager_secret.web_config.arn, setting) + } + ] + + ecs_secrets = concat(local.ecs_infrastructure_secrets, local.ecs_config_secrets) + ecs_worker_command = split(" ", "uv run celery -A config worker --beat --scheduler redbeat.RedBeatScheduler --loglevel=INFO -E") +} +# END_FEATURE ecs diff --git a/terraform/modules/application/outputs.tf b/terraform/modules/application/outputs.tf new file mode 100644 index 0000000..a3a6ed5 --- /dev/null +++ b/terraform/modules/application/outputs.tf @@ -0,0 +1,61 @@ +# START_FEATURE ecs +output "cluster_id" { + description = "The ID of the ECS cluster" + value = aws_ecs_cluster.cluster.id +} + +output "ecr_image_uri" { + description = "The full URI of the ECR image" + value = local.ecr_image_uri +} + +output "ecr_repository_name" { + description = "The name of the ECR repository" + value = aws_ecr_repository.ecr.name +} + +output "web_service_name" { + description = "The name of the ECS web service. This is also the container name." + value = aws_ecs_service.web.name +} + +output "web_network_configuration_security_groups" { + description = "The security groups used by the ECS web task" + value = aws_ecs_service.web.network_configuration[0].security_groups +} + +output "web_network_configuration_subnets" { + description = "The ID of one of the subnets used by the web task" + value = aws_ecs_service.web.network_configuration[0].subnets +} + +output "web_task_definition_arn" { + description = "The ARN of the ECS web service task definition" + value = aws_ecs_task_definition.web.arn +} + +output "web_log_group_name" { + description = "The name of the cloudwatch log group for the web service task" + value = aws_cloudwatch_log_group.web_log_group.name +} + +output "worker_service_name" { + description = "The name of the ECS worker service. This is also the container name." + value = aws_ecs_service.worker.name +} + +output "worker_task_desired_count" { + description = "The intended number of worker tasks" + value = aws_ecs_service.worker.desired_count +} + +output "worker_log_group_name" { + description = "The name of the cloudwatch log group for the worker service task" + value = aws_cloudwatch_log_group.worker_log_group.name +} + +output "s3_bucket_name" { + description = "The name of the S3 bucket" + value = aws_s3_bucket.bucket.bucket +} +# END_FEATURE ecs diff --git a/terraform/modules/application/rds.tf b/terraform/modules/application/rds.tf new file mode 100644 index 0000000..e7c4fb9 --- /dev/null +++ b/terraform/modules/application/rds.tf @@ -0,0 +1,36 @@ +# START_FEATURE ecs +resource "random_password" "db_password" { + length = 50 + special = false +} + +resource "aws_db_instance" "database" { + allocated_storage = 20 + allow_major_version_upgrade = true + apply_immediately = true + backup_retention_period = var.rds_backup_retention_period + db_name = format("%s_db", replace(var.application_name, "-", "_")) + deletion_protection = var.rds_deletion_protection + skip_final_snapshot = !var.rds_deletion_protection + engine = "postgres" + engine_version = var.rds_engine_version + identifier = "${local.app_env_name}-db" + instance_class = var.rds_instance_class + multi_az = var.rds_multi_az + password = random_password.db_password.result + storage_encrypted = true + storage_type = "gp2" + username = "dbuser" + publicly_accessible = false + vpc_security_group_ids = [aws_security_group.database.id] + db_subnet_group_name = aws_db_subnet_group.database.name +} + +resource "aws_db_subnet_group" "database" { + name = "${local.app_env_name}-database-subnets" + subnet_ids = [ + aws_subnet.public_subnet_a.id, + aws_subnet.public_subnet_b.id + ] +} +# END_FEATURE ecs diff --git a/terraform/modules/application/route53.tf b/terraform/modules/application/route53.tf new file mode 100644 index 0000000..c8b03e7 --- /dev/null +++ b/terraform/modules/application/route53.tf @@ -0,0 +1,47 @@ +# START_FEATURE ecs +data "aws_route53_zone" "domain" { + name = var.data_application_domain +} + +resource "aws_route53_record" "subdomain" { + zone_id = data.aws_route53_zone.domain.zone_id + name = var.application_url + type = "A" + + alias { + name = aws_lb.alb.dns_name + zone_id = aws_lb.alb.zone_id + evaluate_target_health = true + } +} + +resource "aws_acm_certificate" "cert" { + domain_name = var.application_url + validation_method = "DNS" + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_route53_record" "cert_validation" { + for_each = { + for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => { + name = dvo.resource_record_name + type = dvo.resource_record_type + value = dvo.resource_record_value + } + } + + zone_id = data.aws_route53_zone.domain.zone_id + name = each.value.name + type = each.value.type + ttl = 300 + records = [each.value.value] +} + +resource "aws_acm_certificate_validation" "cert_validation" { + certificate_arn = aws_acm_certificate.cert.arn + validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn] +} +# END_FEATURE ecs diff --git a/terraform/modules/application/s3.tf b/terraform/modules/application/s3.tf new file mode 100644 index 0000000..4fbaa1b --- /dev/null +++ b/terraform/modules/application/s3.tf @@ -0,0 +1,52 @@ +# START_FEATURE ecs +resource "aws_s3_bucket" "bucket" { + bucket = "${local.app_env_name}-${var.application_url}" + + tags = { + Environment = var.environment + } +} + + +resource "aws_s3_bucket_public_access_block" "bucket" { + bucket = aws_s3_bucket.bucket.id + + block_public_acls = false + block_public_policy = false + ignore_public_acls = false + restrict_public_buckets = false +} + + +resource "aws_s3_bucket_cors_configuration" "bucket" { + bucket = aws_s3_bucket.bucket.id + + cors_rule { + allowed_headers = ["*"] + allowed_methods = ["GET", "POST", "PUT"] + allowed_origins = ["*"] + expose_headers = ["ETag", "Location"] + } +} + + +resource "aws_s3_bucket_versioning" "bucket" { + bucket = aws_s3_bucket.bucket.id + + versioning_configuration { + status = "Enabled" + } +} + + +resource "aws_s3_bucket_server_side_encryption_configuration" "bucket" { + bucket = aws_s3_bucket.bucket.id + + rule { + bucket_key_enabled = false + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} +# END_FEATURE ecs diff --git a/terraform/modules/application/secrets_manager.tf b/terraform/modules/application/secrets_manager.tf new file mode 100644 index 0000000..579c9ba --- /dev/null +++ b/terraform/modules/application/secrets_manager.tf @@ -0,0 +1,37 @@ +# START_FEATURE ecs +data "aws_secretsmanager_secret" "web_config" { + name = "${local.app_env_name}-web-config" +} + +data "aws_secretsmanager_secret_version" "web_config" { + secret_id = data.aws_secretsmanager_secret.web_config.id +} + +resource "aws_secretsmanager_secret" "web_infrastructure" { + name = "${local.app_env_name}-web-infrastructure" +} + +resource "aws_secretsmanager_secret_version" "web_infrastructure" { + secret_id = aws_secretsmanager_secret.web_infrastructure.id + secret_string = jsonencode({ + ALLOWED_HOSTS = var.application_url + AWS_STORAGE_BUCKET_NAME = aws_s3_bucket.bucket.id + DATABASE_URL = format( + "postgres://dbuser:%s@%s:5432/%s?sslmode=require", + random_password.db_password.result, + aws_db_instance.database.address, + aws_db_instance.database.db_name + ) + REMOTE_REPO_NAME = var.remote_repo_name + SECRET_KEY = random_password.app_secret_key.result + CELERY_BROKER_URL = "redis://${aws_elasticache_replication_group.redis.primary_endpoint_address}:${aws_elasticache_replication_group.redis.port}" + # SES + DEFAULT_FROM_EMAIL = var.default_from_email + }) +} + +resource "random_password" "app_secret_key" { + length = 32 + special = false +} +# END_FEATURE ecs diff --git a/terraform/modules/application/security_groups.tf b/terraform/modules/application/security_groups.tf new file mode 100644 index 0000000..1b6bb1a --- /dev/null +++ b/terraform/modules/application/security_groups.tf @@ -0,0 +1,128 @@ +# START_FEATURE ecs +resource "aws_security_group" "web" { + name = "${local.app_env_name}-web" + vpc_id = aws_vpc.vpc.id + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + from_port = 8080 + to_port = 8080 + protocol = "tcp" + security_groups = [aws_security_group.load_balancer.id] + } + + tags = { + Name = "${var.application_name} ${var.environment} web" + } + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_security_group" "worker" { + name = "${local.app_env_name}-worker" + vpc_id = aws_vpc.vpc.id + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "${var.application_name} ${var.environment} worker" + } + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_security_group" "tasks" { + name = "${local.app_env_name}-tasks" + vpc_id = aws_vpc.vpc.id + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "${var.application_name} ${var.environment} tasks" + } + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_security_group" "database" { + name = "${local.app_env_name}-db" + vpc_id = aws_vpc.vpc.id + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + from_port = 5432 + to_port = 5432 + protocol = "tcp" + security_groups = [ + aws_security_group.web.id, + aws_security_group.worker.id, + ] + } + + tags = { + Name = "${var.application_name} ${var.environment} database" + } + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_security_group" "redis" { + name = "${local.app_env_name}-redis" + vpc_id = aws_vpc.vpc.id + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + from_port = 6379 + to_port = 6379 + protocol = "tcp" + security_groups = [ + aws_security_group.web.id, + aws_security_group.worker.id, + ] + } + + tags = { + Name = "${var.application_name} ${var.environment} redis" + } + + lifecycle { + create_before_destroy = true + } +} +# END_FEATURE ecs diff --git a/terraform/modules/application/ses.tf b/terraform/modules/application/ses.tf new file mode 100644 index 0000000..33d0f71 --- /dev/null +++ b/terraform/modules/application/ses.tf @@ -0,0 +1,29 @@ +# START_FEATURE ecs +data "aws_ses_domain_identity" "ses_domain_identity" { + domain = var.data_email_domain +} + +data "aws_iam_policy_document" "ses_domain_identity_policy" { + + statement { + actions = [ + "ses:*" + ] + resources = [ + data.aws_ses_domain_identity.ses_domain_identity.arn + ] + principals { + type = "AWS" + identifiers = [ + aws_iam_role.ecs_task_role.arn + ] + } + } +} + +resource "aws_ses_identity_policy" "allow_ecs" { + identity = data.aws_ses_domain_identity.ses_domain_identity.arn + policy = data.aws_iam_policy_document.ses_domain_identity_policy.json + name = "${local.app_env_name}-Allow-ECS" +} +# END_FEATURE ecs diff --git a/terraform/modules/application/variables.tf b/terraform/modules/application/variables.tf new file mode 100644 index 0000000..ce40e29 --- /dev/null +++ b/terraform/modules/application/variables.tf @@ -0,0 +1,116 @@ +# START_FEATURE ecs +# --------------------------------- REQUIRED --------------------------------- # + + +variable "environment" { + type = string +} + +variable "application_name" { + type = string +} + +variable "application_url" { + type = string +} + +variable "data_application_domain" { + type = string +} + +variable "data_email_domain" { + type = string +} + +variable "remote_repo_name" { + type = string +} + +# --------------------------------- OPTIONAL --------------------------------- # + + +variable "rds_backup_retention_period" { + type = number + default = 30 +} + +variable "rds_deletion_protection" { + type = bool + default = true +} + +variable "rds_instance_class" { + type = string + default = "db.t3.micro" +} + +variable "rds_multi_az" { + type = bool + default = false +} + +variable "rds_engine_version" { + type = string + default = "17" +} + +variable "redis_instance_type" { + type = string + default = "cache.t3.micro" +} + +variable "redis_engine_version" { + type = string + default = "7.1" +} + +variable "fargate_web_cpu" { + type = number + default = 1024 +} + +variable "fargate_web_memory" { + type = number + default = 2048 +} + +variable "fargate_worker_cpu" { + type = number + default = 1024 +} + +variable "fargate_worker_memory" { + type = number + default = 2048 +} + +variable "web_desired_count" { + type = number + default = 1 +} + +variable "worker_desired_count" { + type = number + default = 1 +} + +variable "ssl_policy" { + type = string + default = "ELBSecurityPolicy-TLS13-1-2-Res-FIPS-2023-04" +} + +variable "load_balancer_deletion_protection" { + type = bool + default = true +} + +variable "load_balancer_idle_timeout" { + type = number + default = 60 +} + +variable "default_from_email" { + type = string + default = "noreply@test.com" +} +# END_FEATURE ecs diff --git a/terraform/modules/application/vpc.tf b/terraform/modules/application/vpc.tf new file mode 100644 index 0000000..1ce1bab --- /dev/null +++ b/terraform/modules/application/vpc.tf @@ -0,0 +1,46 @@ +# START_FEATURE ecs +data "aws_region" "current" {} + +resource "aws_vpc" "vpc" { + cidr_block = "10.0.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true +} + +resource "aws_route_table" "route_table" { + vpc_id = aws_vpc.vpc.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.internet_gateway.id + } +} + +resource "aws_internet_gateway" "internet_gateway" { + vpc_id = aws_vpc.vpc.id +} + +resource "aws_route_table_association" "public_a" { + subnet_id = aws_subnet.public_subnet_a.id + route_table_id = aws_route_table.route_table.id +} + +resource "aws_route_table_association" "public_b" { + subnet_id = aws_subnet.public_subnet_b.id + route_table_id = aws_route_table.route_table.id +} + +resource "aws_subnet" "public_subnet_a" { + vpc_id = aws_vpc.vpc.id + cidr_block = "10.0.1.0/24" + availability_zone = "us-east-1a" + map_public_ip_on_launch = true +} + +resource "aws_subnet" "public_subnet_b" { + vpc_id = aws_vpc.vpc.id + cidr_block = "10.0.2.0/24" + availability_zone = "us-east-1b" + map_public_ip_on_launch = true +} +# END_FEATURE ecs diff --git a/terraform/modules/shared/route53.tf b/terraform/modules/shared/route53.tf new file mode 100644 index 0000000..0b2e778 --- /dev/null +++ b/terraform/modules/shared/route53.tf @@ -0,0 +1,14 @@ +# START_FEATURE ecs +data "aws_route53_zone" "domain" { + name = var.application_domain +} + +resource "aws_route53_record" "ses_verification" { + zone_id = data.aws_route53_zone.domain.zone_id + name = "_amazonses.${aws_ses_domain_identity.ses_domain_identity.domain}" + type = "TXT" + ttl = 600 + records = [aws_ses_domain_identity.ses_domain_identity.verification_token] +} + +# END_FEATURE ecs diff --git a/terraform/modules/shared/ses.tf b/terraform/modules/shared/ses.tf new file mode 100644 index 0000000..b73495f --- /dev/null +++ b/terraform/modules/shared/ses.tf @@ -0,0 +1,6 @@ +# START_FEATURE ecs +resource "aws_ses_domain_identity" "ses_domain_identity" { + domain = var.application_domain +} + +# END_FEATURE ecs diff --git a/terraform/modules/shared/variables.tf b/terraform/modules/shared/variables.tf new file mode 100644 index 0000000..710586f --- /dev/null +++ b/terraform/modules/shared/variables.tf @@ -0,0 +1,13 @@ +# START_FEATURE ecs +# --------------------------------- REQUIRED --------------------------------- # + + +variable "environment" { + type = string +} + +variable "application_domain" { + type = string +} + +# END_FEATURE ecs diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..57aa5f7 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,56 @@ +// START_FEATURE vue +import { defineConfig } from "vite" +import vue from "@vitejs/plugin-vue" +import { glob } from "glob" + +const SRC_LOCATION = "src/pages" +const DEST_LOCATION = "static/js/dist" + +export default defineConfig(({ mode }) => { + const DEVELOPMENT = mode === "development" + return { + plugins: [vue()], + base: "/" + DEST_LOCATION, + resolve: { + alias: { + vue: "vue/dist/vue.esm-bundler.js", + }, + }, + build: { + sourcemap: true, + emptyOutDir: DEVELOPMENT, + outDir: DEST_LOCATION, + minify: DEVELOPMENT ? false : "terser", + rollupOptions: { + input: glob.sync(`${SRC_LOCATION}/**/*.js`), + preserveEntrySignatures: true, + output: { + manualChunks(id) { + if (id.includes("pages") || id.includes("mixins")) { + // These files are imported by path and must be chunked individually + return id.split("src/")[1].split(".js")[0] + } + // otherwise, add to combined chunk + return "vue/main" + }, + entryFileNames(chunkInfo) { + if (chunkInfo.isEntry) { + const name = chunkInfo.facadeModuleId.split("src/")[1] + if (!name.includes("pages")) { + // This is not a 'real' entrypoint, should contain hash for cache busting + const path = name.split(".js")[0] + return `${path}-[hash].js` + } + // This is a real entry point, should not contain hash + return name + } + return `[name]-[hash].js` + }, + chunkFileNames: `[name]-[hash].js`, + assetFileNames: `[name].[ext]`, + }, + }, + }, + } +}) +// END_FEATURE vue