diff --git a/.gitignore b/.gitignore index 87d6ac1..c7f699a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,294 @@ -/.venv +# Created by https://www.toptal.com/developers/gitignore/api/django,pycharm +# Edit at https://www.toptal.com/developers/gitignore?templates=django,pycharm + +### Django ### +*.log +*.pot +*.pyc +__pycache__/ +local_settings.py +db.sqlite3 +db.sqlite3-journal +media + +# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ +# in your Git repository. Update and uncomment the following line accordingly. +# /staticfiles/ + +### Django.Python Stack ### +# Byte-compiled / optimized / DLL files +*.py[cod] +*$py.class + +### Redis ### +redis + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo + +# Django stuff: + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments .env -db.sqlite3 \ No newline at end of file +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### PyCharm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +.idea/ + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf +.idea/**/dataSources.xml +.idea/**/vcs.xml + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PyCharm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +# End of https://www.toptal.com/developers/gitignore/api/django,pycharm \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/ChatBox-API.iml b/.idea/ChatBox-API.iml deleted file mode 100644 index 2a615ca..0000000 --- a/.idea/ChatBox-API.iml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 9a8e77c..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 105ce2d..0000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 6613a74..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index df9d391..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a1abed8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +repos: +- repo: https://github.com/psf/black + rev: 22.12.0 + hooks: + - id: black + language_version: python3.11 + exclude: /apps/migrations|\.pyc diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f7a156d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM redis:latest +COPY redis/redis.conf /usr/local/etc/redis/redis.conf +CMD [ "redis-server", "/usr/local/etc/redis/redis.conf" ] +EXPOSE 6379 \ No newline at end of file diff --git a/apps/__pycache__/__init__.cpython-311.pyc b/apps/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index f72630f..0000000 Binary files a/apps/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/apps/__pycache__/__init__.cpython-38.pyc b/apps/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 3f13f2d..0000000 Binary files a/apps/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/apps/chat/__pycache__/__init__.cpython-311.pyc b/apps/chat/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 48912c9..0000000 Binary files a/apps/chat/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/apps/chat/__pycache__/__init__.cpython-38.pyc b/apps/chat/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 6a0370c..0000000 Binary files a/apps/chat/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/apps/chat/__pycache__/admin.cpython-311.pyc b/apps/chat/__pycache__/admin.cpython-311.pyc deleted file mode 100644 index a013212..0000000 Binary files a/apps/chat/__pycache__/admin.cpython-311.pyc and /dev/null differ diff --git a/apps/chat/__pycache__/admin.cpython-38.pyc b/apps/chat/__pycache__/admin.cpython-38.pyc deleted file mode 100644 index e7d7bee..0000000 Binary files a/apps/chat/__pycache__/admin.cpython-38.pyc and /dev/null differ diff --git a/apps/chat/__pycache__/apps.cpython-311.pyc b/apps/chat/__pycache__/apps.cpython-311.pyc deleted file mode 100644 index 381aff8..0000000 Binary files a/apps/chat/__pycache__/apps.cpython-311.pyc and /dev/null differ diff --git a/apps/chat/__pycache__/apps.cpython-38.pyc b/apps/chat/__pycache__/apps.cpython-38.pyc deleted file mode 100644 index 6139a48..0000000 Binary files a/apps/chat/__pycache__/apps.cpython-38.pyc and /dev/null differ diff --git a/apps/chat/__pycache__/models.cpython-311.pyc b/apps/chat/__pycache__/models.cpython-311.pyc deleted file mode 100644 index 19ce9e2..0000000 Binary files a/apps/chat/__pycache__/models.cpython-311.pyc and /dev/null differ diff --git a/apps/chat/__pycache__/models.cpython-38.pyc b/apps/chat/__pycache__/models.cpython-38.pyc deleted file mode 100644 index d20569e..0000000 Binary files a/apps/chat/__pycache__/models.cpython-38.pyc and /dev/null differ diff --git a/apps/chat/apps.py b/apps/chat/apps.py index 8bb7c3d..6886ac8 100644 --- a/apps/chat/apps.py +++ b/apps/chat/apps.py @@ -4,3 +4,4 @@ class ChatConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "apps.chat" + label = "chat" diff --git a/apps/chat/consumers.py b/apps/chat/consumers.py new file mode 100644 index 0000000..ae52331 --- /dev/null +++ b/apps/chat/consumers.py @@ -0,0 +1,132 @@ +from urllib.parse import unquote, quote +from enum import Enum + +import redis +from channels.db import database_sync_to_async +from channels.exceptions import DenyConnection + +from channels.generic.websocket import AsyncJsonWebsocketConsumer +from django.contrib.auth.models import AnonymousUser +from django.http import Http404 +from django.shortcuts import get_object_or_404 +from django.utils import timezone + +from apps.chat.models import Chatroom +from apps.chat.serializers import ChatMessageSerializer +from apps.chat.services import ChatroomService +from apps.user.models import User + + +class UserType(Enum): + GUEST = 0 + USER = 1 + + +class ChatConsumer(AsyncJsonWebsocketConsumer): + async def connect(self): + self.user = self.scope["user"] + # Check Origin + user = await self.get_user_by_origin_header() + self.origin = user.service_domain + # Check user + if isinstance(self.user, AnonymousUser): + # Guest + self.user_type = UserType.GUEST + # Check guest name + query_string = self.scope["query_string"].decode("utf-8") + if "name=" not in query_string: + raise DenyConnection("Query string for 'name' missing") + + self.user = unquote(query_string.split("=")[1]) + self.host = user + + print(f"Anonymous guest <{self.user}> joined the chat room") + else: + # Registered User + self.user_type = UserType.USER + self.user = self.scope["user"] + + print(f"Registered user <{self.user.email}> joined the chat room") + + self.room_name = self.scope["url_route"]["kwargs"]["room_name"] + self.room_group_name = "chat_%s" % self.room_name + self.conn = redis.StrictRedis(host="localhost", port=6379, db=0) + + try: + # Join room group + await self.channel_layer.group_add(self.room_group_name, self.channel_name) + await self.accept() + + # latest messages, max 50 + past_messages = ChatroomService.get_past_messages( + self.room_group_name, self.conn + ) + + for m in past_messages: + await self.channel_layer.group_send(self.room_group_name, m) + + except Exception as e: + print(e) + raise DenyConnection(e) + + async def disconnect(self, close_code): + await self.save_latest_message() + try: + # Leave room group + await self.channel_layer.group_discard( + self.room_group_name, self.channel_name + ) + except Exception as e: + print("Failed to leave group") + + # Receive message from WebSocket + async def receive_json(self, content, **kwargs): + if content["type"] == "request": + past_messages = ChatroomService.get_past_messages( + self.room_group_name, self.conn, int(content["timestamp"] / 1000) + ) + for m in past_messages: + await self.channel_layer.group_send(self.room_group_name, m) + elif content["type"] == "chat_message": + content["timestamp"] = int(content["timestamp"] / 1000) + saved_message = ChatroomService.save_msg_in_mem( + content, self.room_group_name, self.conn + ) + + # Send message to room group + await self.channel_layer.group_send(self.room_group_name, saved_message) + await self.save_message_db(saved_message) + + # Receive message from room group + async def chat_message(self, event): + # Send message to WebSocket + await self.send_json(event) + + @database_sync_to_async + def get_user_by_origin_header(self): + origin = None + for header_tuple in self.scope["headers"]: + if bytes("origin", "utf-8") in header_tuple: + origin = header_tuple[1].decode("utf-8").split("//")[1] + + if not origin: + raise DenyConnection("Origin header missing") + + try: + registered_user = get_object_or_404(User, service_domain=origin) + except Http404: + raise DenyConnection("Request origin not registered") + return registered_user + + @database_sync_to_async + def save_latest_message(self) -> None: + room_name = self.room_group_name.split("_")[1] + latest_message = ChatroomService.get_latest_message( + self.room_group_name, self.conn + ) + + ChatroomService.save_message(room_name, latest_message) + + @database_sync_to_async + def save_message_db(self, msg_obj: dict) -> None: + ChatroomService.save_message(self.room_group_name.split("_")[1], msg_obj) diff --git a/apps/chat/migrations/0001_initial.py b/apps/chat/migrations/0001_initial.py new file mode 100644 index 0000000..1452ca6 --- /dev/null +++ b/apps/chat/migrations/0001_initial.py @@ -0,0 +1,41 @@ +# Generated by Django 4.1.5 on 2023-01-04 09:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="ChatMessage", + fields=[ + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ("message", models.CharField(max_length=2000)), + ("is_host", models.BooleanField()), + ], + options={ + "db_table": "chat_message", + }, + ), + migrations.CreateModel( + name="Chatroom", + fields=[ + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("id", models.BigAutoField(primary_key=True, serialize=False)), + ( + "visitor", + models.CharField(blank=True, default="Guest", max_length=20), + ), + ], + options={ + "db_table": "chatroom", + }, + ), + ] diff --git a/apps/chat/migrations/0002_initial.py b/apps/chat/migrations/0002_initial.py new file mode 100644 index 0000000..81c176b --- /dev/null +++ b/apps/chat/migrations/0002_initial.py @@ -0,0 +1,41 @@ +# Generated by Django 4.1.5 on 2023-01-04 09:02 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("chat", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="chatroom", + name="host", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), + migrations.AddField( + model_name="chatroom", + name="latest_msg", + field=models.ForeignKey( + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="latest_msg", + to="chat.chatmessage", + ), + ), + migrations.AddField( + model_name="chatmessage", + name="chatroom", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="chat.chatroom" + ), + ), + ] diff --git a/apps/chat/migrations/0003_remove_chatroom_latest_msg_remove_chatroom_visitor_and_more.py b/apps/chat/migrations/0003_remove_chatroom_latest_msg_remove_chatroom_visitor_and_more.py new file mode 100644 index 0000000..19888c9 --- /dev/null +++ b/apps/chat/migrations/0003_remove_chatroom_latest_msg_remove_chatroom_visitor_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.1.5 on 2023-01-05 21:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("chat", "0002_initial"), + ] + + operations = [ + migrations.RemoveField( + model_name="chatroom", + name="latest_msg", + ), + migrations.RemoveField( + model_name="chatroom", + name="visitor", + ), + migrations.AddField( + model_name="chatroom", + name="guest", + field=models.CharField(default="guest123", max_length=20), + preserve_default=False, + ), + migrations.AddField( + model_name="chatroom", + name="name", + field=models.CharField(default="playidea:guest123123", max_length=50), + preserve_default=False, + ), + ] diff --git a/apps/chat/migrations/0004_chatroom_latest_msg_alter_chatroom_name.py b/apps/chat/migrations/0004_chatroom_latest_msg_alter_chatroom_name.py new file mode 100644 index 0000000..b495f25 --- /dev/null +++ b/apps/chat/migrations/0004_chatroom_latest_msg_alter_chatroom_name.py @@ -0,0 +1,29 @@ +# Generated by Django 4.1.5 on 2023-01-16 08:41 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("chat", "0003_remove_chatroom_latest_msg_remove_chatroom_visitor_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="chatroom", + name="latest_msg", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="latest_msg", + to="chat.chatmessage", + ), + ), + migrations.AlterField( + model_name="chatroom", + name="name", + field=models.CharField(max_length=22), + ), + ] diff --git a/apps/chat/migrations/__pycache__/__init__.cpython-311.pyc b/apps/chat/migrations/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 8f9d4dd..0000000 Binary files a/apps/chat/migrations/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/apps/chat/migrations/__pycache__/__init__.cpython-38.pyc b/apps/chat/migrations/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index bf0de0e..0000000 Binary files a/apps/chat/migrations/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/apps/chat/models.py b/apps/chat/models.py index 71a8362..c93af8c 100644 --- a/apps/chat/models.py +++ b/apps/chat/models.py @@ -1,3 +1,38 @@ from django.db import models -# Create your models here. +from apps.user.models import User, TimeStampMixin + + +class Chatroom(TimeStampMixin): + id = models.BigAutoField(primary_key=True) + host = models.ForeignKey(User, on_delete=models.CASCADE) + guest = models.CharField(max_length=20, null=False, blank=False) + name = models.CharField(max_length=22, null=False, blank=False) + latest_msg = models.ForeignKey( + "ChatMessage", on_delete=models.DO_NOTHING, related_name="latest_msg", null=True + ) + + class Meta: + db_table = "chatroom" + + def __str__(self): + return f"[{self.id}] {self.host}-{self.guest}" + + def __repr__(self): + return f"Chatroom({self.id}, {self.host}, {self.guest})" + + +class ChatMessage(TimeStampMixin): + id = models.BigAutoField(primary_key=True) + chatroom = models.ForeignKey(Chatroom, on_delete=models.CASCADE) + message = models.CharField(max_length=2000, null=False) + is_host = models.BooleanField(null=False, blank=False) + + class Meta: + db_table = "chat_message" + + def __str__(self): + return f"[{self.id}] chatroom: {self.chatroom_id}" + + def __repr__(self): + return f"ChatMessage({self.id}, {self.chatroom_id})" diff --git a/apps/chat/routing.py b/apps/chat/routing.py new file mode 100644 index 0000000..a2a0f3b --- /dev/null +++ b/apps/chat/routing.py @@ -0,0 +1,6 @@ +from django.urls import re_path +from . import consumers + +websocket_urlpatterns = [ + re_path(r"ws/chat/(?P[-\w]+)/$", consumers.ChatConsumer.as_asgi()), +] diff --git a/apps/chat/serializers.py b/apps/chat/serializers.py new file mode 100644 index 0000000..84f3568 --- /dev/null +++ b/apps/chat/serializers.py @@ -0,0 +1,36 @@ +from rest_framework import serializers + +from apps.chat.models import Chatroom, ChatMessage +from apps.user.serializers import UserSerializer + + +class ChatMessageSerializer(serializers.ModelSerializer): + class Meta: + model = ChatMessage + fields = ["id", "chatroom", "message", "is_host", "created_at", "updated_at"] + read_only_fields = [ + "id", + "chatroom", + "created_at", + "updated_at", + ] + + +class ChatroomSerializer(serializers.ModelSerializer): + host = UserSerializer(read_only=True) + # last_message = ChatMessageSerializer(read_only=True) + + class Meta: + model = Chatroom + fields = ["id", "host", "guest", "name", "created_at", "updated_at"] + read_only_fields = ["id", "host", "name", "created_at", "updated_at"] + + +class ChatMessageInMemorySerializer(serializers.Serializer): + type = serializers.ChoiceField(choices=["chat_message", "notice"]) + message = serializers.CharField(max_length=1000, min_length=1) + is_host = serializers.BooleanField() + timestamp = serializers.IntegerField() + + class Meta: + fields = ["type", "message", "is_host", "timestamp"] diff --git a/apps/chat/services.py b/apps/chat/services.py new file mode 100644 index 0000000..b05369a --- /dev/null +++ b/apps/chat/services.py @@ -0,0 +1,105 @@ +import json +import uuid +from datetime import datetime +from typing import Union, List + +import redis +import shortuuid +from django.http import Http404 +from django.shortcuts import get_object_or_404 +from django.utils import timezone +from rest_framework.request import Request + +from apps.chat.models import Chatroom, ChatMessage +from apps.chat.serializers import ChatMessageInMemorySerializer, ChatMessageSerializer +from config.exceptions import InstanceNotFound + + +class ChatroomService(object): + def __init__( + self, + request: Union[Request, None] = None, + chatroom: Union[Chatroom, None] = None, + ): + self.request = request + self.chatroom = chatroom + + @staticmethod + def generate_chatroom_uuid() -> str: + u = uuid.uuid1() + s = shortuuid.encode(u) + return s + + @staticmethod + def save_msg_in_mem(msg_obj: dict, group_name: str, redis_conn) -> ChatMessage: + serializer = ChatMessageInMemorySerializer(data=msg_obj) + if serializer.is_valid(raise_exception=True): + data = serializer.data + json_msg = json.dumps(data, ensure_ascii=False).encode("utf-8") + redis_conn.zadd( + group_name, + { + json_msg: datetime.fromtimestamp(data["timestamp"]).strftime( + "%Y%m%d%H%M%S" + ) + }, + ) + return serializer.validated_data + + @staticmethod + def get_past_messages( + group_name: str, redis_conn, starting_point: Union[int, None] = None + ) -> List[dict]: + if starting_point is None: + from_time = datetime.now().strftime("%Y%m%d%H%M%S") + else: + from_time = datetime.fromtimestamp(starting_point - 1) + + a_week = str(int(from_time) - 7000000) + messages = redis_conn.zrangebyscore(group_name, a_week, from_time, 0, 50) + + decoded_messages: List[dict] = [] + + for m in messages: + json_dict = m.decode("utf-8") + decoded_messages.append(dict(json.loads(json_dict))) + return decoded_messages + + @staticmethod + def get_latest_message(group_name: str, redis_conn) -> dict: + latest_message = redis_conn.zrevrangebyscore( + group_name, datetime.now().strftime("%Y%m%d%H%M%S"), "-9999999999", 0, 1 + ) + json_dict = latest_message[0].decode("utf-8") + return dict(json.loads(json_dict)) + + @staticmethod + def delete_chatroom_mem(room_name: str, redis_conn=None) -> None: + if redis_conn is None: + rd = redis.StrictRedis(host="localhost", port=6379, db=0) + else: + rd = redis_conn + + rd.delete("chat_" + room_name) + + @staticmethod + def save_message(room_name: str, msg_obj: dict) -> Chatroom: + try: + chatroom = get_object_or_404(Chatroom, name=room_name) + except Http404: + raise InstanceNotFound("chatroom with the provided name does not exist") + + data = { + "message": msg_obj["message"], + "is_host": msg_obj["is_host"], + } + + serializer = ChatMessageSerializer(data=data) + if serializer.is_valid(raise_exception=True): + serializer.save(chatroom_id=chatroom.id) + + chatroom.latest_msg_id = serializer.data.get("id") + chatroom.updated_at = timezone.now() + chatroom.save(update_fields=["latest_msg", "updated_at"]) + + return chatroom diff --git a/apps/chat/urls.py b/apps/chat/urls.py new file mode 100644 index 0000000..48f53e3 --- /dev/null +++ b/apps/chat/urls.py @@ -0,0 +1,24 @@ +from django.urls import path +from .views import ( + ChatroomListView, + ChatroomDestroyView, + ChatroomExportView, + ChatroomCreateView, +) + +urlpatterns = [ + path("client/", ChatroomCreateView.as_view(), name="chatroom-list"), + path( + "/chatrooms/", ChatroomListView.as_view(), name="chatroom-list" + ), + path( + "chatrooms//", + ChatroomDestroyView.as_view(), + name="leave-chatroom", + ), + path( + "chatrooms//export", + ChatroomExportView.as_view(), + name="export-chatroom-messages", + ), +] diff --git a/apps/chat/views.py b/apps/chat/views.py index 91ea44a..521948a 100644 --- a/apps/chat/views.py +++ b/apps/chat/views.py @@ -1,3 +1,135 @@ -from django.shortcuts import render +from django.http import Http404 +from django.shortcuts import render, get_object_or_404 +from django.utils.decorators import method_decorator +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema +from rest_framework import generics, status +from rest_framework.exceptions import ValidationError, NotAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView -# Create your views here. +from apps.chat.models import Chatroom +from apps.chat.serializers import ChatroomSerializer +from apps.chat.services import ChatroomService +from apps.user.models import User +from config.exceptions import InstanceNotFound + + +@method_decorator( + name="get", + decorator=swagger_auto_schema( + operation_summary="Get user's chatroom list", + responses={ + 200: openapi.Response("Success", ChatroomSerializer), + 404: "Not found", + }, + ), +) +class ChatroomListView(generics.ListAPIView): + serializer_class = ChatroomSerializer + queryset = Chatroom.objects.all() + + def get_queryset(self): + try: + user = get_object_or_404(User, access_key=self.kwargs.get("access_key")) + except Http404: + raise InstanceNotFound("User with the provided id does not exist") + + queryset = self.queryset.filter(host__access_key=user.access_key).order_by( + "-latest_msg__created_at" + ) + return queryset + + +class ChatroomCreateView(generics.CreateAPIView): + serializer_class = ChatroomSerializer + + access_key_param = openapi.Parameter( + "X-ChatBox-Access-Key", + openapi.IN_HEADER, + description="service access key", + type=openapi.TYPE_STRING, + ) + secret_key_param = openapi.Parameter( + "X-ChatBox-Secret-Key", + openapi.IN_HEADER, + description="service secret key", + type=openapi.TYPE_STRING, + ) + + @swagger_auto_schema( + tags=["client"], + operation_summary="Open chatroom (client side)", + operation_description="Make a new chatroom with guest's name", + manual_parameters=[access_key_param, secret_key_param], + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + required=["serviceName", "guest"], + properties={ + "serviceName": openapi.Schema( + type=openapi.TYPE_STRING, description="등록한 서비스의 정확한 이름" + ), + "guest": openapi.Schema( + type=openapi.TYPE_STRING, description="게스트가 입력한 채팅명" + ), + }, + ), + responses={ + 201: openapi.Response("Success", ChatroomSerializer), + 401: "User not registered", + }, + ) + def post(self, request, *args, **kwargs): + if ( + not request.headers["X-ChatBox-Access-Key"] + and request.headers["X-ChatBox-Secret-Key"] + ): + raise ValidationError( + "'X-ChatBox-Access-Key' and 'X-ChatBox-Secret-Key' header must be present" + ) + access_key = request.headers["X-ChatBox-Access-Key"] + secret_key = request.headers["X-ChatBox-Secret-Key"] + try: + host_user = get_object_or_404( + User, + access_key=access_key, + secret_key=secret_key, + service_name=request.data.get("service_name"), + ) + except Http404: + raise NotAuthenticated("User not registered") + + serializer = self.get_serializer(data={"guest": request.data.get("guest")}) + if serializer.is_valid(raise_exception=True): + # django channels group name only accepts ASCII alphanumeric, hyphens, underscores, or periods + # max length 100 + serializer.save( + host_id=host_user.id, + name=ChatroomService.generate_chatroom_uuid(), # length 22 + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + +class ChatroomDestroyView(generics.DestroyAPIView): + serializer_class = ChatroomSerializer + queryset = Chatroom.objects.all() + + def get_object(self): + try: + chatroom = get_object_or_404( + Chatroom, host_id=self.kwargs.get("pk"), guest=self.kwargs.get("guest") + ) + except Http404: + raise InstanceNotFound( + "Chatroom with the provided id and guest name does not exist" + ) + + return chatroom + + def perform_destroy(self, instance): + ChatroomService.delete_chatroom_mem(instance.name) + instance.delete() + + +class ChatroomExportView(APIView): + pass diff --git a/apps/theme/__pycache__/__init__.cpython-311.pyc b/apps/theme/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index abd6cc0..0000000 Binary files a/apps/theme/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/apps/theme/__pycache__/apps.cpython-311.pyc b/apps/theme/__pycache__/apps.cpython-311.pyc deleted file mode 100644 index 0b7908f..0000000 Binary files a/apps/theme/__pycache__/apps.cpython-311.pyc and /dev/null differ diff --git a/apps/theme/apps.py b/apps/theme/apps.py deleted file mode 100644 index 1c6d27b..0000000 --- a/apps/theme/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class ThemeConfig(AppConfig): - name = 'apps.theme' - label = "theme" diff --git a/apps/theme/static/css/dist/styles.css b/apps/theme/static/css/dist/styles.css deleted file mode 100644 index ca7df02..0000000 --- a/apps/theme/static/css/dist/styles.css +++ /dev/null @@ -1,769 +0,0 @@ -/* -! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com -*/ - -/* -1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) -2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) -*/ - -*, -::before, -::after { - box-sizing: border-box; - /* 1 */ - border-width: 0; - /* 2 */ - border-style: solid; - /* 2 */ - border-color: #e5e7eb; - /* 2 */ -} - -::before, -::after { - --tw-content: ''; -} - -/* -1. Use a consistent sensible line-height in all browsers. -2. Prevent adjustments of font size after orientation changes in iOS. -3. Use a more readable tab size. -4. Use the user's configured `sans` font-family by default. -5. Use the user's configured `sans` font-feature-settings by default. -*/ - -html { - line-height: 1.5; - /* 1 */ - -webkit-text-size-adjust: 100%; - /* 2 */ - -moz-tab-size: 4; - /* 3 */ - -o-tab-size: 4; - tab-size: 4; - /* 3 */ - font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; - /* 4 */ - font-feature-settings: normal; - /* 5 */ -} - -/* -1. Remove the margin in all browsers. -2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. -*/ - -body { - margin: 0; - /* 1 */ - line-height: inherit; - /* 2 */ -} - -/* -1. Add the correct height in Firefox. -2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) -3. Ensure horizontal rules are visible by default. -*/ - -hr { - height: 0; - /* 1 */ - color: inherit; - /* 2 */ - border-top-width: 1px; - /* 3 */ -} - -/* -Add the correct text decoration in Chrome, Edge, and Safari. -*/ - -abbr:where([title]) { - -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; -} - -/* -Remove the default font size and weight for headings. -*/ - -h1, -h2, -h3, -h4, -h5, -h6 { - font-size: inherit; - font-weight: inherit; -} - -/* -Reset links to optimize for opt-in styling instead of opt-out. -*/ - -a { - color: inherit; - text-decoration: inherit; -} - -/* -Add the correct font weight in Edge and Safari. -*/ - -b, -strong { - font-weight: bolder; -} - -/* -1. Use the user's configured `mono` font family by default. -2. Correct the odd `em` font sizing in all browsers. -*/ - -code, -kbd, -samp, -pre { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - /* 1 */ - font-size: 1em; - /* 2 */ -} - -/* -Add the correct font size in all browsers. -*/ - -small { - font-size: 80%; -} - -/* -Prevent `sub` and `sup` elements from affecting the line height in all browsers. -*/ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -/* -1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) -2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) -3. Remove gaps between table borders by default. -*/ - -table { - text-indent: 0; - /* 1 */ - border-color: inherit; - /* 2 */ - border-collapse: collapse; - /* 3 */ -} - -/* -1. Change the font styles in all browsers. -2. Remove the margin in Firefox and Safari. -3. Remove default padding in all browsers. -*/ - -button, -input, -optgroup, -select, -textarea { - font-family: inherit; - /* 1 */ - font-size: 100%; - /* 1 */ - font-weight: inherit; - /* 1 */ - line-height: inherit; - /* 1 */ - color: inherit; - /* 1 */ - margin: 0; - /* 2 */ - padding: 0; - /* 3 */ -} - -/* -Remove the inheritance of text transform in Edge and Firefox. -*/ - -button, -select { - text-transform: none; -} - -/* -1. Correct the inability to style clickable types in iOS and Safari. -2. Remove default button styles. -*/ - -button, -[type='button'], -[type='reset'], -[type='submit'] { - -webkit-appearance: button; - /* 1 */ - background-color: transparent; - /* 2 */ - background-image: none; - /* 2 */ -} - -/* -Use the modern Firefox focus style for all focusable elements. -*/ - -:-moz-focusring { - outline: auto; -} - -/* -Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) -*/ - -:-moz-ui-invalid { - box-shadow: none; -} - -/* -Add the correct vertical alignment in Chrome and Firefox. -*/ - -progress { - vertical-align: baseline; -} - -/* -Correct the cursor style of increment and decrement buttons in Safari. -*/ - -::-webkit-inner-spin-button, -::-webkit-outer-spin-button { - height: auto; -} - -/* -1. Correct the odd appearance in Chrome and Safari. -2. Correct the outline style in Safari. -*/ - -[type='search'] { - -webkit-appearance: textfield; - /* 1 */ - outline-offset: -2px; - /* 2 */ -} - -/* -Remove the inner padding in Chrome and Safari on macOS. -*/ - -::-webkit-search-decoration { - -webkit-appearance: none; -} - -/* -1. Correct the inability to style clickable types in iOS and Safari. -2. Change font properties to `inherit` in Safari. -*/ - -::-webkit-file-upload-button { - -webkit-appearance: button; - /* 1 */ - font: inherit; - /* 2 */ -} - -/* -Add the correct display in Chrome and Safari. -*/ - -summary { - display: list-item; -} - -/* -Removes the default spacing and border for appropriate elements. -*/ - -blockquote, -dl, -dd, -h1, -h2, -h3, -h4, -h5, -h6, -hr, -figure, -p, -pre { - margin: 0; -} - -fieldset { - margin: 0; - padding: 0; -} - -legend { - padding: 0; -} - -ol, -ul, -menu { - list-style: none; - margin: 0; - padding: 0; -} - -/* -Prevent resizing textareas horizontally by default. -*/ - -textarea { - resize: vertical; -} - -/* -1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) -2. Set the default placeholder color to the user's configured gray 400 color. -*/ - -input::-moz-placeholder, textarea::-moz-placeholder { - opacity: 1; - /* 1 */ - color: #9ca3af; - /* 2 */ -} - -input::placeholder, -textarea::placeholder { - opacity: 1; - /* 1 */ - color: #9ca3af; - /* 2 */ -} - -/* -Set the default cursor for buttons. -*/ - -button, -[role="button"] { - cursor: pointer; -} - -/* -Make sure disabled buttons don't get the pointer cursor. -*/ - -:disabled { - cursor: default; -} - -/* -1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) -2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) - This can trigger a poorly considered lint error in some tools but is included by design. -*/ - -img, -svg, -video, -canvas, -audio, -iframe, -embed, -object { - display: block; - /* 1 */ - vertical-align: middle; - /* 2 */ -} - -/* -Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) -*/ - -img, -video { - max-width: 100%; - height: auto; -} - -/* Make elements with the HTML hidden attribute stay hidden by default */ - -[hidden] { - display: none; -} - -[type='text'],[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - background-color: #fff; - border-color: #6b7280; - border-width: 1px; - border-radius: 0px; - padding-top: 0.5rem; - padding-right: 0.75rem; - padding-bottom: 0.5rem; - padding-left: 0.75rem; - font-size: 1rem; - line-height: 1.5rem; - --tw-shadow: 0 0 #0000; -} - -[type='text']:focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus { - outline: 2px solid transparent; - outline-offset: 2px; - --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-color: #2563eb; - --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); - --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); - box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - border-color: #2563eb; -} - -input::-moz-placeholder, textarea::-moz-placeholder { - color: #6b7280; - opacity: 1; -} - -input::placeholder,textarea::placeholder { - color: #6b7280; - opacity: 1; -} - -::-webkit-datetime-edit-fields-wrapper { - padding: 0; -} - -::-webkit-date-and-time-value { - min-height: 1.5em; -} - -::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field { - padding-top: 0; - padding-bottom: 0; -} - -select { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); - background-position: right 0.5rem center; - background-repeat: no-repeat; - background-size: 1.5em 1.5em; - padding-right: 2.5rem; - -webkit-print-color-adjust: exact; - print-color-adjust: exact; -} - -[multiple] { - background-image: initial; - background-position: initial; - background-repeat: unset; - background-size: initial; - padding-right: 0.75rem; - -webkit-print-color-adjust: unset; - print-color-adjust: unset; -} - -[type='checkbox'],[type='radio'] { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - padding: 0; - -webkit-print-color-adjust: exact; - print-color-adjust: exact; - display: inline-block; - vertical-align: middle; - background-origin: border-box; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - flex-shrink: 0; - height: 1rem; - width: 1rem; - color: #2563eb; - background-color: #fff; - border-color: #6b7280; - border-width: 1px; - --tw-shadow: 0 0 #0000; -} - -[type='checkbox'] { - border-radius: 0px; -} - -[type='radio'] { - border-radius: 100%; -} - -[type='checkbox']:focus,[type='radio']:focus { - outline: 2px solid transparent; - outline-offset: 2px; - --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); - --tw-ring-offset-width: 2px; - --tw-ring-offset-color: #fff; - --tw-ring-color: #2563eb; - --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); - --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); - box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); -} - -[type='checkbox']:checked,[type='radio']:checked { - border-color: transparent; - background-color: currentColor; - background-size: 100% 100%; - background-position: center; - background-repeat: no-repeat; -} - -[type='checkbox']:checked { - background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); -} - -[type='radio']:checked { - background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); -} - -[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus { - border-color: transparent; - background-color: currentColor; -} - -[type='checkbox']:indeterminate { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e"); - border-color: transparent; - background-color: currentColor; - background-size: 100% 100%; - background-position: center; - background-repeat: no-repeat; -} - -[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus { - border-color: transparent; - background-color: currentColor; -} - -[type='file'] { - background: unset; - border-color: inherit; - border-width: 0; - border-radius: 0; - padding: 0; - font-size: unset; - line-height: inherit; -} - -[type='file']:focus { - outline: 1px solid ButtonText; - outline: 1px auto -webkit-focus-ring-color; -} - -*, ::before, ::after { - --tw-border-spacing-x: 0; - --tw-border-spacing-y: 0; - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-rotate: 0; - --tw-skew-x: 0; - --tw-skew-y: 0; - --tw-scale-x: 1; - --tw-scale-y: 1; - --tw-pan-x: ; - --tw-pan-y: ; - --tw-pinch-zoom: ; - --tw-scroll-snap-strictness: proximity; - --tw-ordinal: ; - --tw-slashed-zero: ; - --tw-numeric-figure: ; - --tw-numeric-spacing: ; - --tw-numeric-fraction: ; - --tw-ring-inset: ; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-color: rgb(59 130 246 / 0.5); - --tw-ring-offset-shadow: 0 0 #0000; - --tw-ring-shadow: 0 0 #0000; - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - --tw-blur: ; - --tw-brightness: ; - --tw-contrast: ; - --tw-grayscale: ; - --tw-hue-rotate: ; - --tw-invert: ; - --tw-saturate: ; - --tw-sepia: ; - --tw-drop-shadow: ; - --tw-backdrop-blur: ; - --tw-backdrop-brightness: ; - --tw-backdrop-contrast: ; - --tw-backdrop-grayscale: ; - --tw-backdrop-hue-rotate: ; - --tw-backdrop-invert: ; - --tw-backdrop-opacity: ; - --tw-backdrop-saturate: ; - --tw-backdrop-sepia: ; -} - -::backdrop { - --tw-border-spacing-x: 0; - --tw-border-spacing-y: 0; - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-rotate: 0; - --tw-skew-x: 0; - --tw-skew-y: 0; - --tw-scale-x: 1; - --tw-scale-y: 1; - --tw-pan-x: ; - --tw-pan-y: ; - --tw-pinch-zoom: ; - --tw-scroll-snap-strictness: proximity; - --tw-ordinal: ; - --tw-slashed-zero: ; - --tw-numeric-figure: ; - --tw-numeric-spacing: ; - --tw-numeric-fraction: ; - --tw-ring-inset: ; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-color: rgb(59 130 246 / 0.5); - --tw-ring-offset-shadow: 0 0 #0000; - --tw-ring-shadow: 0 0 #0000; - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - --tw-blur: ; - --tw-brightness: ; - --tw-contrast: ; - --tw-grayscale: ; - --tw-hue-rotate: ; - --tw-invert: ; - --tw-saturate: ; - --tw-sepia: ; - --tw-drop-shadow: ; - --tw-backdrop-blur: ; - --tw-backdrop-brightness: ; - --tw-backdrop-contrast: ; - --tw-backdrop-grayscale: ; - --tw-backdrop-hue-rotate: ; - --tw-backdrop-invert: ; - --tw-backdrop-opacity: ; - --tw-backdrop-saturate: ; - --tw-backdrop-sepia: ; -} - -.container { - width: 100%; -} - -@media (min-width: 640px) { - .container { - max-width: 640px; - } -} - -@media (min-width: 768px) { - .container { - max-width: 768px; - } -} - -@media (min-width: 1024px) { - .container { - max-width: 1024px; - } -} - -@media (min-width: 1280px) { - .container { - max-width: 1280px; - } -} - -@media (min-width: 1536px) { - .container { - max-width: 1536px; - } -} - -.static { - position: static; -} - -.mx-auto { - margin-left: auto; - margin-right: auto; -} - -.block { - display: block; -} - -.flex { - display: flex; -} - -.hidden { - display: none; -} - -.h-screen { - height: 100vh; -} - -.items-center { - align-items: center; -} - -.justify-center { - justify-content: center; -} - -.bg-gray-50 { - --tw-bg-opacity: 1; - background-color: rgb(249 250 251 / var(--tw-bg-opacity)); -} - -.font-serif { - font-family: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; -} - -.text-5xl { - font-size: 3rem; - line-height: 1; -} - -.leading-normal { - line-height: 1.5; -} - -.tracking-normal { - letter-spacing: 0em; -} diff --git a/apps/theme/static_src/.gitignore b/apps/theme/static_src/.gitignore deleted file mode 100644 index 3c3629e..0000000 --- a/apps/theme/static_src/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/apps/theme/static_src/package-lock.json b/apps/theme/static_src/package-lock.json deleted file mode 100644 index cf2d591..0000000 --- a/apps/theme/static_src/package-lock.json +++ /dev/null @@ -1,1880 +0,0 @@ -{ - "name": "theme", - "version": "3.4.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "theme", - "version": "3.4.0", - "license": "MIT", - "devDependencies": { - "@tailwindcss/aspect-ratio": "^0.4.0", - "@tailwindcss/forms": "^0.5.2", - "@tailwindcss/line-clamp": "^0.4.0", - "@tailwindcss/typography": "^0.5.2", - "cross-env": "^7.0.3", - "postcss": "^8.4.14", - "postcss-import": "^14.1.0", - "postcss-nested": "^5.0.6", - "postcss-simple-vars": "^6.0.3", - "rimraf": "^3.0.2", - "tailwindcss": "^3.1.6" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@tailwindcss/aspect-ratio": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.2.tgz", - "integrity": "sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==", - "dev": true, - "peerDependencies": { - "tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1" - } - }, - "node_modules/@tailwindcss/forms": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.3.tgz", - "integrity": "sha512-y5mb86JUoiUgBjY/o6FJSFZSEttfb3Q5gllE4xoKjAAD+vBrnIhE4dViwUuow3va8mpH4s9jyUbUbrRGoRdc2Q==", - "dev": true, - "dependencies": { - "mini-svg-data-uri": "^1.2.3" - }, - "peerDependencies": { - "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1" - } - }, - "node_modules/@tailwindcss/line-clamp": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.2.tgz", - "integrity": "sha512-HFzAQuqYCjyy/SX9sLGB1lroPzmcnWv1FHkIpmypte10hptf4oPUfucryMKovZh2u0uiS9U5Ty3GghWfEJGwVw==", - "dev": true, - "peerDependencies": { - "tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1" - } - }, - "node_modules/@tailwindcss/typography": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.8.tgz", - "integrity": "sha512-xGQEp8KXN8Sd8m6R4xYmwxghmswrd0cPnNI2Lc6fmrC3OojysTBJJGSIVwPV56q4t6THFUK3HJ0EaWwpglSxWw==", - "dev": true, - "dependencies": { - "lodash.castarray": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.merge": "^4.6.2", - "postcss-selector-parser": "6.0.10" - }, - "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders" - } - }, - "node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "dev": true, - "dependencies": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" - } - }, - "node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "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==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/cross-env": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", - "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "bin": { - "cross-env": "src/bin/cross-env.js", - "cross-env-shell": "src/bin/cross-env-shell.js" - }, - "engines": { - "node": ">=10.14", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/defined": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", - "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/detective": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", - "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", - "dev": true, - "dependencies": { - "acorn-node": "^1.8.2", - "defined": "^1.0.0", - "minimist": "^1.2.6" - }, - "bin": { - "detective": "bin/detective.js" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fastq": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz", - "integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/lilconfig": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", - "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/lodash.castarray": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", - "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", - "dev": true - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mini-svg-data-uri": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", - "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", - "dev": true, - "bin": { - "mini-svg-data-uri": "cli.js" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", - "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postcss": { - "version": "8.4.20", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz", - "integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - } - ], - "dependencies": { - "nanoid": "^3.3.4", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", - "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", - "dev": true, - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", - "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", - "dev": true, - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.3.3" - } - }, - "node_modules/postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", - "dev": true, - "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^1.10.2" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", - "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.6" - }, - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-simple-vars": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-simple-vars/-/postcss-simple-vars-6.0.3.tgz", - "integrity": "sha512-fkNn4Zio8vN4vIig9IFdb8lVlxWnYR769RgvxCM6YWlFKie/nQaOcaMMMFz/s4gsfHW4/5bJW+i57zD67mQU7g==", - "dev": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.2.1" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tailwindcss": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz", - "integrity": "sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==", - "dev": true, - "dependencies": { - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "color-name": "^1.1.4", - "detective": "^5.2.1", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.2.12", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "lilconfig": "^2.0.6", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.18", - "postcss-import": "^14.1.0", - "postcss-js": "^4.0.0", - "postcss-load-config": "^3.1.4", - "postcss-nested": "6.0.0", - "postcss-selector-parser": "^6.0.10", - "postcss-value-parser": "^4.2.0", - "quick-lru": "^5.1.1", - "resolve": "^1.22.1" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=12.13.0" - }, - "peerDependencies": { - "postcss": "^8.0.9" - } - }, - "node_modules/tailwindcss/node_modules/postcss-nested": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", - "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", - "dev": true, - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "engines": { - "node": ">=0.4" - } - }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "engines": { - "node": ">= 6" - } - } - }, - "dependencies": { - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@tailwindcss/aspect-ratio": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.2.tgz", - "integrity": "sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==", - "dev": true, - "requires": {} - }, - "@tailwindcss/forms": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.3.tgz", - "integrity": "sha512-y5mb86JUoiUgBjY/o6FJSFZSEttfb3Q5gllE4xoKjAAD+vBrnIhE4dViwUuow3va8mpH4s9jyUbUbrRGoRdc2Q==", - "dev": true, - "requires": { - "mini-svg-data-uri": "^1.2.3" - } - }, - "@tailwindcss/line-clamp": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.2.tgz", - "integrity": "sha512-HFzAQuqYCjyy/SX9sLGB1lroPzmcnWv1FHkIpmypte10hptf4oPUfucryMKovZh2u0uiS9U5Ty3GghWfEJGwVw==", - "dev": true, - "requires": {} - }, - "@tailwindcss/typography": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.8.tgz", - "integrity": "sha512-xGQEp8KXN8Sd8m6R4xYmwxghmswrd0cPnNI2Lc6fmrC3OojysTBJJGSIVwPV56q4t6THFUK3HJ0EaWwpglSxWw==", - "dev": true, - "requires": { - "lodash.castarray": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.merge": "^4.6.2", - "postcss-selector-parser": "6.0.10" - } - }, - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - }, - "acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "dev": true, - "requires": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" - } - }, - "acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true - }, - "anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "cross-env": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", - "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.1" - } - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true - }, - "defined": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", - "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", - "dev": true - }, - "detective": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", - "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", - "dev": true, - "requires": { - "acorn-node": "^1.8.2", - "defined": "^1.0.0", - "minimist": "^1.2.6" - } - }, - "didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true - }, - "dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "dependencies": { - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - } - } - }, - "fastq": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz", - "integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "lilconfig": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", - "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", - "dev": true - }, - "lodash.castarray": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", - "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", - "dev": true - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true - }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "mini-svg-data-uri": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", - "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", - "dev": true - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", - "dev": true - }, - "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", - "dev": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true - }, - "postcss": { - "version": "8.4.20", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.20.tgz", - "integrity": "sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==", - "dev": true, - "requires": { - "nanoid": "^3.3.4", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "postcss-import": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", - "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", - "dev": true, - "requires": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - } - }, - "postcss-js": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz", - "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==", - "dev": true, - "requires": { - "camelcase-css": "^2.0.1" - } - }, - "postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", - "dev": true, - "requires": { - "lilconfig": "^2.0.5", - "yaml": "^1.10.2" - } - }, - "postcss-nested": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz", - "integrity": "sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.6" - } - }, - "postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", - "dev": true, - "requires": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - } - }, - "postcss-simple-vars": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-simple-vars/-/postcss-simple-vars-6.0.3.tgz", - "integrity": "sha512-fkNn4Zio8vN4vIig9IFdb8lVlxWnYR769RgvxCM6YWlFKie/nQaOcaMMMFz/s4gsfHW4/5bJW+i57zD67mQU7g==", - "dev": true, - "requires": {} - }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true - }, - "quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true - }, - "read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "requires": { - "pify": "^2.3.0" - } - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "tailwindcss": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.4.tgz", - "integrity": "sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==", - "dev": true, - "requires": { - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "color-name": "^1.1.4", - "detective": "^5.2.1", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.2.12", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "lilconfig": "^2.0.6", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.18", - "postcss-import": "^14.1.0", - "postcss-js": "^4.0.0", - "postcss-load-config": "^3.1.4", - "postcss-nested": "6.0.0", - "postcss-selector-parser": "^6.0.10", - "postcss-value-parser": "^4.2.0", - "quick-lru": "^5.1.1", - "resolve": "^1.22.1" - }, - "dependencies": { - "postcss-nested": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", - "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", - "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.10" - } - } - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true - }, - "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true - } - } -} diff --git a/apps/theme/static_src/package.json b/apps/theme/static_src/package.json deleted file mode 100644 index 4f80224..0000000 --- a/apps/theme/static_src/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "theme", - "version": "3.4.0", - "description": "", - "scripts": { - "start": "npm run dev", - "build": "npm run build:clean && npm run build:tailwind", - "build:clean": "node_modules/.bin/rimraf ../static/css/dist", - "build:tailwind": "node_modules/.bin/cross-env NODE_ENV=production tailwindcss --postcss -i ./src/styles.css -o ../static/css/dist/styles.css --minify", - "dev": "node_modules/.bin/cross-env NODE_ENV=development tailwindcss --postcss -i ./src/styles.css -o ../static/css/dist/styles.css -w", - "tailwindcss": "node node_modules/tailwindcss/lib/cli.js" - }, - "keywords": [], - "author": "", - "license": "MIT", - "devDependencies": { - "@tailwindcss/aspect-ratio": "^0.4.0", - "@tailwindcss/forms": "^0.5.2", - "@tailwindcss/line-clamp": "^0.4.0", - "@tailwindcss/typography": "^0.5.2", - "cross-env": "^7.0.3", - "postcss": "^8.4.14", - "postcss-import": "^14.1.0", - "postcss-nested": "^5.0.6", - "postcss-simple-vars": "^6.0.3", - "rimraf": "^3.0.2", - "tailwindcss": "^3.1.6" - } -} diff --git a/apps/theme/static_src/postcss.config.js b/apps/theme/static_src/postcss.config.js deleted file mode 100644 index 0b09b34..0000000 --- a/apps/theme/static_src/postcss.config.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - plugins: { - "postcss-import": {}, - "postcss-simple-vars": {}, - "postcss-nested": {} - }, -} diff --git a/apps/theme/static_src/src/styles.css b/apps/theme/static_src/src/styles.css deleted file mode 100644 index b5c61c9..0000000 --- a/apps/theme/static_src/src/styles.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/apps/theme/static_src/tailwind.config.js b/apps/theme/static_src/tailwind.config.js deleted file mode 100644 index 28f3b7b..0000000 --- a/apps/theme/static_src/tailwind.config.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * This is a minimal config. - * - * If you need the full config, get it from here: - * https://unpkg.com/browse/tailwindcss@latest/stubs/defaultConfig.stub.js - */ - -module.exports = { - content: [ - /** - * HTML. Paths to Django template files that will contain Tailwind CSS classes. - */ - - /* Templates within theme app (/templates), e.g. base.html. */ - '../templates/**/*.html', - - /* - * Main templates directory of the project (BASE_DIR/templates). - * Adjust the following line to match your project structure. - */ - '../../templates/**/*.html', - - /* - * Templates in other django apps (BASE_DIR//templates). - * Adjust the following line to match your project structure. - */ - '../../**/templates/**/*.html', - - /** - * JS: If you use Tailwind CSS in JavaScript, uncomment the following lines and make sure - * patterns match your project structure. - */ - /* JS 1: Ignore any JavaScript in node_modules folder. */ - // '!../../**/node_modules', - /* JS 2: Process all JavaScript files in the project. */ - // '../../**/*.js', - - /** - * Python: If you use Tailwind CSS classes in Python, uncomment the following line - * and make sure the pattern below matches your project structure. - */ - // '../../**/*.py' - ], - theme: { - extend: {}, - }, - plugins: [ - /** - * '@tailwindcss/forms' is the forms plugin that provides a minimal styling - * for forms. If you don't like it or have own styling for forms, - * comment the line below to disable '@tailwindcss/forms'. - */ - require('@tailwindcss/forms'), - require('@tailwindcss/typography'), - require('@tailwindcss/line-clamp'), - require('@tailwindcss/aspect-ratio'), - ], -} diff --git a/apps/theme/templates/theme/base.html b/apps/theme/templates/theme/base.html deleted file mode 100644 index 0c40aeb..0000000 --- a/apps/theme/templates/theme/base.html +++ /dev/null @@ -1,19 +0,0 @@ -{% load static tailwind_tags %} - - - - Django Tailwind - - - - {% tailwind_css %} - - - -
-
-

