diff --git a/config/firebase.py b/config/firebase.py new file mode 100644 index 0000000..504a113 --- /dev/null +++ b/config/firebase.py @@ -0,0 +1,10 @@ +import firebase_admin +from firebase_admin import credentials, messaging +from django.conf import settings +import os + +firebase_cred_path = os.path.join(settings.BASE_DIR, 'config/firebase/firebase-key.json') + +if not firebase_admin._apps: + cred = credentials.Certificate(firebase_cred_path) + firebase_admin.initialize_app(cred) diff --git a/config/firebase/firebase-key.json b/config/firebase/firebase-key.json new file mode 100644 index 0000000..9117943 --- /dev/null +++ b/config/firebase/firebase-key.json @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "uyqur-9f7af", + "private_key_id": "708733567d3f54fc967b453ae010d96439d4d8e6", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCY/0PLBiteWmIx\nuYUoG4JIkGctHD+GDlGqnhePdpS7mMuVKHpVf0dCe0azLzj/gC3IONB+ueHI8Cod\nUunB4wgp+ExuemUX+dJ46IEZAI/PA1TbetG+zczzc316TFIrZKpVVOi9ExlLp67Y\nxsYLZFIx6ip/WK39BphxqdDRiKRZxRZ4GOLzJyQZ1dsmlZ/hjIReCEuO6Pd2/hKm\nsUT6XsNWpBhq+0GcphS4cZ0Z+XyHEMfACji+Z14vP0Eh0NXI1Lr+96yWqAm1ywoj\n3ugcFY6z75NEyrK7MNOb3APS6UiH1Uk3kmUPYj1QD1RIK11txEYlYpY8e/njQlUG\nS/oQAp5BAgMBAAECggEAFboyi3N91zFovn0FLvPxJZL0RBC96LDB4kP/Po1tg9Ko\nHq+X5+piWUued7XeF1LBrax713NYYCvTH0T2E2XFdAhh+lKBZs1AmZETPFS7F3/2\nnhCsFretQmmFSUfrZ2QtWF7timRa9EaE6x4XY+jET3hcvqb+Vm+IWKPwFsGb7W96\neuhVS1DhfFHTKy3jAkx0PRFlNqjiPa0TcXrcfDnta+Lak20ZnvQUbd5PF0jKn29A\nsNTg86wWpIqcEN2vnzSDk6I5jIKZ+BvRglIkp3ZqZW/iBDZZevhqdWCMUeFrmERb\nNKrKcZYtd0Mw0DBayYIkGV3nx0DQkdbj9QOZhqTh7QKBgQDRibyQrdcMycAK2YmF\nsadg9y5KjMbONIFPHFmCw+OHnISEj/m1puuVaVOH8pECFQOkONjFLWdL3rbQctNx\nOqVI/SBB/WkHlP9DapEfYHmmzap69Nc9jdfkTDKTFbXMbdfU+OWUAY0Qc6CqCTNi\nUwN8H8OeB0INDRYLVVAtcLZoTwKBgQC67AcaB7/MkrJWgOUX5iNlwoMhti+rYESQ\nv6s9qHuu338mGR+4PkxMCflj+Gc+Om/wAbewAoRlJVdhRFrtigc1taeiFfuFwM4m\n/dvarX8VUPVsstpodeJQ/caa8wKl8nABGnMT/mJ5GJkFv6NRSP4xmKQxFhm4ZE/x\nA5jKKuFcbwKBgEkOnwJKukopJZ4izsIgeN1kEW3Iu6A1ykgM+GCRcAleVw3pLQVa\n15TWjls+BbUWIpjlgR7uf6+CTXdMMdCuw+Y460BW3IHaP04AH+0ys/emiaQpLcq2\nY+mjb5a84RAP1ErbJSB/kfGEfyYJ4zKLAxIJ+ShmG291epQlALl3LQIdAoGBAI+n\nm+GeeQJA77xZfTe70BKBxgPfn40nBCr2kyVk2gFQlMhz4JPZlQuPUtJI8xe5E5Qx\nzbkAhj2x0BDZ1sPeI6JchIOmP1LRFd6TlSbf1d5NBQFQB1jm2FMEZmFpR+y/gOLo\nL+76vzVv+RKY8GwlG+6D8BQldwjmVyXUbNVa1S4TAoGAU7ycLnakKHSMnfULM+xL\n960MQ4TNzap8i6Ml8PMBjObpTU5k0pbjzhLHSLbocF100nLCck2Qb2s7d4/xuiMl\nwRqiqK/0iUEmfGC8bGBghIli6IAnIqkxzKbdx+TJCDN0eYhwMs8o6ljny+4we7Tc\nCLDsVraHoBGsfihDjSbHI5E=\n-----END PRIVATE KEY-----\n", + "client_email": "firebase-adminsdk-fbsvc@uyqur-9f7af.iam.gserviceaccount.com", + "client_id": "102992994110464968021", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40uyqur-9f7af.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +} diff --git a/config/settings/base.py b/config/settings/base.py index 2c9d2c2..ebf201a 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -1,6 +1,14 @@ from datetime import timedelta from pathlib import Path +from config.conf.celery import * +from config.conf.cors_headers import * +from config.conf.drf_yasg import * +from config.conf.jazzmin import * +from config.conf.logs import * +from config.conf.redis import * +from config.conf.rest_framework import * +from config.conf.rest_framework_simplejwt import * from config.env import env # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -11,95 +19,95 @@ BASE_DIR = Path(__file__).resolve().parent.parent.parent # See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = env.str('SECRET_KEY') +SECRET_KEY = env.str("SECRET_KEY") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = env.bool('DEBUG') +DEBUG = env.bool("DEBUG") -ALLOWED_HOSTS = env.list('ALLOWED_HOSTS') +ALLOWED_HOSTS = env.list("ALLOWED_HOSTS") # Application definition APPS = [ - 'core.apps.accounts', - 'core.apps.shared', - 'core.apps.company', - 'core.apps.wherehouse', - 'core.apps.products', - 'core.apps.projects', - 'core.apps.orders', - 'core.apps.finance', - 'core.apps.counterparty', - 'core.apps.notifications', + "core.apps.accounts", + "core.apps.shared", + "core.apps.company", + "core.apps.wherehouse", + "core.apps.products", + "core.apps.projects", + "core.apps.orders", + "core.apps.finance", + "core.apps.counterparty", + "core.apps.notifications", ] PACKAGES = [ - 'drf_yasg', - 'rest_framework', - 'rest_framework_simplejwt', - 'corsheaders', - 'cacheops', + "drf_yasg", + "rest_framework", + "rest_framework_simplejwt", + "corsheaders", + "cacheops", ] DJANGO_APPS = [ - 'jazzmin', - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', + "jazzmin", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", ] INSTALLED_APPS = [] INSTALLED_APPS += DJANGO_APPS -INSTALLED_APPS += PACKAGES +INSTALLED_APPS += PACKAGES INSTALLED_APPS += APPS 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.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", # 'silk.middleware.SilkyMiddleware', ] -ROOT_URLCONF = 'config.urls' +ROOT_URLCONF = "config.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'config.wsgi.application' +WSGI_APPLICATION = "config.wsgi.application" # Database # https://docs.djangoproject.com/en/5.2/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': env.str('POSTGRES_DB'), - 'USER': env.str('POSTGRES_USER'), - 'PASSWORD': env.str('POSTGRES_PASSWORD'), - 'HOST': env.str('POSTGRES_HOST'), - 'PORT': env.str('POSTGRES_PORT'), + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": env.str("POSTGRES_DB"), + "USER": env.str("POSTGRES_USER"), + "PASSWORD": env.str("POSTGRES_PASSWORD"), + "HOST": env.str("POSTGRES_HOST"), + "PORT": env.str("POSTGRES_PORT"), } } @@ -109,16 +117,16 @@ DATABASES = { AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -126,9 +134,9 @@ AUTH_PASSWORD_VALIDATORS = [ # Internationalization # https://docs.djangoproject.com/en/5.2/topics/i18n/ -LANGUAGE_CODE = 'uz' +LANGUAGE_CODE = "uz" -TIME_ZONE = 'Asia/Tashkent' +TIME_ZONE = "Asia/Tashkent" USE_I18N = True @@ -138,25 +146,21 @@ USE_TZ = False # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.2/howto/static-files/ -STATIC_URL = 'static/' -STATIC_ROOT = BASE_DIR / 'resources/static' -MEDIA_URL = 'media/' -MEDIA_ROOT = BASE_DIR / 'resources/media' +STATIC_URL = "static/" +STATIC_ROOT = BASE_DIR / "resources/static" +MEDIA_URL = "media/" +MEDIA_ROOT = BASE_DIR / "resources/media" # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -AUTH_USER_MODEL = 'accounts.User' +AUTH_USER_MODEL = "accounts.User" -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', env.str("SWAGGER_PROTOCOL", 'https')) +SECURE_PROXY_SSL_HEADER = ( + "HTTP_X_FORWARDED_PROTO", + env.str("SWAGGER_PROTOCOL", "https"), +) -from config.conf.rest_framework import * -from config.conf.rest_framework_simplejwt import * -from config.conf.logs import * -from config.conf.cors_headers import * -from config.conf.drf_yasg import * -from config.conf.jazzmin import * -from config.conf.celery import * -from config.conf.redis import * \ No newline at end of file +FCM_SERVER_KEY = env.str("FCM_SERVER_KEY") diff --git a/core/apps/notifications/migrations/0002_notification_type_alter_notification_unique_together.py b/core/apps/notifications/migrations/0002_notification_type_alter_notification_unique_together.py new file mode 100644 index 0000000..3c94fde --- /dev/null +++ b/core/apps/notifications/migrations/0002_notification_type_alter_notification_unique_together.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.4 on 2025-10-30 14:56 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('notifications', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='notification', + name='type', + field=models.CharField(choices=[('web', 'web'), ('mobile', 'mobile')], default='mobile', max_length=6), + ), + migrations.AlterUniqueTogether( + name='notification', + unique_together={('type', 'user', 'token')}, + ), + ] diff --git a/core/apps/notifications/models/notification.py b/core/apps/notifications/models/notification.py index b03cf56..d90ccae 100644 --- a/core/apps/notifications/models/notification.py +++ b/core/apps/notifications/models/notification.py @@ -3,7 +3,15 @@ from django.db import models from core.apps.shared.models import BaseModel from core.apps.accounts.models import User + class Notification(BaseModel): + type = models.CharField( + choices=[('web', 'web'), ('mobile', 'mobile')], + max_length=6, + default='mobile' + ) user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='notifications') token = models.CharField(max_length=255, unique=True) - \ No newline at end of file + + class Meta: + unique_together = ('type', 'user', 'token') \ No newline at end of file diff --git a/core/apps/notifications/serializers/notification.py b/core/apps/notifications/serializers/notification.py index 7188384..4b21072 100644 --- a/core/apps/notifications/serializers/notification.py +++ b/core/apps/notifications/serializers/notification.py @@ -7,5 +7,5 @@ class NotificationSerializer(serializers.ModelSerializer): class Meta: model = Notification fields = [ - 'token' + 'type', 'token' ] \ No newline at end of file diff --git a/core/apps/notifications/utils/notify_user.py b/core/apps/notifications/utils/notify_user.py index 83ef144..940d5f7 100644 --- a/core/apps/notifications/utils/notify_user.py +++ b/core/apps/notifications/utils/notify_user.py @@ -1,8 +1,11 @@ from core.apps.notifications.models import Notification -from core.apps.notifications.utils.send_notification import send_notification +from core.apps.notifications.utils.send_notification import send_notification, send_web_notification def notify_user(user, title, body, data): tokens = Notification.objects.filter(user=user) for token in tokens: - send_notification(token.token, title, body, data) \ No newline at end of file + if token.type == 'mobile': + send_notification(token.token, title, body, data) + if token.type == 'web': + send_web_notification(token.token, title, body, data) diff --git a/core/apps/notifications/utils/send_notification.py b/core/apps/notifications/utils/send_notification.py index f0dc668..4daada3 100644 --- a/core/apps/notifications/utils/send_notification.py +++ b/core/apps/notifications/utils/send_notification.py @@ -1,4 +1,8 @@ import requests +from firebase_admin import messaging + +from django.conf import settings + def send_notification(token, title, body, data=None): message = { @@ -11,6 +15,19 @@ def send_notification(token, title, body, data=None): response = requests.post( "https://exp.host/--/api/v2/push/send", json=message, - headers={"Content-Type": "application/json"} + headers={"Content-Type": "application/json"}, ) - return response.json() \ No newline at end of file + return response.json() + + +def send_web_notification(token, title, body, data=None): + message = messaging.MulticastMessage( + notification=messaging.Notification( + title=title, + body=body + ), + data=data or {}, + tokens=token, + ) + + response = messaging.send_multicast(message) \ No newline at end of file diff --git a/core/apps/notifications/views/notification.py b/core/apps/notifications/views/notification.py index 68ef8c9..8ad7c5a 100644 --- a/core/apps/notifications/views/notification.py +++ b/core/apps/notifications/views/notification.py @@ -16,7 +16,8 @@ class RegisterExpoPushToken(generics.GenericAPIView): if serializer.is_valid(): Notification.objects.get_or_create( user=request.user, - token=serializer.validated_data['token'] + token=serializer.validated_data['token'], + type=serializer.validated_data.get('type'), ) return Response({"message": "Token saqlandi"}, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index a97daeb..29375a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,22 @@ +aiohappyeyeballs==2.6.1 +aiohttp==3.13.2 +aiosignal==1.4.0 amqp==5.3.1 +anyio==4.11.0 asgiref==3.9.1 +attrs==25.4.0 billiard==4.2.1 +CacheControl==0.14.3 +cachetools==6.2.1 celery==5.5.3 certifi==2025.10.5 +cffi==2.0.0 charset-normalizer==3.4.4 click==8.2.1 click-didyoumean==0.3.1 click-plugins==1.1.1.2 click-repl==0.3.0 +cryptography==46.0.3 Django==5.2.4 django-cacheops==7.2 django-cors-headers==4.7.0 @@ -21,28 +30,58 @@ djangorestframework==3.16.0 djangorestframework_simplejwt==5.5.1 drf-yasg==1.21.10 exponent_server_sdk==2.2.0 +firebase_admin==7.1.0 +frozenlist==1.8.0 funcy==2.0 +google-api-core==2.28.1 +google-auth==2.42.0 +google-cloud-core==2.5.0 +google-cloud-firestore==2.21.0 +google-cloud-storage==3.4.1 +google-crc32c==1.7.1 +google-resumable-media==2.7.2 +googleapis-common-protos==1.71.0 gprof2dot==2025.4.14 +grpcio==1.76.0 +grpcio-status==1.76.0 gunicorn==23.0.0 h11==0.16.0 +h2==4.3.0 +hpack==4.1.0 +httpcore==1.0.9 +httpx==0.28.1 +hyperframe==6.1.0 idna==3.11 inflection==0.5.1 kombu==5.5.4 +msgpack==1.1.2 +multidict==6.7.0 packaging==25.0 pillow==11.3.0 prompt_toolkit==3.0.51 +propcache==0.4.1 +proto-plus==1.26.1 +protobuf==6.33.0 psycopg2-binary==2.9.10 +pyasn1==0.6.1 +pyasn1_modules==0.4.2 +pycparser==2.23 +pyfcm==2.1.0 PyJWT==2.10.1 python-dateutil==2.9.0.post0 pytz==2025.2 PyYAML==6.0.2 redis==6.2.0 requests==2.32.5 +rsa==4.9.1 six==1.17.0 +sniffio==1.3.1 sqlparse==0.5.3 +typing_extensions==4.15.0 tzdata==2025.2 uritemplate==4.2.0 urllib3==2.5.0 uvicorn==0.35.0 vine==5.1.0 wcwidth==0.2.13 +yarl==1.22.0