mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2024-11-24 04:23:43 +03:00
Compare commits
No commits in common. "513de19a16fff716a0d7089977c3b294ef375177" and "0a0714f969f8a7d9c948672f796d443768d828d2" have entirely different histories.
513de19a16
...
0a0714f969
|
@ -1,46 +1,20 @@
|
||||||
from importlib import import_module
|
|
||||||
|
|
||||||
from channels.db import database_sync_to_async
|
from channels.db import database_sync_to_async
|
||||||
from channels.generic.websocket import AsyncJsonWebsocketConsumer, JsonWebsocketConsumer
|
from channels.generic.websocket import AsyncJsonWebsocketConsumer, JsonWebsocketConsumer
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.sessions.models import Session
|
|
||||||
|
|
||||||
from akarpov.common.jwt import read_jwt
|
from akarpov.common.jwt import read_jwt
|
||||||
from akarpov.users.models import User
|
|
||||||
|
|
||||||
engine = import_module(settings.SESSION_ENGINE)
|
|
||||||
sessionstore = engine.SessionStore
|
|
||||||
|
|
||||||
|
|
||||||
@database_sync_to_async
|
@database_sync_to_async
|
||||||
def get_user(headers):
|
def get_user(headers):
|
||||||
# WARNING headers type is bytes
|
# WARNING headers type is bytes
|
||||||
if (b"authorization" not in headers or not headers[b"authorization"]) and (
|
if b"authorization" not in headers or not headers[b"authorization"]:
|
||||||
b"cookie" not in headers or not headers[b"cookie"]
|
return False
|
||||||
):
|
|
||||||
return None
|
|
||||||
if b"authorization" in headers:
|
|
||||||
jwt = headers[b"authorization"].decode()
|
|
||||||
data = read_jwt(jwt)
|
|
||||||
if not data:
|
|
||||||
return None
|
|
||||||
payload = data
|
|
||||||
elif b"cookie" in headers:
|
|
||||||
cookies = dict([x.split("=") for x in headers[b"cookie"].decode().split("; ")])
|
|
||||||
if "sessionid" not in cookies:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
session = sessionstore(cookies["sessionid"])
|
|
||||||
user_id = session["_auth_user_id"]
|
|
||||||
except (Session.DoesNotExist, User.DoesNotExist, KeyError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
payload = {"id": user_id}
|
jwt = headers[b"authorization"].decode()
|
||||||
else:
|
payload = read_jwt(jwt)
|
||||||
payload = {}
|
|
||||||
|
|
||||||
if not payload or "id" not in payload:
|
if not payload or "id" not in payload:
|
||||||
return None
|
return False
|
||||||
|
|
||||||
return payload["id"]
|
return payload["id"]
|
||||||
|
|
||||||
|
@ -53,7 +27,7 @@ def __init__(self, app):
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
||||||
async def __call__(self, scope, receive, send):
|
async def __call__(self, scope, receive, send):
|
||||||
scope["user_id"] = await get_user(dict(scope["headers"]))
|
scope["user"] = await get_user(dict(scope["headers"]))
|
||||||
try:
|
try:
|
||||||
return await self.app(scope, receive, send)
|
return await self.app(scope, receive, send)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
import jwt
|
import jwt
|
||||||
import pytz
|
import pytz
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from jwt import DecodeError, ExpiredSignatureError, InvalidSignatureError
|
from jwt import ExpiredSignatureError, InvalidSignatureError
|
||||||
|
|
||||||
TIMEZONE = pytz.timezone("Europe/Moscow")
|
TIMEZONE = pytz.timezone("Europe/Moscow")
|
||||||
|
|
||||||
|
@ -24,10 +24,7 @@ def sign_jwt(data: dict, t_life: None | int = None) -> str:
|
||||||
|
|
||||||
def read_jwt(token: str) -> dict | bool:
|
def read_jwt(token: str) -> dict | bool:
|
||||||
"""reads jwt, validates it and return payload if correct"""
|
"""reads jwt, validates it and return payload if correct"""
|
||||||
try:
|
|
||||||
header_data = jwt.get_unverified_header(token)
|
header_data = jwt.get_unverified_header(token)
|
||||||
except DecodeError:
|
|
||||||
return False
|
|
||||||
secret = settings.SECRET_KEY
|
secret = settings.SECRET_KEY
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(token, key=secret, algorithms=[header_data["alg"]])
|
payload = jwt.decode(token, key=secret, algorithms=[header_data["alg"]])
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
class FileForm(forms.ModelForm):
|
class FileForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = File
|
model = File
|
||||||
fields = ["name", "private", "notify_user_on_view", "description"]
|
fields = ["name", "private", "description"]
|
||||||
|
|
||||||
|
|
||||||
class FolderForm(forms.ModelForm):
|
class FolderForm(forms.ModelForm):
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
# Generated by Django 4.2.5 on 2023-09-24 16:47
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("files", "0024_alter_file_options_alter_filereport_options_and_more"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="file",
|
|
||||||
name="notify_user_on_view",
|
|
||||||
field=models.BooleanField(
|
|
||||||
default=False, verbose_name="Receive notifications on file view"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="basefileitem",
|
|
||||||
name="parent",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="children",
|
|
||||||
to="files.folder",
|
|
||||||
verbose_name="Folder",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -26,7 +26,6 @@
|
||||||
|
|
||||||
class BaseFileItem(PolymorphicModel):
|
class BaseFileItem(PolymorphicModel):
|
||||||
parent = ForeignKey(
|
parent = ForeignKey(
|
||||||
verbose_name="Folder",
|
|
||||||
to="files.Folder",
|
to="files.Folder",
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
@ -75,11 +74,6 @@ class File(BaseFileItem, TimeStampedModel, ShortLinkModel, UserHistoryModel):
|
||||||
description = TextField(blank=True, null=True)
|
description = TextField(blank=True, null=True)
|
||||||
file_type = CharField(max_length=255, null=True, blank=True)
|
file_type = CharField(max_length=255, null=True, blank=True)
|
||||||
|
|
||||||
# extra settings
|
|
||||||
notify_user_on_view = BooleanField(
|
|
||||||
"Receive notifications on file view", default=False
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def file_name(self):
|
def file_name(self):
|
||||||
return self.file.path.split("/")[-1]
|
return self.file.path.split("/")[-1]
|
||||||
|
|
|
@ -1,3 +1 @@
|
||||||
from . import doc, docx, json, odt, pdf, zip # noqa
|
from . import doc, docx, json, odt, pdf, zip # noqa
|
||||||
|
|
||||||
# TODO: add gzip, xlsx
|
|
||||||
|
|
|
@ -34,7 +34,6 @@
|
||||||
from akarpov.files.services.folders import delete_folder
|
from akarpov.files.services.folders import delete_folder
|
||||||
from akarpov.files.services.preview import get_base_meta
|
from akarpov.files.services.preview import get_base_meta
|
||||||
from akarpov.files.tables import FileTable
|
from akarpov.files.tables import FileTable
|
||||||
from akarpov.notifications.services import send_notification
|
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
@ -149,16 +148,6 @@ def dispatch(self, request, *args, **kwargs):
|
||||||
if "bot" in useragent:
|
if "bot" in useragent:
|
||||||
if file.file_type and file.file_type.split("/")[0] == "image":
|
if file.file_type and file.file_type.split("/")[0] == "image":
|
||||||
return HttpResponseRedirect(file.file.url)
|
return HttpResponseRedirect(file.file.url)
|
||||||
else:
|
|
||||||
if file.notify_user_on_view:
|
|
||||||
if self.request.user != file.user:
|
|
||||||
send_notification(
|
|
||||||
"File view",
|
|
||||||
f"File {file.name} was opened",
|
|
||||||
"site",
|
|
||||||
user_id=file.user.id,
|
|
||||||
conformation=True,
|
|
||||||
)
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
from akarpov.notifications.models import Notification
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Notification)
|
|
||||||
class NotificationAdmin(admin.ModelAdmin):
|
|
||||||
list_filter = ["provider"]
|
|
|
@ -1,12 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationsConfig(AppConfig):
|
|
||||||
name = "akarpov.notifications"
|
|
||||||
verbose_name = "Notifications"
|
|
||||||
|
|
||||||
def ready(self):
|
|
||||||
try:
|
|
||||||
import akarpov.notifications.signals # noqa F401
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
|
@ -1,56 +0,0 @@
|
||||||
# Generated by Django 4.2.5 on 2023-09-20 10:52
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django_extensions.db.fields
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = []
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Notification",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"created",
|
|
||||||
django_extensions.db.fields.CreationDateTimeField(
|
|
||||||
auto_now_add=True, verbose_name="created"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"modified",
|
|
||||||
django_extensions.db.fields.ModificationDateTimeField(
|
|
||||||
auto_now=True, verbose_name="modified"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("title", models.CharField(max_length=255)),
|
|
||||||
("body", models.TextField(blank=True, max_length=5000, null=True)),
|
|
||||||
(
|
|
||||||
"provider",
|
|
||||||
models.CharField(
|
|
||||||
choices=[
|
|
||||||
("akarpov.notifications.providers.site", "site"),
|
|
||||||
("akarpov.notifications.providers.email", "email"),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("meta", models.JSONField(null=True)),
|
|
||||||
("delivered", models.BooleanField(default=False)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"get_latest_by": "modified",
|
|
||||||
"abstract": False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,17 +0,0 @@
|
||||||
from django.db import models
|
|
||||||
from django_extensions.db.models import TimeStampedModel
|
|
||||||
|
|
||||||
|
|
||||||
class Notification(TimeStampedModel):
|
|
||||||
class NotificationProviders(models.TextChoices):
|
|
||||||
site = "akarpov.notifications.providers.site", "site"
|
|
||||||
email = "akarpov.notifications.providers.email", "email"
|
|
||||||
|
|
||||||
title = models.CharField(max_length=255)
|
|
||||||
body = models.TextField(max_length=5000, null=True, blank=True)
|
|
||||||
provider = models.CharField(choices=NotificationProviders.choices)
|
|
||||||
meta = models.JSONField(null=True)
|
|
||||||
delivered = models.BooleanField(default=False)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.title
|
|
|
@ -1 +0,0 @@
|
||||||
from .send import send_notification # noqa
|
|
|
@ -1,34 +0,0 @@
|
||||||
from django.conf import settings
|
|
||||||
from django.core.mail import send_mail
|
|
||||||
from django.template.loader import render_to_string
|
|
||||||
|
|
||||||
from akarpov.notifications.models import Notification
|
|
||||||
from akarpov.users.models import User
|
|
||||||
|
|
||||||
|
|
||||||
def send_notification(notification: Notification) -> bool:
|
|
||||||
if not notification.meta or all(
|
|
||||||
["email" not in notification.meta, "user_id" not in notification.meta]
|
|
||||||
):
|
|
||||||
raise KeyError(
|
|
||||||
f"can't send notification {notification.id}, email/user_id is not found"
|
|
||||||
)
|
|
||||||
if "email" in notification.meta:
|
|
||||||
email = notification.meta["email"]
|
|
||||||
username = ""
|
|
||||||
else:
|
|
||||||
user = User.objects.get(id=notification.meta["user_id"])
|
|
||||||
email = user.email
|
|
||||||
username = user.username
|
|
||||||
message = render_to_string(
|
|
||||||
"email/notification.html", {"username": username, "body": notification.body}
|
|
||||||
)
|
|
||||||
send_mail(
|
|
||||||
notification.title,
|
|
||||||
notification.body,
|
|
||||||
settings.EMAIL_FROM,
|
|
||||||
[email],
|
|
||||||
fail_silently=False,
|
|
||||||
html_message=message,
|
|
||||||
)
|
|
||||||
return True
|
|
|
@ -1,8 +0,0 @@
|
||||||
"""
|
|
||||||
Notifications site provider
|
|
||||||
meta params:
|
|
||||||
- user_id: bool, required
|
|
||||||
- conformation: bool, optional
|
|
||||||
"""
|
|
||||||
|
|
||||||
from .send import send_notification # noqa
|
|
|
@ -1,9 +0,0 @@
|
||||||
from rest_framework import serializers
|
|
||||||
|
|
||||||
from akarpov.notifications.models import Notification
|
|
||||||
|
|
||||||
|
|
||||||
class SiteNotificationSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Notification
|
|
||||||
fields = ["title", "body", "created", "delivered"]
|
|
|
@ -1,19 +0,0 @@
|
||||||
from rest_framework import generics, permissions
|
|
||||||
|
|
||||||
from akarpov.common.api import StandardResultsSetPagination
|
|
||||||
from akarpov.notifications.models import Notification
|
|
||||||
from akarpov.notifications.providers.site.api.serializers import (
|
|
||||||
SiteNotificationSerializer,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ListNotificationsAPIView(generics.ListAPIView):
|
|
||||||
permission_classes = [permissions.IsAuthenticated]
|
|
||||||
serializer_class = SiteNotificationSerializer
|
|
||||||
pagination_class = StandardResultsSetPagination
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return Notification.objects.filter(meta__user_id=self.request.user.id)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: add read notification url here
|
|
|
@ -1,28 +0,0 @@
|
||||||
from akarpov.common.channels import BaseConsumer
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationsConsumer(BaseConsumer):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(args, kwargs)
|
|
||||||
self.room_group_name = None
|
|
||||||
|
|
||||||
async def connect(self):
|
|
||||||
self.room_group_name = f"notifications_{self.scope['user_id']}"
|
|
||||||
|
|
||||||
await self.accept()
|
|
||||||
await self.channel_layer.group_add(self.room_group_name, self.channel_name)
|
|
||||||
if not self.scope["user_id"]:
|
|
||||||
await self.send_error("Authorization is required")
|
|
||||||
await self.disconnect(close_code=None)
|
|
||||||
return
|
|
||||||
|
|
||||||
async def disconnect(self, close_code):
|
|
||||||
await self.channel_layer.group_discard(self.room_group_name, self.channel_name)
|
|
||||||
await self.close()
|
|
||||||
|
|
||||||
async def receive_json(self, content: dict, **kwargs):
|
|
||||||
return content
|
|
||||||
|
|
||||||
async def notification(self, event):
|
|
||||||
data = event["data"]
|
|
||||||
await self.send_json(data)
|
|
|
@ -1,30 +0,0 @@
|
||||||
from asgiref.sync import async_to_sync
|
|
||||||
from channels.layers import get_channel_layer
|
|
||||||
|
|
||||||
from akarpov.notifications.models import Notification
|
|
||||||
from akarpov.notifications.providers.site.api.serializers import (
|
|
||||||
SiteNotificationSerializer,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def send_notification(notification: Notification) -> bool:
|
|
||||||
if (
|
|
||||||
not notification.meta
|
|
||||||
or "user_id" not in notification.meta
|
|
||||||
or not notification.meta["user_id"]
|
|
||||||
):
|
|
||||||
raise KeyError(
|
|
||||||
f"can't send notification {notification.id}, user_id is not found"
|
|
||||||
)
|
|
||||||
channel_layer = get_channel_layer()
|
|
||||||
async_to_sync(channel_layer.group_send)(
|
|
||||||
f"notifications_{notification.meta['user_id']}",
|
|
||||||
{
|
|
||||||
"type": "notification",
|
|
||||||
"data": SiteNotificationSerializer().to_representation(notification),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if "conformation" in notification.meta and notification.meta["conformation"]:
|
|
||||||
# no view conformation required, only pop up on site
|
|
||||||
return False
|
|
||||||
return True
|
|
|
@ -1,8 +0,0 @@
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
from akarpov.notifications.providers.site.api.views import ListNotificationsAPIView
|
|
||||||
|
|
||||||
app_name = "notifications:site"
|
|
||||||
urlpatterns = [
|
|
||||||
path("", ListNotificationsAPIView.as_view(), name="list"),
|
|
||||||
]
|
|
|
@ -1,8 +0,0 @@
|
||||||
from django.urls import include, path
|
|
||||||
|
|
||||||
app_name = "notifications"
|
|
||||||
urlpatterns = [
|
|
||||||
path(
|
|
||||||
"site/", include("akarpov.notifications.providers.site.urls", namespace="site")
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,7 +0,0 @@
|
||||||
from akarpov.notifications.tasks import run_create_send_notification
|
|
||||||
|
|
||||||
|
|
||||||
def send_notification(title: str, body: str, provider: str, **kwargs):
|
|
||||||
run_create_send_notification.apply_async(
|
|
||||||
kwargs={"title": title, "body": body, "provider": provider} | kwargs
|
|
||||||
)
|
|
|
@ -1,11 +0,0 @@
|
||||||
from django.db.models.signals import post_save
|
|
||||||
from django.dispatch import receiver
|
|
||||||
|
|
||||||
from akarpov.notifications.models import Notification
|
|
||||||
from akarpov.notifications.tasks import run_send_notification
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Notification)
|
|
||||||
def notification_create(sender, instance: Notification, created, **kwargs):
|
|
||||||
if created:
|
|
||||||
run_send_notification.apply_async(kwargs={"pk": instance.pk}, countdown=2)
|
|
|
@ -1,32 +0,0 @@
|
||||||
from importlib import import_module
|
|
||||||
|
|
||||||
from celery import shared_task
|
|
||||||
|
|
||||||
from akarpov.notifications.models import Notification
|
|
||||||
|
|
||||||
providers = {x[1]: x[0] for x in Notification.NotificationProviders.choices}
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
|
||||||
def run_send_notification(pk):
|
|
||||||
instance = Notification.objects.get(pk=pk)
|
|
||||||
provider = import_module(instance.provider)
|
|
||||||
instance.delivered = provider.send_notification(instance)
|
|
||||||
instance.save()
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task
|
|
||||||
def run_create_send_notification(title: str, body: str, provider: str, **kwargs):
|
|
||||||
if provider != "*" and provider not in providers:
|
|
||||||
raise ValueError(f"no such provider: {provider}")
|
|
||||||
if provider == "*":
|
|
||||||
for provider in providers:
|
|
||||||
Notification.objects.create(
|
|
||||||
title=title, body=body, provider=providers[provider], meta=kwargs
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
Notification.objects.create(
|
|
||||||
title=title, body=body, provider=providers[provider], meta=kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
|
@ -140,42 +140,5 @@
|
||||||
|
|
||||||
{% block inline_javascript %}
|
{% block inline_javascript %}
|
||||||
{% endblock inline_javascript %}
|
{% endblock inline_javascript %}
|
||||||
{% if request.user.is_authenticated %}
|
|
||||||
<script>
|
|
||||||
{# TODO: add automatic socket host retrieve #}
|
|
||||||
let socket = new WebSocket(`ws://127.0.0.1:8000/ws/notifications/`);
|
|
||||||
|
|
||||||
function sleep(ms) {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
let fn = async function(event) {
|
|
||||||
let data = JSON.parse(event.data)
|
|
||||||
console.log(data)
|
|
||||||
alert(data.body)
|
|
||||||
{# TODO add pretty pop up #}
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.onmessage = fn
|
|
||||||
socket.onclose = async function(event) {
|
|
||||||
console.log("Notifications socket disconnected, reconnecting...")
|
|
||||||
let socketClosed = true;
|
|
||||||
await sleep(5000)
|
|
||||||
while (socketClosed) {
|
|
||||||
{# TODO: reconnect socket here #}
|
|
||||||
try {
|
|
||||||
let cl = socket.onclose
|
|
||||||
socket = new WebSocket(`ws://127.0.0.1:8000/ws/notifications/`);
|
|
||||||
socket.onmessage = fn
|
|
||||||
socket.onclose = cl
|
|
||||||
socketClosed = false
|
|
||||||
} catch (e) {
|
|
||||||
console.log("Can't connect to socket, reconnecting...")
|
|
||||||
await sleep(1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
<h3>Hello, {% if username %}{{ username }}{% endif %}!</h3>
|
|
||||||
<p>You are seeing this message, because you received notification from <a href="https://akarpov.ru">akarpov.ru</a></p>
|
|
||||||
|
|
||||||
<p>{{ body }}</p>
|
|
||||||
|
|
||||||
<p>If you don't want to receive notifications via email, or got it by excitement <a href="{# TODO: add unsubscribe #}">press here</a></p>
|
|
|
@ -56,7 +56,7 @@
|
||||||
</nav>
|
</nav>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if request.user.is_authenticated and is_folder_owner and not folder_slug %}
|
{% if request.user.is_authenticated and is_folder_owner %}
|
||||||
<div class="d-flex justify-content-end me-5 col">
|
<div class="d-flex justify-content-end me-5 col">
|
||||||
<a class="me-5" href="{% url 'files:table' %}">table view</a>
|
<a class="me-5" href="{% url 'files:table' %}">table view</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -26,11 +26,11 @@
|
||||||
|
|
||||||
{% block inline_javascript %}
|
{% block inline_javascript %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
let md5 = "",
|
var md5 = "",
|
||||||
csrf = $("input[name='csrfmiddlewaretoken']")[0].value,
|
csrf = $("input[name='csrfmiddlewaretoken']")[0].value,
|
||||||
form_data = [{"name": "csrfmiddlewaretoken", "value": csrf}];
|
form_data = [{"name": "csrfmiddlewaretoken", "value": csrf}];
|
||||||
function calculate_md5(file, chunk_size) {
|
function calculate_md5(file, chunk_size) {
|
||||||
let slice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,
|
var slice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,
|
||||||
chunks = chunks = Math.ceil(file.size / chunk_size),
|
chunks = chunks = Math.ceil(file.size / chunk_size),
|
||||||
current_chunk = 0,
|
current_chunk = 0,
|
||||||
spark = new SparkMD5.ArrayBuffer();
|
spark = new SparkMD5.ArrayBuffer();
|
||||||
|
@ -44,9 +44,9 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
function read_next_chunk() {
|
function read_next_chunk() {
|
||||||
let reader = new FileReader();
|
var reader = new FileReader();
|
||||||
reader.onload = onload;
|
reader.onload = onload;
|
||||||
let start = current_chunk * chunk_size,
|
var start = current_chunk * chunk_size,
|
||||||
end = Math.min(start + chunk_size, file.size);
|
end = Math.min(start + chunk_size, file.size);
|
||||||
reader.readAsArrayBuffer(slice.call(file, start, end));
|
reader.readAsArrayBuffer(slice.call(file, start, end));
|
||||||
};
|
};
|
||||||
|
@ -71,7 +71,7 @@
|
||||||
{"name": "upload_id", "value": data.result.upload_id}
|
{"name": "upload_id", "value": data.result.upload_id}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let progress = parseInt(data.loaded / data.total * 100.0, 10);
|
var progress = parseInt(data.loaded / data.total * 100.0, 10);
|
||||||
$("#progress").text(Array(progress).join("=") + "> " + progress + "%");
|
$("#progress").text(Array(progress).join("=") + "> " + progress + "%");
|
||||||
},
|
},
|
||||||
done: function (e, data) { // Called when the file has completely uploaded
|
done: function (e, data) { // Called when the file has completely uploaded
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
from drf_spectacular.types import OpenApiTypes
|
|
||||||
from drf_spectacular.utils import extend_schema
|
from drf_spectacular.utils import extend_schema
|
||||||
from rest_framework import generics, permissions, status, views
|
from rest_framework import generics, permissions, status, views
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from akarpov.common.api import SmallResultsSetPagination
|
from akarpov.common.api import SmallResultsSetPagination
|
||||||
from akarpov.common.jwt import sign_jwt
|
|
||||||
from akarpov.users.api.serializers import (
|
from akarpov.users.api.serializers import (
|
||||||
UserEmailVerification,
|
UserEmailVerification,
|
||||||
UserFullPublicInfoSerializer,
|
UserFullPublicInfoSerializer,
|
||||||
|
@ -29,14 +27,6 @@ def post(self, request, *args, **kwargs):
|
||||||
return self.create(request, *args, **kwargs)
|
return self.create(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class GenerateUserJWTTokenAPIView(generics.GenericAPIView):
|
|
||||||
permission_classes = [permissions.IsAuthenticated]
|
|
||||||
|
|
||||||
@extend_schema(responses={200: OpenApiTypes.STR})
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
return Response(data=sign_jwt(data={"id": self.request.user.id}))
|
|
||||||
|
|
||||||
|
|
||||||
class UserEmailValidationAPIViewSet(views.APIView):
|
class UserEmailValidationAPIViewSet(views.APIView):
|
||||||
"""Receives token from email and activates user"""
|
"""Receives token from email and activates user"""
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,6 @@ ARG APP_HOME=/app
|
||||||
ENV PYTHONUNBUFFERED 1
|
ENV PYTHONUNBUFFERED 1
|
||||||
ENV PYTHONDONTWRITEBYTECODE 1
|
ENV PYTHONDONTWRITEBYTECODE 1
|
||||||
ENV BUILD_ENV ${BUILD_ENVIRONMENT}
|
ENV BUILD_ENV ${BUILD_ENVIRONMENT}
|
||||||
ENV POETRY_VERSION 1.4.2
|
|
||||||
|
|
||||||
WORKDIR ${APP_HOME}
|
WORKDIR ${APP_HOME}
|
||||||
|
|
||||||
|
@ -35,12 +34,14 @@ RUN addgroup --system django \
|
||||||
|
|
||||||
|
|
||||||
# Install required system dependencies
|
# Install required system dependencies
|
||||||
RUN apt-get update && \
|
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||||
apt-get install -y build-essential libpq-dev gettext libmagic-dev libjpeg-dev zlib1g-dev && \
|
# psycopg2 dependencies
|
||||||
# Dependencies for file preview generation
|
libpq-dev \
|
||||||
apt-get install -y webp libimage-exiftool-perl libmagickwand-dev ffmpeg libgdal-dev && \
|
# Translations dependencies
|
||||||
apt-get purge -y --auto-remove -o APT:AutoRemove:RecommendsImportant=false && \
|
gettext \
|
||||||
rm -rf /var/lib/apt/lists/*
|
# cleaning up unused files
|
||||||
|
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
||||||
RUN pip install "poetry==$POETRY_VERSION"
|
RUN pip install "poetry==$POETRY_VERSION"
|
||||||
|
|
|
@ -63,11 +63,6 @@ http:
|
||||||
servers:
|
servers:
|
||||||
- url: http://django:5000
|
- url: http://django:5000
|
||||||
|
|
||||||
redirect:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: http://redirect:3000
|
|
||||||
|
|
||||||
flower:
|
flower:
|
||||||
loadBalancer:
|
loadBalancer:
|
||||||
servers:
|
servers:
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from rest_framework.authtoken.views import obtain_auth_token
|
from rest_framework.authtoken.views import obtain_auth_token
|
||||||
|
|
||||||
from akarpov.users.api.views import GenerateUserJWTTokenAPIView, UserRegisterAPIViewSet
|
from akarpov.users.api.views import UserRegisterAPIViewSet
|
||||||
|
|
||||||
app_name = "api"
|
app_name = "api"
|
||||||
|
|
||||||
|
@ -16,7 +16,6 @@
|
||||||
name="user_register_api",
|
name="user_register_api",
|
||||||
),
|
),
|
||||||
path("token/", obtain_auth_token),
|
path("token/", obtain_auth_token),
|
||||||
path("jwt/", GenerateUserJWTTokenAPIView.as_view()),
|
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -24,10 +23,6 @@
|
||||||
"users/",
|
"users/",
|
||||||
include("akarpov.users.api.urls", namespace="users"),
|
include("akarpov.users.api.urls", namespace="users"),
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"notifications/",
|
|
||||||
include("akarpov.notifications.providers.urls", namespace="notifications"),
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"blog/",
|
"blog/",
|
||||||
include("akarpov.blog.api.urls", namespace="blog"),
|
include("akarpov.blog.api.urls", namespace="blog"),
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
from django.urls import re_path
|
from django.urls import re_path
|
||||||
|
|
||||||
from akarpov.music.consumers import RadioConsumer
|
from akarpov.music.consumers import RadioConsumer
|
||||||
from akarpov.notifications.providers.site.consumers import NotificationsConsumer
|
|
||||||
|
|
||||||
websocket_urlpatterns = [
|
websocket_urlpatterns = [
|
||||||
re_path(r"ws/radio/", RadioConsumer.as_asgi()),
|
re_path(r"ws/radio/", RadioConsumer.as_asgi()),
|
||||||
re_path(r"ws/notifications/", NotificationsConsumer.as_asgi()),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -155,11 +155,10 @@
|
||||||
"akarpov.files",
|
"akarpov.files",
|
||||||
"akarpov.music",
|
"akarpov.music",
|
||||||
"akarpov.gallery",
|
"akarpov.gallery",
|
||||||
"akarpov.tools.qr",
|
|
||||||
"akarpov.pipeliner",
|
"akarpov.pipeliner",
|
||||||
"akarpov.notifications",
|
|
||||||
"akarpov.test_platform",
|
"akarpov.test_platform",
|
||||||
"akarpov.tools.shortener",
|
"akarpov.tools.shortener",
|
||||||
|
"akarpov.tools.qr",
|
||||||
"akarpov.tools.promocodes",
|
"akarpov.tools.promocodes",
|
||||||
]
|
]
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
|
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
"DJANGO_SECRET_KEY",
|
"DJANGO_SECRET_KEY",
|
||||||
default="Lm4alUqtub6qQT4MnV4NmtXQP02RCBtmGj1bJhyDho07Bkjk9WFZxGtwpnLNQGJQ",
|
default="Lm4alUqtub6qQT4MnV4NmtXQP02RCBtmGj1bJhyDho07Bkjk9WFZxGtwpnLNQGJQ",
|
||||||
)
|
)
|
||||||
TOKEN_EXP = 24 * 60 * 60
|
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
|
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
|
||||||
ALLOWED_HOSTS = ["*"]
|
ALLOWED_HOSTS = ["*"]
|
||||||
CSRF_TRUSTED_ORIGINS = ["http://127.0.0.1", "https://*.akarpov.ru"]
|
CSRF_TRUSTED_ORIGINS = ["http://127.0.0.1", "https://*.akarpov.ru"]
|
||||||
|
@ -21,7 +20,6 @@
|
||||||
EMAIL_HOST = env("EMAIL_HOST", default="mailhog")
|
EMAIL_HOST = env("EMAIL_HOST", default="mailhog")
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#email-port
|
# https://docs.djangoproject.com/en/dev/ref/settings/#email-port
|
||||||
EMAIL_PORT = env("EMAIL_PORT", default="1025")
|
EMAIL_PORT = env("EMAIL_PORT", default="1025")
|
||||||
EMAIL_FROM = env("EMAIL_FROM", default="noreply@akarpov.ru")
|
|
||||||
|
|
||||||
# WhiteNoise
|
# WhiteNoise
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
|
@ -24,6 +24,7 @@ services:
|
||||||
- ./.envs/.production/.postgres
|
- ./.envs/.production/.postgres
|
||||||
command: /start
|
command: /start
|
||||||
|
|
||||||
|
|
||||||
redirect:
|
redirect:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
|
@ -34,30 +35,16 @@ services:
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
- postgres
|
||||||
- redis
|
- redis
|
||||||
|
- mailhog
|
||||||
volumes:
|
volumes:
|
||||||
- type: bind
|
- .:/app:z
|
||||||
source: /var/www/media/
|
|
||||||
target: /app/akarpov/media/
|
|
||||||
env_file:
|
env_file:
|
||||||
- ./.envs/.production/.django
|
- ./.envs/.production/.django
|
||||||
- ./.envs/.production/.postgres
|
- ./.envs/.production/.postgres
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
command: /start-redirect
|
command: /start-redirect
|
||||||
|
|
||||||
traefik:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: ./compose/production/traefik/Dockerfile
|
|
||||||
image: akarpov_production_traefik
|
|
||||||
depends_on:
|
|
||||||
- django
|
|
||||||
- redirect
|
|
||||||
volumes:
|
|
||||||
- production_traefik:/etc/traefik/acme
|
|
||||||
ports:
|
|
||||||
- "0.0.0.0:80:80"
|
|
||||||
- "0.0.0.0:443:443"
|
|
||||||
- "0.0.0.0:3000:3000"
|
|
||||||
- "0.0.0.0:5555:5555"
|
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
build:
|
build:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user