Django + Tailwind = ❤️

-
-
- - diff --git a/apps/user/__pycache__/__init__.cpython-311.pyc b/apps/user/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 04d1084..0000000 Binary files a/apps/user/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/apps/user/__pycache__/__init__.cpython-38.pyc b/apps/user/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 2628c6a..0000000 Binary files a/apps/user/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/apps/user/__pycache__/admin.cpython-311.pyc b/apps/user/__pycache__/admin.cpython-311.pyc deleted file mode 100644 index 8187953..0000000 Binary files a/apps/user/__pycache__/admin.cpython-311.pyc and /dev/null differ diff --git a/apps/user/__pycache__/admin.cpython-38.pyc b/apps/user/__pycache__/admin.cpython-38.pyc deleted file mode 100644 index c134067..0000000 Binary files a/apps/user/__pycache__/admin.cpython-38.pyc and /dev/null differ diff --git a/apps/user/__pycache__/apps.cpython-311.pyc b/apps/user/__pycache__/apps.cpython-311.pyc deleted file mode 100644 index 2a85dec..0000000 Binary files a/apps/user/__pycache__/apps.cpython-311.pyc and /dev/null differ diff --git a/apps/user/__pycache__/apps.cpython-38.pyc b/apps/user/__pycache__/apps.cpython-38.pyc deleted file mode 100644 index b18f04e..0000000 Binary files a/apps/user/__pycache__/apps.cpython-38.pyc and /dev/null differ diff --git a/apps/user/__pycache__/forms.cpython-311.pyc b/apps/user/__pycache__/forms.cpython-311.pyc deleted file mode 100644 index 1c923dd..0000000 Binary files a/apps/user/__pycache__/forms.cpython-311.pyc and /dev/null differ diff --git a/apps/user/__pycache__/html_urls.cpython-311.pyc b/apps/user/__pycache__/html_urls.cpython-311.pyc deleted file mode 100644 index 406db6e..0000000 Binary files a/apps/user/__pycache__/html_urls.cpython-311.pyc and /dev/null differ diff --git a/apps/user/__pycache__/html_views.cpython-311.pyc b/apps/user/__pycache__/html_views.cpython-311.pyc deleted file mode 100644 index e2c84eb..0000000 Binary files a/apps/user/__pycache__/html_views.cpython-311.pyc and /dev/null differ diff --git a/apps/user/__pycache__/models.cpython-311.pyc b/apps/user/__pycache__/models.cpython-311.pyc deleted file mode 100644 index 73d7408..0000000 Binary files a/apps/user/__pycache__/models.cpython-311.pyc and /dev/null differ diff --git a/apps/user/__pycache__/models.cpython-38.pyc b/apps/user/__pycache__/models.cpython-38.pyc deleted file mode 100644 index 3d78e6c..0000000 Binary files a/apps/user/__pycache__/models.cpython-38.pyc and /dev/null differ diff --git a/apps/user/__pycache__/services.cpython-311.pyc b/apps/user/__pycache__/services.cpython-311.pyc deleted file mode 100644 index e8e4d4c..0000000 Binary files a/apps/user/__pycache__/services.cpython-311.pyc and /dev/null differ diff --git a/apps/user/__pycache__/services.cpython-38.pyc b/apps/user/__pycache__/services.cpython-38.pyc deleted file mode 100644 index c73eea9..0000000 Binary files a/apps/user/__pycache__/services.cpython-38.pyc and /dev/null differ diff --git a/apps/user/admin.py b/apps/user/admin.py index 8c38f3f..5efba3f 100644 --- a/apps/user/admin.py +++ b/apps/user/admin.py @@ -1,3 +1,6 @@ from django.contrib import admin +from apps.user.models import User + # Register your models here. +admin.site.register(User) diff --git a/apps/user/auth_urls.py b/apps/user/auth_urls.py new file mode 100644 index 0000000..a763aa5 --- /dev/null +++ b/apps/user/auth_urls.py @@ -0,0 +1,39 @@ +from django.urls import path + +# from rest_framework_simplejwt.views import TokenVerifyView + +from . import auth_views + +urlpatterns = [ + path("signup/", auth_views.BasicSignUpView.as_view(), name="basic-signup"), + path("login/", auth_views.BasicSignInView.as_view(), name="basic-login"), + path("leave/", auth_views.SecessionView.as_view(), name="basic-leave"), +] +# urlpatterns += [ +# path("token/verify/", TokenVerifyView.as_view(), name="token-verify"), +# path("token/refresh/", auth_views.TokenRefreshView.as_view(), name="token-refresh"), +# ] + +urlpatterns += [ + path( + "check-email/", + auth_views.CheckDuplicateUsernameView.as_view(), + name="check-email", + ), + path( + "email-verification/", + auth_views.EmailVerification.as_view(), + name="verify-email", + ), + path( + "email-confirmation/", auth_views.EmailConfirmation.as_view(), name="activate" + ), + path( + "password-change/", + auth_views.PasswordChangeView.as_view(), + name="password-change", + ), + path( + "password-reset/", auth_views.PasswordResetView.as_view(), name="password-reset" + ), +] diff --git a/apps/user/auth_views.py b/apps/user/auth_views.py new file mode 100644 index 0000000..cd0909c --- /dev/null +++ b/apps/user/auth_views.py @@ -0,0 +1,306 @@ +from django.contrib.auth.models import update_last_login +from django.http import JsonResponse, Http404 +from django.shortcuts import get_object_or_404 +from django.utils import timezone +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema +from rest_framework.permissions import AllowAny +from rest_framework.views import APIView +from rest_framework.exceptions import AuthenticationFailed, NotFound, ValidationError + +from config.exceptions import DuplicateInstance +from .models import User +from rest_framework.response import Response +from rest_framework import status, permissions +from django.contrib.auth.hashers import check_password +from django.core.mail import EmailMessage + +from .serializers import UserSerializer +from .services import UserService + + +class BasicSignUpView(APIView): + serializer_class = UserSerializer + + @swagger_auto_schema( + operation_summary="Sign up", + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + required=[ + "email", + "service_name", + "service_expl", + "password", + "confirm_password", + ], + properties={ + "email": openapi.Schema( + type=openapi.FORMAT_EMAIL, description="가입하려는 이메일" + ), + "service_name": openapi.Schema( + type=openapi.TYPE_STRING, description="운영하려는 서비스의 이름" + ), + "service_expl": openapi.Schema( + type=openapi.TYPE_STRING, + description="서비스 설명", + ), + "password": openapi.Schema(type=openapi.FORMAT_PASSWORD), + "confirm_password": openapi.Schema(type=openapi.FORMAT_PASSWORD), + }, + ), + responses={ + 201: openapi.Response("user", UserSerializer), + 400: "Passwords doesn't match", + }, + ) + def post(self, request, *args, **kwargs): + + password = request.data.get("password") + confirm_password = request.data.get("confirm_password") + + if password != confirm_password: + raise ValidationError("Passwords doesn't match") + + email = request.data.get("email") + service_name = request.data.get("service_name") + service_expl = request.data.get("service_expl") + + access_key = UserService.generate_access_key() + secret_key = UserService.generate_secret_key() + + user = User.objects.create_user( + email=email, + password=password, + service_name=service_name, + service_expl=service_expl, + access_key=access_key, + secret_key=secret_key, + ) + + serializer = UserSerializer(user) + + return Response(serializer.data, status=status.HTTP_201_CREATED) + + +class BasicSignInView(APIView): + serializer = UserSerializer + + @swagger_auto_schema( + operation_summary="Sign In", + responses={ + 201: openapi.Response("user", UserSerializer), + 401: "Incorrect password", + }, + ) + def post(self, request, *args, **kwargs): + email = request.data.get("email") + password = request.data.get("password") + + try: + user = get_object_or_404(User, email=email) + except Http404: + raise AuthenticationFailed("No user by the provided email") + + if not check_password(password, user.password): + raise AuthenticationFailed("Incorrect password") + + update_last_login(None, user) + serializer = UserSerializer(user) + + return Response( + serializer.data, + status=status.HTTP_200_OK, + ) + + +class SecessionView(APIView): + serializer = UserSerializer + permission_classes = [permissions.IsAuthenticated] + + @swagger_auto_schema( + operation_summary="Leave", + responses={200: openapi.Response("user", UserSerializer)}, + ) + def post(self, request, *args, **kwargs): + service = UserService(request.user, request) + user = service.deactivate_user() + serializer = UserSerializer(user) + + return Response(serializer.data, status=status.HTTP_200_OK) + + +class CheckDuplicateUsernameView(APIView): + @swagger_auto_schema( + operation_summary="Check if there's duplicate email (username)", + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={"email": openapi.Schema(type=openapi.FORMAT_EMAIL)}, + ), + responses={ + 200: openapi.Response( + description="No duplicates", + schema=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + "email": openapi.Schema( + type=openapi.TYPE_STRING, description="email" + ), + }, + ), + ), + 409: "Provided email already exists", + }, + ) + def post(self, request, *args, **kwargs): + email = request.data.get("email") + + existing_email = User.objects.filter(email=email).first() + if existing_email: + raise DuplicateInstance("Provided email already exists") + + return Response({"email": email}, status=status.HTTP_200_OK) + + +class PasswordChangeView(APIView): + serializer = UserSerializer + permission_classes = [permissions.IsAuthenticated] + + @swagger_auto_schema( + operation_summary="Change user password", + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + "currentPassword": openapi.Schema(type=openapi.FORMAT_PASSWORD), + "newPassword": openapi.Schema(type=openapi.FORMAT_PASSWORD), + }, + ), + responses={ + 200: openapi.Response("Success", UserSerializer), + 401: "Password do not match", + }, + ) + def post(self, request, *args, **kwargs): + user = request.user + current_password = request.data.get("current_password") + new_password = request.data.get("new_password") + + if not check_password(current_password, user.password): + raise AuthenticationFailed("Password do not match") + + user.set_password(new_password) + user.updated_at = timezone.now() + user.save(update_fields=["password", "updated_at"]) + + serializer = UserSerializer(user) + return Response(serializer.data, status=status.HTTP_200_OK) + + +class PasswordResetView(APIView): + permission_classes = [AllowAny] + + @swagger_auto_schema( + operation_summary="Reset password to random string sent to user email", + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={"email": openapi.Schema(type=openapi.FORMAT_EMAIL)}, + ), + responses={ + 404: "User with the provided email does not exist", + 500: "Failed to send email. Try again later.", + }, + ) + def post(self, request, *args, **kwargs): + email = request.data.get("email") + + try: + user = get_object_or_404(User, email=email) + except Http404: + raise NotFound("User with the provided email does not exist") + + new_password = UserService.generate_random_code(3, 8) + user.set_password(new_password) + user.save(update_fields=["password"]) + + email = EmailMessage( + "[ChatBox] 비밀번호가 초기화 되었습니다.", + f"비밀번호가 아래의 임시 비밀번호로 변경되었습니다. 아래 비밀번호로 다시 로그인하신 뒤 꼭 비밀번호를 변경해주세요.\n임시 비밀번호: {new_password}", + to=[email], # 받는 이메일 + ) + success = email.send() + + if success > 0: + return Response(status=status.HTTP_200_OK) + elif success == 0: + return Response( + {"details": "Failed to send email. Try again later."}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + + +class EmailVerification(APIView): + @swagger_auto_schema( + operation_summary="Send verification code to user email when signing up", + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={"email": openapi.Schema(type=openapi.FORMAT_EMAIL)}, + ), + responses={ + 500: "Failed to send email. Try again later or try with a valid email." + }, + ) + def post(self, request, *args, **kwargs): + email = request.data.get("email") + generated_code = UserService.generate_random_code(5, 8) + + # set code in cookie + res = JsonResponse({"success": True}) + # TODO: httponly, secure options + res.set_cookie("email_verification_code", generated_code, max_age=300) + + # send email + email = EmailMessage( + "[ChatBox] 이메일 인증 코드입니다.", + generated_code, + to=[email], # 받는 이메일 + ) + success = email.send() + + if success > 0: + return Response(status=status.HTTP_200_OK) + elif success == 0: + return Response( + { + "details": "Failed to send email. Try again later or try with a valid email." + }, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + + +class EmailConfirmation(APIView): + @swagger_auto_schema( + operation_summary="Confirm code sent to email for signing up", + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={"verification_code": openapi.Schema(type=openapi.TYPE_STRING)}, + ), + responses={ + 400: "No cookies attached", + 409: "Verification code does not match", + }, + ) + def post(self, request, *args, **kwargs): + if "email_verification_code" in request.COOKIES: + code_cookie = request.COOKIES.get("email_verification_code") + else: + return Response( + {"detail": "No cookies attached"}, status=status.HTTP_400_BAD_REQUEST + ) + + code_input = request.data.get("verification_code") + if code_cookie == code_input: + return Response(status=status.HTTP_200_OK) + else: + return Response( + {"detail": "Verification code does not match"}, + status=status.HTTP_409_CONFLICT, + ) diff --git a/apps/user/html_urls.py b/apps/user/html_urls.py deleted file mode 100644 index a5b921a..0000000 --- a/apps/user/html_urls.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.urls import path, include -from .html_views import home_view, profile_view - -urlpatterns = [ - path("", include("allauth.urls")), - path("home", home_view, name="home"), - path("profile", profile_view, name="profile"), -] \ No newline at end of file diff --git a/apps/user/html_views.py b/apps/user/html_views.py deleted file mode 100644 index 5c1c490..0000000 --- a/apps/user/html_views.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.shortcuts import render - - -def home_view(request): - return render(request, "account/home.html") - - -def profile_view(request): - return render(request, "account/profile.html") diff --git a/apps/user/migrations/0001_initial.py b/apps/user/migrations/0001_initial.py index 5f28a6d..d074ab7 100644 --- a/apps/user/migrations/0001_initial.py +++ b/apps/user/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.4 on 2022-12-26 01:48 +# Generated by Django 4.1.5 on 2023-01-04 09:02 import apps.user.models from django.db import migrations, models @@ -8,7 +8,9 @@ class Migration(migrations.Migration): initial = True - dependencies = [] + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] operations = [ migrations.CreateModel( @@ -21,6 +23,14 @@ class Migration(migrations.Migration): blank=True, null=True, verbose_name="last login" ), ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), ("created_at", models.DateTimeField(auto_now_add=True)), ("updated_at", models.DateTimeField(auto_now=True)), ("is_deleted", models.BooleanField(default=False)), @@ -30,6 +40,43 @@ class Migration(migrations.Migration): ("secret_key", models.CharField(max_length=64)), ("service_name", models.CharField(max_length=50)), ("service_expl", models.CharField(max_length=200)), + ( + "profile_name", + models.CharField(blank=True, default="", max_length=50), + ), + ( + "description", + models.CharField(blank=True, default="", max_length=200), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + ), + ), + ( + "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={ "db_table": "user", diff --git a/apps/user/migrations/0002_user_groups_user_is_superuser_user_user_permissions_and_more.py b/apps/user/migrations/0002_user_groups_user_is_superuser_user_user_permissions_and_more.py deleted file mode 100644 index 56f4638..0000000 --- a/apps/user/migrations/0002_user_groups_user_is_superuser_user_user_permissions_and_more.py +++ /dev/null @@ -1,51 +0,0 @@ -# Generated by Django 4.1.4 on 2022-12-26 18:22 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("auth", "0012_alter_user_first_name_max_length"), - ("user", "0001_initial"), - ] - - operations = [ - migrations.AddField( - model_name="user", - name="groups", - field=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", - ), - ), - migrations.AddField( - model_name="user", - name="is_superuser", - field=models.BooleanField( - default=False, - help_text="Designates that this user has all permissions without explicitly assigning them.", - verbose_name="superuser status", - ), - ), - migrations.AddField( - model_name="user", - name="user_permissions", - field=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", - ), - ), - migrations.AlterModelTable( - name="user", - table="user", - ), - ] diff --git a/apps/user/migrations/0002_user_service_domain.py b/apps/user/migrations/0002_user_service_domain.py new file mode 100644 index 0000000..0e80f5e --- /dev/null +++ b/apps/user/migrations/0002_user_service_domain.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.5 on 2023-01-06 11:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("user", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="service_domain", + field=models.CharField(max_length=200, null=True), + ), + ] diff --git a/apps/user/migrations/0003_user_profile_image.py b/apps/user/migrations/0003_user_profile_image.py new file mode 100644 index 0000000..6718621 --- /dev/null +++ b/apps/user/migrations/0003_user_profile_image.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.5 on 2023-01-14 14:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("user", "0002_user_service_domain"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="profile_image", + field=models.ImageField(blank=True, null=True, upload_to=""), + ), + ] diff --git a/apps/user/migrations/__pycache__/0001_initial.cpython-311.pyc b/apps/user/migrations/__pycache__/0001_initial.cpython-311.pyc deleted file mode 100644 index 0793576..0000000 Binary files a/apps/user/migrations/__pycache__/0001_initial.cpython-311.pyc and /dev/null differ diff --git a/apps/user/migrations/__pycache__/0001_initial.cpython-38.pyc b/apps/user/migrations/__pycache__/0001_initial.cpython-38.pyc deleted file mode 100644 index 79ed2f9..0000000 Binary files a/apps/user/migrations/__pycache__/0001_initial.cpython-38.pyc and /dev/null differ diff --git a/apps/user/migrations/__pycache__/0002_user_groups_user_is_superuser_user_user_permissions_and_more.cpython-311.pyc b/apps/user/migrations/__pycache__/0002_user_groups_user_is_superuser_user_user_permissions_and_more.cpython-311.pyc deleted file mode 100644 index dca90fa..0000000 Binary files a/apps/user/migrations/__pycache__/0002_user_groups_user_is_superuser_user_user_permissions_and_more.cpython-311.pyc and /dev/null differ diff --git a/apps/user/migrations/__pycache__/__init__.cpython-311.pyc b/apps/user/migrations/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index fe58cfc..0000000 Binary files a/apps/user/migrations/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/apps/user/migrations/__pycache__/__init__.cpython-38.pyc b/apps/user/migrations/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 5087028..0000000 Binary files a/apps/user/migrations/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/apps/user/models.py b/apps/user/models.py index 9cefdbf..8d13ce3 100644 --- a/apps/user/models.py +++ b/apps/user/models.py @@ -67,7 +67,10 @@ def create_superuser(self, email, password, **extra_fields): extra_fields.setdefault("is_deleted", False) # TODO: more details on access key and secret key - extra_fields.setdefault("access_key", base64.urlsafe_b64encode(uuid.uuid4().bytes).decode('utf8').rstrip('=\n')) + extra_fields.setdefault( + "access_key", + base64.urlsafe_b64encode(uuid.uuid4().bytes).decode("utf8").rstrip("=\n"), + ) extra_fields.setdefault("secret_key", secrets.token_hex(32)) extra_fields.setdefault("service_name", "Chat Box") @@ -83,18 +86,29 @@ class User(AbstractBaseUser, TimeStampMixin, SoftDeleteMixin, PermissionsMixin): access_key = models.CharField(max_length=22, null=False, blank=False) secret_key = models.CharField(max_length=64, null=False, blank=False) service_name = models.CharField(max_length=50, null=False, blank=False) + service_domain = models.CharField(max_length=200, null=True, blank=False) service_expl = models.CharField(max_length=200, null=False, blank=False) + profile_name = models.CharField(max_length=50, null=False, blank=True, default="") + description = models.CharField(max_length=200, null=False, blank=True, default="") + profile_image = models.ImageField(blank=True, null=True) + + is_staff = models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + ) objects = UserManager() EMAIL_FIELD = "email" USERNAME_FIELD = "email" - REQUIRED_FIELDS = ["service_name", "service_expl"] + REQUIRED_FIELDS = ["profile_name", "service_name", "service_expl"] class Meta: db_table = "user" unique_together = ["email"] + def __str__(self): + return f"[{self.id}] {self.get_username()}" - - + def __repr__(self): + return f"User({self.id}, {self.get_username()})" diff --git a/apps/user/serializers.py b/apps/user/serializers.py new file mode 100644 index 0000000..d8470c1 --- /dev/null +++ b/apps/user/serializers.py @@ -0,0 +1,52 @@ +from rest_framework import serializers + +from apps.user.models import User + + +class UserSerializer(serializers.ModelSerializer): + # TODO: profile image 반활할 때는 thumbnail 로 + class Meta: + model = User + fields = [ + "id", + "email", + "profile_name", + "description", + "access_key", + "secret_key", + "service_name", + "service_expl", + "service_domain", + "is_deleted", + "created_at", + "updated_at", + ] + read_only_fields = [ + "id", + "email", + "service_name", + "service_expl", + "access_key", + "secret_key", + "is_deleted", + "created_at", + "updated_at", + ] + + +class ClientSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = [ + "email", + "profile_name", + "description", + "service_name", + # TODO: add profile image + ] + read_only_fields = [ + "email", + "profile_name", + "description", + "service_name", + ] diff --git a/apps/user/services.py b/apps/user/services.py index 2eabc50..7752eaf 100644 --- a/apps/user/services.py +++ b/apps/user/services.py @@ -1,6 +1,10 @@ +import datetime +import string import uuid import base64 import secrets +import random + from rest_framework.request import Request from apps.user.models import User @@ -11,10 +15,24 @@ def __init__(self, user: User, request: Request): self.user = user self.request = request + def deactivate_user(self): + self.user.is_deleted = True + self.user.updated_at = datetime.datetime.now() + self.user.save(updated_field=["is_deleted", "updated_at"]) + return self.user + @staticmethod def generate_access_key(): - return base64.urlsafe_b64encode(uuid.uuid4().bytes).decode('utf8').rstrip('=\n') + return base64.urlsafe_b64encode(uuid.uuid4().bytes).decode("utf8").rstrip("=\n") @staticmethod def generate_secret_key(): - return secrets.token_hex(32) \ No newline at end of file + return secrets.token_hex(32) + + @staticmethod + def generate_random_code(number_of_strings, length_of_string): + for x in range(number_of_strings): + return "".join( + random.choice(string.ascii_letters + string.digits) + for _ in range(length_of_string) + ) diff --git a/apps/user/templates/account/home.html b/apps/user/templates/account/home.html deleted file mode 100644 index ee821cd..0000000 --- a/apps/user/templates/account/home.html +++ /dev/null @@ -1,14 +0,0 @@ -{% load tailwind_tags %} - - - - - - Chat Box - {% tailwind_css %} - - -

Main Page

-

working with Tailwind css

- - \ No newline at end of file diff --git a/apps/user/templates/account/instructions.html b/apps/user/templates/account/instructions.html deleted file mode 100644 index 566549b..0000000 --- a/apps/user/templates/account/instructions.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Title - - - - - \ No newline at end of file diff --git a/apps/user/templates/account/login.html b/apps/user/templates/account/login.html deleted file mode 100644 index ad5438c..0000000 --- a/apps/user/templates/account/login.html +++ /dev/null @@ -1,58 +0,0 @@ -{% extends "account/base.html" %} - -{% load i18n %} -{% load account socialaccount %} -{% load tailwind_tags %} - - - - - Login - {% tailwind_css %} - - - - -{% block head_title %}{% trans "Sign In" %}{% endblock %} - -{% block content %} - -

{% trans "Sign In" %}

- -{% get_providers as socialaccount_providers %} - -{% if socialaccount_providers %} -

{% blocktrans with site.name as site_name %}Please sign in with one -of your existing third party accounts. Or, sign up -for a {{ site_name }} account and sign in below:{% endblocktrans %}

- -
- -
    - {% include "socialaccount/snippets/provider_list.html" with process="login" %} -
- - - -
- -{% include "socialaccount/snippets/login_extra.html" %} - -{% else %} -

{% blocktrans %}If you have not created an account yet, then please -sign up first.{% endblocktrans %}

-{% endif %} - - - -{% endblock %} - - \ No newline at end of file diff --git a/apps/user/templates/account/profile.html b/apps/user/templates/account/profile.html deleted file mode 100644 index 43fd3b5..0000000 --- a/apps/user/templates/account/profile.html +++ /dev/null @@ -1,13 +0,0 @@ -{% load tailwind_tags %} - - - - - My Page - {% tailwind_css %} - - -

My Page

- - - \ No newline at end of file diff --git a/apps/user/templates/account/signup.html b/apps/user/templates/account/signup.html deleted file mode 100644 index fbb495b..0000000 --- a/apps/user/templates/account/signup.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends "account/base.html" %} -{% load i18n %} - - - - - - Title - - - -{% block head_title %}{% trans "Signup" %}{% endblock %} - -{% block content %} -

{% trans "Sign Up" %}

- -

{% blocktrans %}Already have an account? Then please sign in.{% endblocktrans %}

- - - -{% endblock %} - - \ No newline at end of file diff --git a/apps/user/urls.py b/apps/user/urls.py new file mode 100644 index 0000000..198ba6d --- /dev/null +++ b/apps/user/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from .views import UserDetailView, UserListView, ClientProfileView + +urlpatterns = [ + path("", UserListView.as_view(), name="user-list"), + path("/", UserDetailView.as_view(), name="user-detail"), + path("client/", ClientProfileView.as_view(), name="client-profile"), +] diff --git a/apps/user/views.py b/apps/user/views.py index 91ea44a..8cd0662 100644 --- a/apps/user/views.py +++ b/apps/user/views.py @@ -1,3 +1,98 @@ -from django.shortcuts import render +import datetime -# Create your views here. +from django.http import Http404 +from django.shortcuts import get_object_or_404 +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema +from rest_framework import generics, status +from rest_framework.exceptions import AuthenticationFailed +from rest_framework.response import Response + +from apps.user.models import User +from apps.user.serializers import UserSerializer, ClientSerializer +from config.exceptions import InstanceNotFound + + +class UserListView(generics.ListAPIView): + serializer_class = UserSerializer + queryset = User.objects.all() + + +class UserDetailView(generics.RetrieveUpdateAPIView): + serializer_class = UserSerializer + queryset = User.objects.all() + allowed_methods = ["PATCH", "GET"] + + @swagger_auto_schema( + operation_summary="Update user profile", + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + "profileName": openapi.Schema( + type=openapi.TYPE_STRING, description="채팅 프로필 이름" + ), + "description": openapi.Schema( + type=openapi.TYPE_STRING, description="채팅 프로필 상태 메세지" + ), + "serviceDomain": openapi.Schema( + type=openapi.TYPE_STRING, description="운영하려는 사이트의 도메인" + ), + "profileImage": openapi.Schema( + type=openapi.TYPE_FILE, description="프로필 사진" + ), + }, + ), + responses={ + 200: openapi.Response("user", UserSerializer), + 400: "Passwords doesn't match", + }, + ) + def patch(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data, partial=True) + if serializer.is_valid(raise_exception=True): + serializer.save(updated_at=datetime.datetime.now()) + + return Response(serializer.data, status=status.HTTP_200_OK) + + +class ClientProfileView(generics.RetrieveAPIView): + serializer_class = ClientSerializer + queryset = User.objects.all() + + access_key_param = openapi.Parameter( + "X-ChatBox-Access-Key", + openapi.IN_HEADER, + description="service access key", + type=openapi.TYPE_STRING, + ) + secret_key_param = openapi.Parameter( + "X-ChatBox-Secret-Key", + openapi.IN_HEADER, + description="service secret key", + type=openapi.TYPE_STRING, + ) + + @swagger_auto_schema( + tags=["client"], + operation_summary="Fetch host data (for client side)", + manual_parameters=[access_key_param, secret_key_param], + responses={ + 200: openapi.Response("user", UserSerializer), + 400: "Passwords doesn't match", + }, + ) + def get(self, request, *args, **kwargs) -> Response: + access_key = request.headers["X-ChatBox-Access-Key"] + secret_key = request.headers["X-ChatBox-Secret-Key"] + + try: + instance = get_object_or_404( + User, access_key=access_key, secret_key=secret_key + ) + except Http404: + raise AuthenticationFailed("Invalid access key or secret key") + + serializer = self.get_serializer(instance) + + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 39f748a..0000000 Binary files a/config/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/config/__pycache__/__init__.cpython-38.pyc b/config/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 3f5bb4e..0000000 Binary files a/config/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/config/__pycache__/api_urls_v1.cpython-311.pyc b/config/__pycache__/api_urls_v1.cpython-311.pyc deleted file mode 100644 index 01bba0f..0000000 Binary files a/config/__pycache__/api_urls_v1.cpython-311.pyc and /dev/null differ diff --git a/config/__pycache__/api_urls_v1.cpython-38.pyc b/config/__pycache__/api_urls_v1.cpython-38.pyc deleted file mode 100644 index 955840e..0000000 Binary files a/config/__pycache__/api_urls_v1.cpython-38.pyc and /dev/null differ diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc deleted file mode 100644 index 44c3717..0000000 Binary files a/config/__pycache__/settings.cpython-311.pyc and /dev/null differ diff --git a/config/__pycache__/settings.cpython-38.pyc b/config/__pycache__/settings.cpython-38.pyc deleted file mode 100644 index 2968ff3..0000000 Binary files a/config/__pycache__/settings.cpython-38.pyc and /dev/null differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc deleted file mode 100644 index d96091d..0000000 Binary files a/config/__pycache__/urls.cpython-311.pyc and /dev/null differ diff --git a/config/__pycache__/urls.cpython-38.pyc b/config/__pycache__/urls.cpython-38.pyc deleted file mode 100644 index 0921abe..0000000 Binary files a/config/__pycache__/urls.cpython-38.pyc and /dev/null differ diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc deleted file mode 100644 index 8ab080c..0000000 Binary files a/config/__pycache__/wsgi.cpython-311.pyc and /dev/null differ diff --git a/config/__pycache__/wsgi.cpython-38.pyc b/config/__pycache__/wsgi.cpython-38.pyc deleted file mode 100644 index 2493d44..0000000 Binary files a/config/__pycache__/wsgi.cpython-38.pyc and /dev/null differ diff --git a/config/api_urls_v1.py b/config/api_urls_v1.py index 60c148d..03eea67 100644 --- a/config/api_urls_v1.py +++ b/config/api_urls_v1.py @@ -49,7 +49,9 @@ def hello_world(request: Request) -> Response: urlpatterns = [ path("", hello_world), path("admin/", admin.site.urls), - # path("users/", include("apps.user.urls")), + path("auth/", include("apps.user.auth_urls")), + path("users/", include("apps.user.urls")), + path("chat/", include("apps.chat.urls")), path("api-auth/", include("rest_framework.urls")), ] diff --git a/config/asgi.py b/config/asgi.py index 0fdc25c..b71a044 100644 --- a/config/asgi.py +++ b/config/asgi.py @@ -9,8 +9,19 @@ import os +from channels.auth import AuthMiddlewareStack +from channels.routing import ProtocolTypeRouter, URLRouter +from channels.security.websocket import AllowedHostsOriginValidator from django.core.asgi import get_asgi_application +from apps.chat.routing import websocket_urlpatterns os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") -application = get_asgi_application() +application = ProtocolTypeRouter( + { + "http": get_asgi_application(), + "websocket": AllowedHostsOriginValidator( + AuthMiddlewareStack(URLRouter(websocket_urlpatterns)) + ), + } +) diff --git a/config/exceptions.py b/config/exceptions.py new file mode 100644 index 0000000..e279cbe --- /dev/null +++ b/config/exceptions.py @@ -0,0 +1,67 @@ +from django.core.exceptions import BadRequest +from rest_framework.views import exception_handler +from rest_framework import exceptions +from django.http import Http404 +from rest_framework.exceptions import APIException + + +class InstanceNotFound(APIException): + status_code = 404 + default_detail = "Not Found" + default_code = "not_found" + + def __init__(self, detail=None): + if detail is None: + self.detail = self.default_detail + else: + self.detail = detail + + +class DuplicateInstance(APIException): + status_code = 409 + default_detail = "Instance with provided data already exists" + default_code = "duplicate_instance" + + def __init__(self, detail=None): + if detail is None: + self.detail = self.default_detail + else: + self.detail = detail + + +def custom_exception_handler(exc, context): + # Call REST framework's default exception handler first, + # to get the standard error response. + response = exception_handler(exc, context) + + # Update the structure of the response data. + if response is not None: + if isinstance(exc, Http404): + customized_response = {"code": response.status_code, "detail": "Not Found"} + elif isinstance(exc, exceptions.NotFound): + customized_response = {"code": response.status_code, "detail": exc.detail} + elif isinstance(exc, exceptions.MethodNotAllowed): + customized_response = {"code": response.status_code, "detail": exc.detail} + elif isinstance(exc, exceptions.NotAcceptable): + customized_response = {"code": response.status_code, "detail": exc.detail} + elif isinstance(exc, exceptions.UnsupportedMediaType): + customized_response = {"code": response.status_code, "detail": exc.detail} + elif isinstance(exc, exceptions.AuthenticationFailed): + customized_response = {"code": response.status_code, "detail": exc.detail} + elif isinstance(exc, exceptions.PermissionDenied): + customized_response = {"code": response.status_code, "detail": exc.detail} + elif isinstance(exc, exceptions.NotAuthenticated): + customized_response = {"code": response.status_code, "detail": exc.detail} + elif isinstance(exc, InstanceNotFound): + customized_response = {"code": response.status_code, "detail": exc.detail} + elif isinstance(exc, DuplicateInstance): + customized_response = {"code": response.status_code, "detail": exc.detail} + else: + customized_response = { + "code": response.status_code, + "detail": response.data, + } + + response.data = customized_response + + return response diff --git a/apps/theme/__init__.py b/config/middlewares/__init__.py similarity index 100% rename from apps/theme/__init__.py rename to config/middlewares/__init__.py diff --git a/config/middlewares/add_headers.py b/config/middlewares/add_headers.py new file mode 100644 index 0000000..564117c --- /dev/null +++ b/config/middlewares/add_headers.py @@ -0,0 +1,9 @@ +class AddHeaders: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = self.get_response(request) + response["Host"] = "localhost" + response["Link"] = '; rel="profile"' + return response diff --git a/config/middlewares/request_middleware.py b/config/middlewares/request_middleware.py new file mode 100644 index 0000000..108c5b3 --- /dev/null +++ b/config/middlewares/request_middleware.py @@ -0,0 +1,44 @@ +from rest_framework import status +from rest_framework.exceptions import ValidationError +from rest_framework.response import Response + +from config.renderer import CustomRenderer + + +class RequestMiddleware: + def __init__(self, get_response): + self.get_response = get_response + self.VALID_URLS = ["api", "swagger", "redoc"] + + def __call__(self, request): + + try: + self.process_request(request) + except ValidationError as e: + response = Response( + {"detail": e.detail[0]}, status=status.HTTP_400_BAD_REQUEST + ) + response.accepted_renderer = CustomRenderer() + response.accepted_media_type = "application/json" + response.renderer_context = {} + response.render() + return response + + response = self.get_response(request) + + return response + + def process_request(self, request): + + if ( + "api" in request.path + and "swagger" not in request.path + and "redoc" not in request.path + ): + if "application/json" not in request.headers["Accept"]: + raise ValidationError("Accept type is 'application/json'") + + if "version" not in request.headers["Accept"].split(";")[1]: + raise ValidationError( + "Accept header must include api version for api requests" + ) diff --git a/config/renderer.py b/config/renderer.py new file mode 100644 index 0000000..10831ee --- /dev/null +++ b/config/renderer.py @@ -0,0 +1,18 @@ +from rest_framework.renderers import JSONRenderer + + +class CustomRenderer(JSONRenderer): + def render(self, data, accepted_media_type=None, renderer_context=None): + status_code = renderer_context["response"].status_code + response = {} + + if not str(status_code).startswith("2"): + # 에러 일 때 + response["code"] = status_code + response["detail"] = data["detail"] + else: + response = data + + return super(CustomRenderer, self).render( + response, accepted_media_type, renderer_context + ) diff --git a/config/settings.py b/config/settings.py index 08eaf4a..b51e3a0 100644 --- a/config/settings.py +++ b/config/settings.py @@ -15,6 +15,7 @@ import os import sys from dotenv import load_dotenv +from corsheaders.defaults import default_headers load_dotenv() @@ -42,67 +43,96 @@ CHATBOX_APPS = [ "apps.user", "apps.chat", - "apps.theme", ] THIRD_PARTY_APPS = [ - "allauth", - "allauth.account", - "allauth.socialaccount", - # "allauth.socialaccount.providers.google", - # "allauth.socialaccount.providers.kakao", - # "allauth.socialaccount.providers.naver", "drf_yasg", "rest_framework", "django_extensions", - "tailwind", - "django_browser_reload" + "django_filters", + "channels_redis", ] DJANGO_CORE_APPS = [ + "daphne", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", + "django.contrib.sites", "django.contrib.staticfiles", ] -INSTALLED_APPS = DJANGO_CORE_APPS + CHATBOX_APPS + THIRD_PARTY_APPS +INSTALLED_APPS = DJANGO_CORE_APPS + THIRD_PARTY_APPS + CHATBOX_APPS AUTH_USER_MODEL = "user.User" ACCOUNT_USER_MODEL_USERNAME_FIELD = None ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_USERNAME_REQUIRED = False -ACCOUNT_AUTHENTICATION_METHOD = 'email' +ACCOUNT_AUTHENTICATION_METHOD = "email" LOGIN_REDIRECT_URL = "/profile" -# AllAuth -ACCOUNT_FORMS = { - 'login': 'apps.user.forms.CustomLoginForm', - 'signup': "apps.user.forms.CustomSignupForm", - 'add_email': 'allauth.account.forms.AddEmailForm', - 'change_password': 'allauth.account.forms.ChangePasswordForm', - 'set_password': 'allauth.account.forms.SetPasswordForm', - 'reset_password': 'allauth.account.forms.ResetPasswordForm', - 'reset_password_from_key': 'allauth.account.forms.ResetPasswordKeyForm', - 'disconnect': 'allauth.socialaccount.forms.DisconnectForm', -} - MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", + "corsheaders.middleware.CorsMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", - "django_browser_reload.middleware.BrowserReloadMiddleware", + "config.middlewares.add_headers.AddHeaders", + "config.middlewares.request_middleware.RequestMiddleware", ] ROOT_URLCONF = "config.urls" +# Rest Framework +REST_FRAMEWORK = { + "EXCEPTION_HANDLER": "config.exceptions.custom_exception_handler", + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.AllowAny", + ], + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework.authentication.BasicAuthentication", + "rest_framework.authentication.SessionAuthentication", + ), + "DEFAULT_RENDERER_CLASSES": [ + "config.renderer.CustomRenderer", + "djangorestframework_camel_case.render.CamelCaseJSONRenderer", + ], + "DEFAULT_PARSER_CLASSES": [ + "djangorestframework_camel_case.parser.CamelCaseJSONParser", + ], + "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.AcceptHeaderVersioning", + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 20, + "TEST_REQUEST_DEFAULT_FORMAT": "json", + "DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",), + "DATE_INPUT_FORMATS": ["iso-8601", "%Y-%m-%dT%H:%M:%S.%fZ"], +} + +# CORS +# CORS_ALLOWED_ORIGINS = ["http://localhost:3000", "127.0.0.1:8000"] + +CORS_ALLOW_ALL_ORIGINS = True + +CORS_ALLOW_HEADERS = list(default_headers) + [ + "version", + "X-ChatBox-Access-Key", + "X-ChatBox-Secret-Key", +] + +# CSRF_TRUSTED_ORIGINS = [ +# "http://localhost:3000", +# ] + +# SESSION +SESSION_COOKIE_AGE = 3600 # in seconds +SESSION_SAVE_EVERY_REQUEST = True + TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", @@ -120,15 +150,15 @@ ] WSGI_APPLICATION = "config.wsgi.application" - -# Authentication Backend -AUTHENTICATION_BACKENDS = [ - # Needed to login by username in Django admin, regardless of `allauth` - 'django.contrib.auth.backends.ModelBackend', - - # `allauth` specific authentication methods, such as login by e-mail - 'allauth.account.auth_backends.AuthenticationBackend', -] +ASGI_APPLICATION = "config.asgi.application" +CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels_redis.core.RedisChannelLayer", + "CONFIG": { + "hosts": [("127.0.0.1", 6379)], + }, + }, +} # Database @@ -136,9 +166,15 @@ DATABASES = { "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", - } + "ENGINE": "django.db.backends.mysql", + "HOST": os.environ.get("DB_HOST"), + "PORT": os.environ.get("DB_PORT"), + "NAME": os.environ.get("DB_NAME"), + "USER": os.environ.get("DB_USER"), + "PASSWORD": os.environ.get("DB_PASSWORD"), + "CONN_MAX_AGE": 60 * 10, # 10 minutes + "init_command": "SET sql_mode='STRICT_TRANS_TABLES'", + }, } @@ -160,13 +196,14 @@ }, ] -TAILWIND_APP_NAME = "theme" - -INTERNAL_IPS = [ - "127.0.0.1", -] - -SITE_ID = 1 +# Email Backend +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_HOST = "smtp.gmail.com" +EMAIL_USE_TLS = True +EMAIL_PORT = 587 +EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER") +EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD") +DEFAULT_FROM_EMAIL = EMAIL_HOST_USER # Internationalization # https://docs.djangoproject.com/en/4.1/topics/i18n/ @@ -191,7 +228,7 @@ STATIC_URL = "static/" # STATIC_ROOT = os.path.join(BASE_DIR, "static") -# STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),) +STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),) # Default primary key field type # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field diff --git a/config/urls.py b/config/urls.py index e95a729..fcab4ff 100644 --- a/config/urls.py +++ b/config/urls.py @@ -24,7 +24,5 @@ def health_check_view(request): urlpatterns = [ path("health-check", health_check_view, name="health-check"), - path("", include("apps.user.html_urls")), re_path(r"^api/", include("config.api_urls_v1")), - path("__reload__/", include("django_browser_reload.urls")), ] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d3da497 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: "3.8" +services: + redis-docker: + image: chatbox-redis + command: redis-server --port 6379 + restart: always + container_name: chatbox-redis + labels: + - "name=chatbox-redis" + volumes: + - /Users/jisoo/Desktop/dev/playidea/ChatBox/ChatBox-API/redis/redis_data:/data + ports: + - 6379:6379 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index df93731..1381f34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,8 +13,8 @@ coreschema==0.0.4 cryptography==38.0.4 daphne==4.0.0 defusedxml==0.7.1 -Django==4.1.4 -django-allauth==0.51.0 +Django==4.1.5 +django-allauth==0.52.0 django-channels==0.7.0 djangorestframework==3.14.0 djangorestframework-camel-case==1.3.0 @@ -30,6 +30,7 @@ mypy-extensions==0.4.3 oauthlib==3.2.2 packaging==22.0 pathspec==0.10.3 +Pillow==9.4.0 platformdirs==2.6.0 pyasn1==0.4.8 pyasn1-modules==0.2.8 @@ -53,3 +54,8 @@ typing_extensions==4.4.0 uritemplate==4.1.1 urllib3==1.26.13 zope.interface==5.5.2 + +djangochannelsrestframework~=1.1.0 +channels~=4.0.0 +shortuuid~=1.0.11 +redis~=4.4.0 \ No newline at end of file