From c332915bf4ac89d8f1d531ad67c46c79df8c6f1f Mon Sep 17 00:00:00 2001 From: 101t Date: Tue, 27 Dec 2022 20:34:02 +0300 Subject: [PATCH 01/14] python3.11, bug fixes --- .pre-commit-config.yaml | 6 +++ Dockerfile | 48 ++++++------------- config/settings/com.py | 2 +- deployment/docker/alpine/Dockerfile | 48 +++++++++++++++++++ .../docker/alpine/docker-compose-alpine.yml | 34 +++++++++++++ deployment/docker/slim-buster/Dockerfile | 4 +- deployment/docker/slim-buster/celery_run.sh | 5 ++ .../docker/slim-buster/docker-entrypoint.sh | 9 ++++ docker-compose-alpine.yml | 36 ++++++++++++++ docker-compose.yml | 22 +++++++-- docker-entrypoint.sh | 2 +- main/api/__init__.py | 1 - main/core/__init__.py | 1 - main/core/middleware.py | 19 ++++++-- main/taskapp/__init__.py | 1 - main/users/__init__.py | 1 - main/users/urls.py | 1 - requirements.txt | 4 +- 18 files changed, 191 insertions(+), 53 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 deployment/docker/alpine/Dockerfile create mode 100644 deployment/docker/alpine/docker-compose-alpine.yml create mode 100755 deployment/docker/slim-buster/celery_run.sh create mode 100755 deployment/docker/slim-buster/docker-entrypoint.sh create mode 100644 docker-compose-alpine.yml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..e11ee1c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,6 @@ +repos: +- repo: https://github.com/adamchainz/django-upgrade + rev: "1.12.0" # replace with latest tag on GitHub + hooks: + - id: django-upgrade + args: [--target-version, "4.1"] # Replace with Django version diff --git a/Dockerfile b/Dockerfile index 302cd43..1029d9f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,46 +1,26 @@ -FROM python:3.8-alpine +FROM python:3.11 -ENV PYTHONDONTWRITEBYTECODE 1 -ENV PYTHONUNBUFFERED 1 -ENV PATH="/jasmin:${PATH}" -ENV JASMIN_HOME=/jasmin - -# RUN mkdir /jasmin - -RUN addgroup -S jasmin && adduser -S jasmin -G jasmin -h $JASMIN_HOME +RUN apt-get update && apt-get install telnet -#RUN apk del busybox-extras -RUN apk update && apk add busybox-extras -RUN apk add build-base git gcc cmake py3-setuptools -RUN busybox-extras --list -RUN apk add --no-cache bash +RUN apt-get install -y libpq-dev postgresql-client python3-psycopg2 +RUN adduser --home /jasmin --system --group jasmin +ENV JASMIN_HOME=/jasmin +ENV JASMIN_PORT=8000 WORKDIR $JASMIN_HOME -RUN mkdir -p $JASMIN_HOME/public/media -RUN mkdir -p $JASMIN_HOME/public/static - -# RUN chown -R jasmin:jasmin $JASMIN_HOME/ - -COPY ./requirements.txt $JASMIN_HOME/requirements.txt - -RUN pip install --upgrade pip -RUN pip install -r requirements.txt - -# RUN export PATH="/usr/local/bin:$HOME/.local/bin::$PATH" - -COPY . $JASMIN_HOME +# USER jasmin -COPY ./docker-entrypoint.sh docker-entrypoint.sh +RUN mkdir -p $JASMIN_HOME/public/media && \ + mkdir -p $JASMIN_HOME/public/static -#RUN chmod -R u+x ${JASMIN_HOME} && \ -# chgrp -R 0 ${JASMIN_HOME} && \ -# chmod -R g=u ${JASMIN_HOME} +COPY requirements.txt /tmp/requirements.txt +RUN pip install --no-cache-dir -r /tmp/requirements.txt -RUN chown -R jasmin:jasmin $JASMIN_HOME/ +COPY . . -USER jasmin +# ENTRYPOINT [ "./docker-entrypoint.sh" ] -ENTRYPOINT ["docker-entrypoint.sh"] \ No newline at end of file +CMD ["./docker-entrypoint.sh"] diff --git a/config/settings/com.py b/config/settings/com.py index 49a7e34..705723e 100644 --- a/config/settings/com.py +++ b/config/settings/com.py @@ -49,6 +49,7 @@ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'crequest.middleware.CrequestMiddleware', + 'main.core.middleware.AjaxMiddleware', 'main.core.middleware.TelnetConnectionMiddleware', 'main.core.middleware.UserAgentMiddleware', 'main.users.middleware.LastUserActivityMiddleware', @@ -109,7 +110,6 @@ USE_I18N = True -USE_L10N = True USE_TZ = True diff --git a/deployment/docker/alpine/Dockerfile b/deployment/docker/alpine/Dockerfile new file mode 100644 index 0000000..b61871c --- /dev/null +++ b/deployment/docker/alpine/Dockerfile @@ -0,0 +1,48 @@ +FROM alpine:3.11 + +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 +ENV JASMIN_HOME=/jasmin +ENV PATH="${PATH}:/jasmin" + +# RUN mkdir /jasmin +RUN addgroup -S jasmin && adduser -S jasmin -G jasmin -h $JASMIN_HOME + +#RUN apk del busybox-extras +RUN apk --update --no-cache upgrade +RUN apk add python3 --repository=http://dl-cdn.alpinelinux.org/alpine/v3.11/main && ln -sf python3 /usr/bin/python +# RUN apk search busybox-extras +RUN apk add busybox-extras +# RUN busybox --list +# RUN apk add --no-cache bash curl nmap apache2-utils bind-tools tcpdump mtr iperf3 strace tree busybox-extras netcat-openbsd +RUN echo alias telnet='busybox-extras telnet' >> .bashrc +RUN telnet google.com 80 + +RUN apk add --update build-base git gcc cmake py3-setuptools py3-pip python3-dev bash + +# RUN apk add --no-cache bash + +WORKDIR $JASMIN_HOME + +USER jasmin + +RUN mkdir -p $JASMIN_HOME/public/media +RUN mkdir -p $JASMIN_HOME/public/static + +# RUN chown -R jasmin:jasmin $JASMIN_HOME/ + +COPY --chown=jasmin:jasmin ./requirements.txt $JASMIN_HOME/requirements.txt + +ENV PATH="${PATH}:/jasmin/.local/bin" + +RUN pip3 install --upgrade pip && pip3 install -r requirements.txt + +COPY --chown=jasmin:jasmin . $JASMIN_HOME + +COPY --chown=jasmin:jasmin ./docker-entrypoint.sh docker-entrypoint.sh + +# RUN chown -R jasmin:jasmin $JASMIN_HOME/ + +# USER root + +ENTRYPOINT ["docker-entrypoint.sh"] \ No newline at end of file diff --git a/deployment/docker/alpine/docker-compose-alpine.yml b/deployment/docker/alpine/docker-compose-alpine.yml new file mode 100644 index 0000000..d30b958 --- /dev/null +++ b/deployment/docker/alpine/docker-compose-alpine.yml @@ -0,0 +1,34 @@ +version: '3.7' + +services: + jasmin_web: + image: tarekaec/jasmin_web_panel:1.0-alpine + ports: + - "8000:8000" + deploy: + replicas: 1 + env_file: + - .env + environment: + JASMIN_PORT: 8000 + healthcheck: + disable: true + volumes: + - ./public:/web/public + # entrypoint: /jasmin/docker-entrypoint.sh + jasmin_celery: + image: tarekaec/jasmin_web_panel:1.0-alpine + deploy: + replicas: 1 + env_file: + - .env + environment: + DEBUG: 0 + healthcheck: + disable: true + depends_on: + - jasmin_redis + entrypoint: /jasmin/celery_run.sh + jasmin_redis: + image: redis:alpine + tty: true diff --git a/deployment/docker/slim-buster/Dockerfile b/deployment/docker/slim-buster/Dockerfile index b1d8416..594a776 100644 --- a/deployment/docker/slim-buster/Dockerfile +++ b/deployment/docker/slim-buster/Dockerfile @@ -1,8 +1,8 @@ FROM python:3.8-slim-buster -RUN apt update && apt install telnet +RUN apt-get update && apt-get install telnet -RUN apt install -y git build-essential libpq-dev postgresql-client postgresql-client-common python3-psycopg2 +RUN apt-get install -y git build-essential libpq-dev postgresql-client postgresql-client-common python3-psycopg2 RUN adduser --home /jasmin --system --group jasmin diff --git a/deployment/docker/slim-buster/celery_run.sh b/deployment/docker/slim-buster/celery_run.sh new file mode 100755 index 0000000..498b73f --- /dev/null +++ b/deployment/docker/slim-buster/celery_run.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +cd $JASMIN_HOME + +celery -A main.taskapp worker -l info --autoscale=10,3 \ No newline at end of file diff --git a/deployment/docker/slim-buster/docker-entrypoint.sh b/deployment/docker/slim-buster/docker-entrypoint.sh new file mode 100755 index 0000000..7573a7a --- /dev/null +++ b/deployment/docker/slim-buster/docker-entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +cd $JASMIN_HOME + +python manage.py migrate +python manage.py load_new +python manage.py collectstatic --noinput --clear --no-post-process + +/usr/local/bin/gunicorn config.wsgi:application --workers 4 -b :$JASMIN_PORT --log-level info --worker-class=gevent --reload \ No newline at end of file diff --git a/docker-compose-alpine.yml b/docker-compose-alpine.yml new file mode 100644 index 0000000..2c96005 --- /dev/null +++ b/docker-compose-alpine.yml @@ -0,0 +1,36 @@ +version: '3.7' + +services: + jasmin_web: + #image: tarekaec/jasmin_web_panel:1.0 + image: tarekaec/jasmin_web_panel:1.0-alpine + ports: + - "8000:8000" + deploy: + replicas: 1 + env_file: + - .env + environment: + JASMIN_PORT: 8000 + healthcheck: + disable: true + volumes: + - ./public:/web/public + entrypoint: /jasmin/docker-entrypoint.sh + jasmin_celery: + # image: tarekaec/jasmin_web_panel:1.0 + image: tarekaec/jasmin_web_panel:1.0-alpine + deploy: + replicas: 1 + env_file: + - .env + environment: + DEBUG: 0 + healthcheck: + disable: true + depends_on: + - jasmin_redis + entrypoint: /jasmin/celery_run.sh + jasmin_redis: + image: redis:alpine + tty: true diff --git a/docker-compose.yml b/docker-compose.yml index ae8faad..eba6a4b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,8 +2,7 @@ version: '3.7' services: jasmin_web: - image: tarekaec/jasmin_web_panel:1.0 - # image: tarekaec/jasmin_web_panel:1.0-alpine + image: tarekaec/jasmin_web_panel:1.1 ports: - "8000:8000" deploy: @@ -15,11 +14,10 @@ services: healthcheck: disable: true volumes: - - ./public:/web/public + - ./public:/jasmin/public entrypoint: /jasmin/docker-entrypoint.sh jasmin_celery: - image: tarekaec/jasmin_web_panel:1.0 - # image: tarekaec/jasmin_web_panel:1.0-alpine + image: tarekaec/jasmin_web_panel:1.1 deploy: replicas: 1 env_file: @@ -34,3 +32,17 @@ services: jasmin_redis: image: redis:alpine tty: true + volumes: + - redis_data:/data + command: + - 'redis-server' + - '--appendonly yes' + - '--save 60 1' + # restart: unless-stopped + environment: + REDIS_REPLICATION_MODE: master + ALLOW_EMPTY_PASSWORD: "yes" + +volumes: + redis_data: + driver: local diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 7573a7a..26a277d 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -6,4 +6,4 @@ python manage.py migrate python manage.py load_new python manage.py collectstatic --noinput --clear --no-post-process -/usr/local/bin/gunicorn config.wsgi:application --workers 4 -b :$JASMIN_PORT --log-level info --worker-class=gevent --reload \ No newline at end of file +gunicorn config.wsgi:application --workers 4 -b :$JASMIN_PORT --log-level info --worker-class=gevent --reload diff --git a/main/api/__init__.py b/main/api/__init__.py index 49ebbb5..40a96af 100644 --- a/main/api/__init__.py +++ b/main/api/__init__.py @@ -1,2 +1 @@ # -*- coding: utf-8 -*- -default_app_config = 'main.api.apps.CoreConfig' diff --git a/main/core/__init__.py b/main/core/__init__.py index cfa75ca..40a96af 100644 --- a/main/core/__init__.py +++ b/main/core/__init__.py @@ -1,2 +1 @@ # -*- coding: utf-8 -*- -default_app_config = 'main.core.apps.CoreConfig' diff --git a/main/core/middleware.py b/main/core/middleware.py index 564372a..e419abe 100644 --- a/main/core/middleware.py +++ b/main/core/middleware.py @@ -12,6 +12,19 @@ logger = logging.getLogger(__name__) +class AjaxMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + def is_ajax(self): # noqa + return request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest' + + request.is_ajax = is_ajax.__get__(request) + response = self.get_response(request) + return response + + class TelnetConnectionMiddleware(MiddlewareMixin): def process_request(self, request): """Add a telnet connection to all request paths that start with /api/ @@ -53,9 +66,9 @@ def process_request(self, request): # raise TelnetLoginFailed except UnboundLocalError as e: logger.error(f"Cannot connect through Telnet, the error: \n {e}") - else: - request.telnet = telnet - return None + # else: + request.telnet = telnet + return None def process_response(self, request, response): "Make sure telnet connection is closed when unleashing response back to client" diff --git a/main/taskapp/__init__.py b/main/taskapp/__init__.py index 45e0195..40a96af 100644 --- a/main/taskapp/__init__.py +++ b/main/taskapp/__init__.py @@ -1,2 +1 @@ # -*- coding: utf-8 -*- -default_app_config = 'main.taskapp.apps.CeleryConfig' diff --git a/main/users/__init__.py b/main/users/__init__.py index 479a02a..40a96af 100644 --- a/main/users/__init__.py +++ b/main/users/__init__.py @@ -1,2 +1 @@ # -*- coding: utf-8 -*- -default_app_config = 'main.users.apps.UsersConfig' \ No newline at end of file diff --git a/main/users/urls.py b/main/users/urls.py index 30a7b0d..d8b6029 100644 --- a/main/users/urls.py +++ b/main/users/urls.py @@ -1,7 +1,6 @@ # -*- encoding: utf-8 -*- from __future__ import unicode_literals from django.utils.translation import gettext_lazy as _ -from django.conf.urls import url from django.urls import path, re_path from .views import * diff --git a/requirements.txt b/requirements.txt index d216135..b956da2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ celery coreapi coreschema -Django==3.2.* +Django==4.1.* # django-3-jet==1.0.8 django-jet-reboot django-autoslug @@ -23,7 +23,7 @@ pexpect Pillow psycopg2-binary ptyprocess -pysha3 +#pysha3 python-dateutil pytz PyYAML From dc9ca62025bf8c38d205f26c26d293171701be38 Mon Sep 17 00:00:00 2001 From: 101t Date: Thu, 29 Dec 2022 08:53:11 +0300 Subject: [PATCH 02/14] CSRF_TRUSTED_ORIGINS added django 4.1 --- config/settings/pro.py | 1 + 1 file changed, 1 insertion(+) diff --git a/config/settings/pro.py b/config/settings/pro.py index 22bb22c..89edf9d 100644 --- a/config/settings/pro.py +++ b/config/settings/pro.py @@ -6,6 +6,7 @@ } ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['*']) +CSRF_TRUSTED_ORIGINS = env.list('CSRF_TRUSTED_ORIGINS', default=[]) INSTALLED_APPS += ("gunicorn", ) From 0d7ec541afe53ac52122449b081d4b75d53ebb98 Mon Sep 17 00:00:00 2001 From: 101t Date: Thu, 29 Dec 2022 08:56:55 +0300 Subject: [PATCH 03/14] CSRF_TRUSTED_ORIGINS to .env file --- Sample.env | 1 + 1 file changed, 1 insertion(+) diff --git a/Sample.env b/Sample.env index 57886dd..292b167 100644 --- a/Sample.env +++ b/Sample.env @@ -21,6 +21,7 @@ PRODB_URL=postgres://postgres:123@127.0.0.1:5432/jasmin_web_db ACTIVE_APP=web ALLOWED_HOSTS=* +CSRF_TRUSTED_ORIGINS= TELNET_HOST=127.0.0.1 TELNET_PORT=8990 From 7e0c3cf1350206926824dc41a66591f66e259bc2 Mon Sep 17 00:00:00 2001 From: Tarek Kalaji Date: Tue, 27 Jun 2023 12:19:46 +0300 Subject: [PATCH 04/14] upgrade django 4.2, psycopg3 version, working on reset password --- .pre-commit-config.yaml | 6 - .vscode/settings.json | 12 - DEVELOPMENT.md | 101 -------- Dockerfile | 26 -- Sample.env | 7 +- .../docker/alpine/Dockerfile | 0 .../docker/alpine/docker-compose-alpine.yml | 0 config/docker/slim/Dockerfile | 64 +++++ .../docker/slim/docker-entrypoint-celery.sh | 0 .../docker/slim}/docker-entrypoint.sh | 0 config/locale/README.md | 9 - config/settings/com.py | 77 +++--- config/settings/pro.py | 26 +- config/version.py | 1 + deploy.py | 21 -- deployment/docker/slim-buster/Dockerfile | 32 --- deployment/docker/slim-buster/celery_run.sh | 5 - deployment/nginx/nginx.conf | 44 ---- deployment/supervisor/celery.conf | 32 --- deployment/supervisor/daphne.conf | 14 - deployment/supervisor/gunicorn.conf | 14 - deployment/systemctl/README.md | 108 -------- deployment/systemctl/djangoaio-celery.service | 21 -- .../systemctl/djangoaio-celerybeat.service | 21 -- deployment/systemctl/djangoaio-daphne.service | 21 -- .../systemctl/djangoaio-gunicorn.service | 21 -- docker-compose-alpine.yml | 36 --- docker-entrypoint.sh | 9 - docs/README.md | 240 ------------------ load_data.sh | 21 -- load_data_win.bat | 34 --- main/api/urls.py | 1 + main/api/views/__init__.py | 3 +- main/api/views/health_check.py | 7 + main/core/context_processors.py | 6 +- main/core/middleware.py | 2 +- main/static/assets/img/django-aio.png | Bin 3862 -> 0 bytes main/static/assets/img/jasmin.jpg | Bin 0 -> 5450 bytes main/static/assets/img/jasmin.png | Bin 0 -> 29234 bytes main/users/apps.py | 6 +- main/users/middleware.py | 9 +- main/users/urls.py | 12 +- main/users/views/__init__.py | 3 +- main/users/views/reset.py | 12 +- main/users/views/signin.py | 20 +- main/web/templates/auth/base.html | 18 +- main/web/templates/auth/reset.html | 18 +- main/web/templates/auth/signin.html | 59 ++--- main/web/templates/web/includes/topbar.html | 2 +- requirements.txt | 40 +-- scripts/base.txt | 28 ++ scripts/development.txt | 3 + scripts/production.txt | 4 + 53 files changed, 247 insertions(+), 1029 deletions(-) delete mode 100644 .pre-commit-config.yaml delete mode 100644 .vscode/settings.json delete mode 100644 DEVELOPMENT.md delete mode 100644 Dockerfile rename {deployment => config}/docker/alpine/Dockerfile (100%) rename {deployment => config}/docker/alpine/docker-compose-alpine.yml (100%) create mode 100644 config/docker/slim/Dockerfile rename celery_run.sh => config/docker/slim/docker-entrypoint-celery.sh (100%) rename {deployment/docker/slim-buster => config/docker/slim}/docker-entrypoint.sh (100%) delete mode 100644 config/locale/README.md create mode 100644 config/version.py delete mode 100755 deploy.py delete mode 100644 deployment/docker/slim-buster/Dockerfile delete mode 100755 deployment/docker/slim-buster/celery_run.sh delete mode 100644 deployment/nginx/nginx.conf delete mode 100644 deployment/supervisor/celery.conf delete mode 100644 deployment/supervisor/daphne.conf delete mode 100644 deployment/supervisor/gunicorn.conf delete mode 100644 deployment/systemctl/README.md delete mode 100644 deployment/systemctl/djangoaio-celery.service delete mode 100644 deployment/systemctl/djangoaio-celerybeat.service delete mode 100644 deployment/systemctl/djangoaio-daphne.service delete mode 100644 deployment/systemctl/djangoaio-gunicorn.service delete mode 100644 docker-compose-alpine.yml delete mode 100755 docker-entrypoint.sh delete mode 100644 docs/README.md delete mode 100755 load_data.sh delete mode 100644 load_data_win.bat create mode 100644 main/api/views/health_check.py delete mode 100644 main/static/assets/img/django-aio.png create mode 100644 main/static/assets/img/jasmin.jpg create mode 100644 main/static/assets/img/jasmin.png create mode 100644 scripts/base.txt create mode 100644 scripts/development.txt create mode 100644 scripts/production.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index e11ee1c..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,6 +0,0 @@ -repos: -- repo: https://github.com/adamchainz/django-upgrade - rev: "1.12.0" # replace with latest tag on GitHub - hooks: - - id: django-upgrade - args: [--target-version, "4.1"] # Replace with Django version diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index f197519..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "python.pythonPath": "C:\\Users\\Dell\\AppData\\Local\\Programs\\Python\\Python37\\python.exe", - "python.linting.pycodestyleEnabled": true, - "python.linting.pylintEnabled": true, - "files.associations": { - "**/*.html": "html", - "**/templates/**/*.html": "django-html", - "**/templates/**/*": "django-txt", - "**/requirements{/**,*}.{txt,in}": "pip-requirements" - }, - "emmet.includeLanguages": {"django-html": "html"} -} \ No newline at end of file diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md deleted file mode 100644 index eb9abfc..0000000 --- a/DEVELOPMENT.md +++ /dev/null @@ -1,101 +0,0 @@ -# Jasmin Web Panel -

- travis-ci -

- -Jasmin SMS Web Interface for Jasmin SMS Gateway - -## Getting Started - -In your terminal for **Unix** (Linux/Mac) - -```sh -pip install virtualenv - -git clone https://github.com/101t/jasmin-web-panel --depth 1 - -cd jasmin-web-panel/ - -virtualenv -p python3 env - -source env/bin/activate - -pip install -r requirements.txt - -cp -rf Sample.env .env - -./load_data.sh --init -``` - -In Command Prompt for **Windows** - -```sh -python -m pip install virtualenv - -git clone https://github.com/101t/jasmin-web-panel --depth 1 - -cd jasmin-web-panel/ - -virtualenv env - -env/Scripts/activate - -pip install -r requirements.txt - -copy Sample.env .env - -load_data_win.bat --init -``` - -> Note: the `admin` user automatically added to project as default administrator user, the credentials authentication is **Username: `admin`, Password: `secret`**. - -## Development - -### Prepare Translations - -Adding translation made easy by this commands - -```sh -cd jasmin-web-panel/main/ - -django-admin makemessages -l en - -django-admin compilemessages -``` -> Note: make sure you have `gettext` installed in your `Unix` Environment - -```sh -# using gettext in ubuntu or macOS -msgunfmt [django.mo] > [django.po] -``` - -### Run Celery - -To run your celery in development -```sh -celery worker -A main.taskapp -l debug -``` - -### Run Channels -To run channels in development as `ASGI` using `daphne` -```sh -daphne config.asgi:application -b 0.0.0.0 -p 9000 -``` - -### Run Django -To run django in development as `HTTP` -```sh -python manage.py runserver 0.0.0.0:8000 -``` - -### Upgrading Packages - -Here the following examples how to upgrade some packages - -```sh -pip install -U django -pip install -U channels -pip install -U celery -pip install -U djangorestframework markdown django-filter -``` -> Note: be careful about sub-packages compatibility and dependencies conflict while **upgrading** diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 1029d9f..0000000 --- a/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -FROM python:3.11 - -RUN apt-get update && apt-get install telnet - -RUN apt-get install -y libpq-dev postgresql-client python3-psycopg2 - -RUN adduser --home /jasmin --system --group jasmin - -ENV JASMIN_HOME=/jasmin -ENV JASMIN_PORT=8000 - -WORKDIR $JASMIN_HOME - -# USER jasmin - -RUN mkdir -p $JASMIN_HOME/public/media && \ - mkdir -p $JASMIN_HOME/public/static - -COPY requirements.txt /tmp/requirements.txt -RUN pip install --no-cache-dir -r /tmp/requirements.txt - -COPY . . - -# ENTRYPOINT [ "./docker-entrypoint.sh" ] - -CMD ["./docker-entrypoint.sh"] diff --git a/Sample.env b/Sample.env index 292b167..65d5ada 100644 --- a/Sample.env +++ b/Sample.env @@ -8,9 +8,9 @@ TIME_ZONE=Etc/GMT-3 LANGUAGE_CODE=en SITE_ID=1 -MYSQLDB_URL=mysql://root:123456@127.0.0.1/jasmin_web_db -SQLITE3_URL=sqlite:///db.sqlite3 -POSTGRE_URL=postgres://postgres:123@127.0.0.1:5432/jasmin_web_db +# mysql://root:123456@127.0.0.1/jasmin_web_db +# sqlite:///db.sqlite3 +# postgres://postgres:123@127.0.0.1:5432/jasmin_web_db DEVDB_URL=sqlite:///db.sqlite3 PRODB_URL=postgres://postgres:123@127.0.0.1:5432/jasmin_web_db @@ -19,7 +19,6 @@ PRODB_URL=postgres://postgres:123@127.0.0.1:5432/jasmin_web_db # REDIS_PORT=6379 # REDIS_DB=0 -ACTIVE_APP=web ALLOWED_HOSTS=* CSRF_TRUSTED_ORIGINS= diff --git a/deployment/docker/alpine/Dockerfile b/config/docker/alpine/Dockerfile similarity index 100% rename from deployment/docker/alpine/Dockerfile rename to config/docker/alpine/Dockerfile diff --git a/deployment/docker/alpine/docker-compose-alpine.yml b/config/docker/alpine/docker-compose-alpine.yml similarity index 100% rename from deployment/docker/alpine/docker-compose-alpine.yml rename to config/docker/alpine/docker-compose-alpine.yml diff --git a/config/docker/slim/Dockerfile b/config/docker/slim/Dockerfile new file mode 100644 index 0000000..6f7faae --- /dev/null +++ b/config/docker/slim/Dockerfile @@ -0,0 +1,64 @@ +FROM python:3.11-slim + +# disable debian interactive +ARG DEBIAN_FRONTEND=noninteractive +# suppress pip upgrade warning +ARG PIP_DISABLE_PIP_VERSION_CHECK=1 +# disable cache directory, image size 2.1GB to 1.9GB +ARG PIP_NO_CACHE_DIR=1 + +RUN apt-get update && apt-get -y upgrade + +RUN apt-get install --no-install-recommends -y \ + python3-dev python3-wheel python3-setuptools virtualenv \ + build-essential gcc curl \ + libpq-dev libpq5 telnet + +# Pillow dependencies +RUN apt-get install --no-install-recommends -y \ + libtiff5-dev libjpeg-dev libopenjp2-7-dev zlib1g-dev \ + libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev \ + tk8.6-dev python3-tk libharfbuzz-dev libfribidi-dev libxcb1-dev + +RUN apt-get clean autoclean && \ + apt-get autoremove -y && \ + rm -rf /var/lib/{apt,dpkg,cache,log}/ + +# -------------------------------------- +ENV APP_DIR=/app +ENV APP_USER=app +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONBUFFERED=1 + +RUN useradd -m -d ${APP_DIR} -U -r -s /bin/bash ${APP_USER} + +USER ${APP_USER} + +WORKDIR ${APP_DIR} + +# Create the virtual environment +RUN python -m venv /app/env +# Activate the virtual environment +ENV PATH="$APP_DIR/env/bin:$PATH" + +COPY scripts/base.txt base.txt +COPY scripts/production.txt requirements.txt + +RUN pip install -U pip wheel + +RUN pip install -r requirements.txt + +# copy code to image +COPY --chown=$APP_USER . . + +COPY --chown=$APP_USER config/docker/docker-entrypoint.sh docker-entrypoint.sh + +RUN mkdir -p public/static && mkdir -p public/media && mkdir -p logs/ + +EXPOSE 8000 + +ENTRYPOINT ["docker-entrypoint.sh"] + +HEALTHCHECK --interval=10s --timeout=10s --retries=30 \ + CMD curl -L http://127.0.0.1:8000/api/health_check > /dev/null + diff --git a/celery_run.sh b/config/docker/slim/docker-entrypoint-celery.sh similarity index 100% rename from celery_run.sh rename to config/docker/slim/docker-entrypoint-celery.sh diff --git a/deployment/docker/slim-buster/docker-entrypoint.sh b/config/docker/slim/docker-entrypoint.sh similarity index 100% rename from deployment/docker/slim-buster/docker-entrypoint.sh rename to config/docker/slim/docker-entrypoint.sh diff --git a/config/locale/README.md b/config/locale/README.md deleted file mode 100644 index ff0558d..0000000 --- a/config/locale/README.md +++ /dev/null @@ -1,9 +0,0 @@ -## Commands that used in django translations - -```sh -cd django-aio/config/ - -django-admin makemessages -l en - -django-admin compilemessages -``` \ No newline at end of file diff --git a/config/settings/com.py b/config/settings/com.py index 705723e..ec40123 100644 --- a/config/settings/com.py +++ b/config/settings/com.py @@ -1,7 +1,8 @@ -"""Django 3.0.5""" -from __future__ import absolute_import, unicode_literals +"""Django 4.2""" from django.utils.translation import gettext_lazy as _ -import os, environ +from django.contrib.messages import constants as message_constants +import os +import environ ROOT_DIR = environ.Path(__file__) - 3 # (/a/b/myfile.py - 3 = /) APPS_DIR = ROOT_DIR.path('main') @@ -11,11 +12,11 @@ env = environ.Env() env.read_env('.env') -SECRET_KEY = env("SECRET_KEY", default='8na#(#x@0i*3ah%&$-q)b&wqu5ct_a3))d8-sqk-ux*5lol*wl') +SECRET_KEY = os.environ.get("SECRET_KEY", default='8na#(#x@0i*3ah%&$-q)b&wqu5ct_a3))d8-sqk-ux*5lol*wl') -DEBUG = env.bool("DEBUG", False) +DEBUG = bool(os.environ.get("DEBUG", '0')) -SITE_ID = int(env("SITE_ID", default='1')) +SITE_ID = int(os.environ.get("SITE_ID", default='1')) INSTALLED_APPS = [ 'jet.dashboard', @@ -29,7 +30,7 @@ 'django.contrib.humanize', # 'channels', - 'crequest', + 'crequest', # noqa 'rest_framework', 'main.api', @@ -48,7 +49,7 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'crequest.middleware.CrequestMiddleware', + 'crequest.middleware.CrequestMiddleware', # noqa 'main.core.middleware.AjaxMiddleware', 'main.core.middleware.TelnetConnectionMiddleware', 'main.core.middleware.UserAgentMiddleware', @@ -58,7 +59,7 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [str(APPS_DIR.path('templates')),], + 'DIRS': [str(APPS_DIR.path('templates')), ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -77,12 +78,14 @@ ] AUTH_PASSWORD_VALIDATORS = [ - {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',}, - {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',}, - {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',}, - {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',}, + {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, + {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, + {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, + {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] +ASGI_APPLICATION = 'config.asgi.application' + WSGI_APPLICATION = 'config.wsgi.application' ROOT_URLCONF = 'config.urls' @@ -95,29 +98,27 @@ LOGIN_URL = "/account/login/" -ADMIN_URL = env('ADMIN_URL', default="admin/") +ADMIN_URL = os.environ.get('ADMIN_URL', default="admin/") LOCALE_PATHS = (str(APPS_DIR('locale')), str(CONF_DIR('locale')),) -LANGUAGE_CODE = env('LANGUAGE_CODE', default="en") +LANGUAGE_CODE = os.environ.get('LANGUAGE_CODE', default="en") LANGUAGES = ( ('en', _('English')), ('tr', _('Türkçe')), ) -TIME_ZONE = env('TIME_ZONE', default='UTC') +TIME_ZONE = os.environ.get('TIME_ZONE', default='UTC') USE_I18N = True - USE_TZ = True -SITE_TITLE = "Jasmin Web site admin" +SITE_TITLE = "Jasmin Web site admin" SITE_HEADER = "Jasmin Web administration" INDEX_TITLE = "Dashboard administration" -from django.contrib.messages import constants as message_constants MESSAGE_TAGS = { message_constants.DEBUG: 'info', message_constants.INFO: 'info', @@ -141,9 +142,9 @@ MEDIA_URL = '/media/' -REDIS_HOST = env("REDIS_HOST", default="jasmin_redis") -REDIS_PORT = env.int("REDIS_PORT", default=6379) -REDIS_DB = env.int("REDIS_DB", default=0) +REDIS_HOST = os.environ.get("REDIS_HOST", default="jasmin_redis") +REDIS_PORT = int(os.environ.get("REDIS_PORT", default=6379)) +REDIS_DB = int(os.environ.get("REDIS_DB", default=0)) REDIS_URL = (REDIS_HOST, REDIS_PORT) DEFAULT_USER_AVATAR = STATIC_URL + "assets/img/user.png" @@ -151,7 +152,6 @@ LAST_ACTIVITY_INTERVAL_SECS = 3600 # REST API Settings - REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.BasicAuthentication', @@ -177,17 +177,21 @@ 'SHOW_REQUEST_HEADERS': True, } -# Jasmin Settings -"""Jasmin telnet defaults""" -TELNET_HOST = env('TELNET_HOST', default='127.0.0.1') -TELNET_PORT = env.int('TELNET_PORT', default=8990) -TELNET_USERNAME = env('TELNET_USERNAME', default='jcliadmin') -TELNET_PW = env('TELNET_PW', default='jclipwd') # no alternative storing as plain text -TELNET_TIMEOUT = env.int('TELNET_TIMEOUT', default=10) # reasonable value for intranet. - -STANDARD_PROMPT = 'jcli : ' # There should be no need to change this -INTERACTIVE_PROMPT = '> ' # Prompt for interactive commands -SUBMIT_LOG = env.bool('SUBMIT_LOG', False) # This is used for DLR Report +# Jasmin SMS Gateway Settings - telnet configurations +TELNET_HOST = os.environ.get('TELNET_HOST', default='127.0.0.1') +TELNET_PORT = int(os.environ.get('TELNET_PORT', default=8990)) +TELNET_USERNAME = os.environ.get('TELNET_USERNAME', default='jcliadmin') # noqa +# no alternative storing as plain text +TELNET_PW = os.environ.get('TELNET_PW', default='jclipwd') # noqa +# reasonable value for intranet. +TELNET_TIMEOUT = int(os.environ.get('TELNET_TIMEOUT', default=10)) +# There should be no need to change this +STANDARD_PROMPT = 'jcli : ' +# Prompt for interactive commands +INTERACTIVE_PROMPT = '> ' +# This is used for DLR Report +SUBMIT_LOG = bool(os.environ.get('SUBMIT_LOG', '0')) + """ SYSCTL_HEALTH_CHECK boolean field to enable Jasmin Health Check UI Monitoring SYSCTL_HEALTH_CHECK_SERVICES list of available services: @@ -203,8 +207,7 @@ - rabbitmq - postgresql """ -SYSCTL_HEALTH_CHECK = env.bool("SYSCTL_HEALTH_CHECK", default=False) -SYSCTL_HEALTH_CHECK_SERVICES = env.list("SYSCTL_HEALTH_CHECK_SERVICES", default="jasmind") - +SYSCTL_HEALTH_CHECK = os.environ.get("SYSCTL_HEALTH_CHECK", default=False) +SYSCTL_HEALTH_CHECK_SERVICES = os.environ.get("SYSCTL_HEALTH_CHECK_SERVICES", default="jasmind") DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' diff --git a/config/settings/pro.py b/config/settings/pro.py index 89edf9d..1262c8f 100644 --- a/config/settings/pro.py +++ b/config/settings/pro.py @@ -8,7 +8,9 @@ ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['*']) CSRF_TRUSTED_ORIGINS = env.list('CSRF_TRUSTED_ORIGINS', default=[]) -INSTALLED_APPS += ("gunicorn", ) +INSTALLED_APPS += ("gunicorn",) + +DJANGO_LOG_LEVEL = os.environ.get("DJANGO_LOG_LEVEL", default="WARNING") LOGGING = { 'version': 1, @@ -20,32 +22,32 @@ }, 'handlers': { 'default': { - 'level':'DEBUG', - 'class':'logging.handlers.RotatingFileHandler', + 'level': DJANGO_LOG_LEVEL, + 'class': 'logging.handlers.RotatingFileHandler', 'filename': os.path.join(str(ROOT_DIR), 'logs/app.log'), - 'maxBytes': 1024*1024*5, # 5 MB + 'maxBytes': 1024 * 1024 * 5, # 5 MB 'backupCount': 5, - 'formatter':'standard', + 'formatter': 'standard', }, 'request_handler': { - 'level':'DEBUG', - 'class':'logging.handlers.RotatingFileHandler', + 'level': DJANGO_LOG_LEVEL, + 'class': 'logging.handlers.RotatingFileHandler', 'filename': os.path.join(str(ROOT_DIR), 'logs/django.log'), - 'maxBytes': 1024*1024*5, # 5 MB + 'maxBytes': 1024 * 1024 * 5, # 5 MB 'backupCount': 5, - 'formatter':'standard', + 'formatter': 'standard', }, }, 'loggers': { '': { 'handlers': ['default'], - 'level': 'DEBUG', + 'level': DJANGO_LOG_LEVEL, 'propagate': True }, 'django.request': { 'handlers': ['request_handler'], - 'level': 'DEBUG', + 'level': DJANGO_LOG_LEVEL, 'propagate': False }, } -} \ No newline at end of file +} diff --git a/config/version.py b/config/version.py new file mode 100644 index 0000000..ea9d694 --- /dev/null +++ b/config/version.py @@ -0,0 +1 @@ +VERSION = "3.0.0" diff --git a/deploy.py b/deploy.py deleted file mode 100755 index 20e0f7f..0000000 --- a/deploy.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" -import os -import sys - - -def main(): - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.pro') - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == '__main__': - main() diff --git a/deployment/docker/slim-buster/Dockerfile b/deployment/docker/slim-buster/Dockerfile deleted file mode 100644 index 594a776..0000000 --- a/deployment/docker/slim-buster/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -FROM python:3.8-slim-buster - -RUN apt-get update && apt-get install telnet - -RUN apt-get install -y git build-essential libpq-dev postgresql-client postgresql-client-common python3-psycopg2 - -RUN adduser --home /jasmin --system --group jasmin - -ENV PYTHONDONTWRITEBYTECODE 1 -ENV PYTHONUNBUFFERED 1 -ENV JASMIN_HOME=/jasmin -#ENV PATH=$PATH:/usr/local/bin - -WORKDIR $JASMIN_HOME - -RUN mkdir -p $JASMIN_HOME/public/media && \ - mkdir -p $JASMIN_HOME/public/static - -COPY --chown=jasmin:jasmin ./requirements.txt $JASMIN_HOME/requirements.txt - -USER jasmin - -RUN pip install -U pip && pip install -r requirements.txt - -COPY --chown=jasmin:jasmin . $JASMIN_HOME - -COPY --chown=jasmin:jasmin ./docker-entrypoint.sh $JASMIN_HOME -COPY --chown=jasmin:jasmin ./docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh -#RUN ln -s docker-entrypoint.sh /usr/local/bin -#RUN chmod +x docker-entrypoint.sh -# RUN chown -R jasmin:jasmin $JASMIN_HOME/ - diff --git a/deployment/docker/slim-buster/celery_run.sh b/deployment/docker/slim-buster/celery_run.sh deleted file mode 100755 index 498b73f..0000000 --- a/deployment/docker/slim-buster/celery_run.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -cd $JASMIN_HOME - -celery -A main.taskapp worker -l info --autoscale=10,3 \ No newline at end of file diff --git a/deployment/nginx/nginx.conf b/deployment/nginx/nginx.conf deleted file mode 100644 index a00f9fa..0000000 --- a/deployment/nginx/nginx.conf +++ /dev/null @@ -1,44 +0,0 @@ -upstream channel_sck2{ - server 0.0.0.0:8000; -} -server { - listen 80; - charset utf-8; - server_name example.com www.example.com; - client_body_timeout 500; - client_header_timeout 500; - keepalive_timeout 500 500; - send_timeout 30; - access_log /var/log/nginx/django_aio_access.log combined; - error_log /var/log/nginx/django_aio_error.log; - - location / { - proxy_pass http://channel_sck2; - proxy_http_version 1.1; - proxy_read_timeout 86400; - proxy_redirect off; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Host $server_name; - proxy_max_temp_file_size 1600m; - proxy_buffering off; - proxy_request_buffering on; - client_max_body_size 2000M; - client_body_buffer_size 256K; - } - - location ^~ /media/ { - root /home/username/projects/django-aio/public/; - add_header Accept-Ranges bytes; - } - location ^~ /static/ { - root /home/username/projects/django-aio/public/; - add_header Pragma public; - add_header Cache-Control "public"; - expires 30d; - } -} \ No newline at end of file diff --git a/deployment/supervisor/celery.conf b/deployment/supervisor/celery.conf deleted file mode 100644 index d485d6e..0000000 --- a/deployment/supervisor/celery.conf +++ /dev/null @@ -1,32 +0,0 @@ -[program:django_aio_celerybeat] -environment = DJANGO_SETTINGS_MODULE="config.settings.pro" -command=/home/username/projects/django-aio/env/bin/celery beat -A main.taskapp -l info -directory=/home/username/projects/django-aio -user=username -numprocs=1 -stdout_logfile=/home/username/projects/django-aio/logs/beat.log -stderr_logfile=/home/username/projects/django-aio/logs/beat.log -autostart=true -autorestart=true -startsecs=10 -priority=997 -startretries=20 -stdout_logfile_maxbytes=5MB -stderr_logfile_maxbytes=5MB - -[program:django_aio_celery] -environment = DJANGO_SETTINGS_MODULE="config.settings.pro" -command=/home/username/projects/django-aio/env/bin/celery worker -A main.taskapp -l info #--autoscale=10,3 -directory=/home/username/projects/django-aio -user=username -numprocs=1 -stdout_logfile=/home/username/projects/django-aio/logs/worker.log -stderr_logfile=/home/username/projects/django-aio/logs/worker.log -autostart=true -autorestart=true -startsecs=10 -killasgroup=true -priority=998 -startretries=20 -stdout_logfile_maxbytes=5MB -stderr_logfile_maxbytes=5MB diff --git a/deployment/supervisor/daphne.conf b/deployment/supervisor/daphne.conf deleted file mode 100644 index bc2500a..0000000 --- a/deployment/supervisor/daphne.conf +++ /dev/null @@ -1,14 +0,0 @@ -[program:django_aio_asgi] -directory=/home/username/projects/django-aio -command=/home/username/projects/django-aio/env/bin/daphne -b 0.0.0.0 -p 8000 config.asgi:application -numprocs=1 -user=username -autostart=true -autorestart=true -stdout_logfile=/home/username/projects/django-aio/logs/asgi.log -stderr_logfile=/home/username/projects/django-aio/logs/asgi.log -redirect_stderr=true -priority=999 -stdout_logfile_maxbytes=5MB -stderr_logfile_maxbytes=5MB -environment=LANG=en_US.UTF-8,LC_ALL=en_US.UTF-8 \ No newline at end of file diff --git a/deployment/supervisor/gunicorn.conf b/deployment/supervisor/gunicorn.conf deleted file mode 100644 index d7ed94b..0000000 --- a/deployment/supervisor/gunicorn.conf +++ /dev/null @@ -1,14 +0,0 @@ -[program:django_aio_gunicorn] -directory=/home/username/projects/django-aio -command=/home/username/projects/django-aio/env/bin/gunicorn --bind 0.0.0.0:8000 config.wsgi -w 10 --timeout=120 --log-level=error -numprocs=1 -user=username -autostart=true -autorestart=true -stdout_logfile=/home/username/projects/django-aio/logs/gunicorn.log -stderr_logfile=/home/username/projects/django-aio/logs/gunicorn.log -redirect_stderr=true -priority=999 -stdout_logfile_maxbytes=5MB -stderr_logfile_maxbytes=5MB -environment=LANG=en_US.UTF-8,LC_ALL=en_US.UTF-8 \ No newline at end of file diff --git a/deployment/systemctl/README.md b/deployment/systemctl/README.md deleted file mode 100644 index 77aa7fd..0000000 --- a/deployment/systemctl/README.md +++ /dev/null @@ -1,108 +0,0 @@ -# Django-AIO Systemd Way - -You know - that tool you didn’t really knew you would need when you started to work on your app project. Sometime before you found out that deploying your app can turn into much more work than expected. - -Let’s make sure you have a broad overview, and enough understanding of useful commands to get going. After reading this, you should have just the right amount of information to interact with systemd and continue climbing the steep deployment learning curve. - -## What systemd does - -You need a process supervisor to keep your services running. Systemd help to keep your web app and its backing services (NGiNX, PostgreSQL) running smoothly. - -Systemd is an excellent choice. It’s used in many mainstream distros, and can do everything you need. You can start, stop or restart any services with a simple command, check their status and view logs - all via systemd. - -If something crashes, systemd will make sure to start a new process (if you configure it right), so you don’t have to do it manually. - -## Telling systemd about a new service - -Systemd uses configuration files to keep track of all service it manages. - -The folder you care about is `/etc/systemd/system`. If you look inside, you’ll see (among other content) lots of `.service` files. - -If you type a command like `systemctl status django-projectname`, systemd will look for a file names `django-projectname.service`. The `/etc/systemd/system` folder is the first place it looks. - -That django-projectname.service file is called a “unit file”, and it tells systemd: - -* That your service exists -* How to describe it -* How and when to run it - -The file name "django-projectname" is arbitrary, you can just name it "projectname", or some other string. Lower-case letters with dashes is how most services are named. Make sure it doesn’t clash with existing system services. - -## An example Django unit file - -Here’s how a `.service` file for a Django project can look like: -```sh -[Unit] -Description=A useful description to be shown in command-line tools - -[Service] -Restart=on-failure -WorkingDirectory=/var/www/django-projectname/projectname -ExecStart=/var/www/django-projectname/env/bin/gunicorn config.wsgi -b 127.0.0.1:8000 - -[Install] -WantedBy=multi-user.target -``` -A lot of nuance is missing there, so don’t use it for your project. Consult a more in-depth tutorial on the topic of writing a great unit file for your service. This one’s here to get you started with the topic. - -The [Service] section tells systemd how to run your app. We want it to restart if something goes wrong, and tell systemd the directory and exact command to run. A few assumptions about folder structure is made here - this can vary and is a matter of taste. I like to create a `/var/www/django-projectname` directory where everything related to a single project can be found. - -The [Install] section, is used when you `enable` the service, so it’s started automatically after the service restarts. - -## Most useful commands - -You interact with systemd-managed services, by using the command-line tools `systemctl` and `journalctl`. - -You’ll need to have superuser privileges for most of those, so add a `sudo` in the beginning of each command if needed. - -Once the file above is in place, you should tell systemd to take a look at its own configurations, so it can notice that stuff has changed. - -```sh -# added a new unit file? let systemd know -systemctl daemon-reload -``` -Now, you can: -```sh -# check the status of the service -systemctl status django-projectname - -# you can start the service -systemctl start django-projectname - -# restart it (stop and then start again in one command) -systemctl restart django-projectname - -# just stop it -systemctl stop django-projectname -``` -If you want the service to be started by default after a reboot, use: - -```sh -systemctl enable django-projectname -``` - -You’ll need the [Install] section in your unit file for this to have any effect. - -To view the logs of a service, you can use: - -```sh -journalctl -u django-projectname -``` - -You can add a `-b` to view all lines since the last reboot - -## One more thing - -Here’s some cool but mostly useless knowledge I really like! - -If you want to get a quick overview of all services which are started when your server boots up, you can use: - -```sh -systemctl list-dependencies multi-user.target -``` - -If you want to know what your new service needs, just take a look: - -```sh -systemctl list-dependencies django-projectname -``` \ No newline at end of file diff --git a/deployment/systemctl/djangoaio-celery.service b/deployment/systemctl/djangoaio-celery.service deleted file mode 100644 index 4e7d4b6..0000000 --- a/deployment/systemctl/djangoaio-celery.service +++ /dev/null @@ -1,21 +0,0 @@ -[Unit] -Description=Django-AIO Celery Worker -Requires=postgresql.service -After=network.target postgresql.service - -[Service] -Type=simple -SyslogIdentifier=djangoaiocelery -PermissionsStartOnly=true -User=username -Group=username -Environment="DJANGO_SETTINGS_MODULE=config.settings.pro" -WorkingDirectory=/home/username/projects/django-aio -ExecStart=/home/username/projects/django-aio/env/bin/celery worker -A main.taskapp -l info #--autoscale=10,3 -StandardOutput=file:/home/username/projects/django-aio/logs/worker.log -StandardError=file:/home/username/projects/django-aio/logs/worker.log -StandardOutput=journal+console -Restart=on-failure - -[Install] -WantedBy=multi-user.target \ No newline at end of file diff --git a/deployment/systemctl/djangoaio-celerybeat.service b/deployment/systemctl/djangoaio-celerybeat.service deleted file mode 100644 index ae15523..0000000 --- a/deployment/systemctl/djangoaio-celerybeat.service +++ /dev/null @@ -1,21 +0,0 @@ -[Unit] -Description=Django-AIO Celery Beat -Requires=postgresql.service -After=network.target postgresql.service - -[Service] -Type=simple -SyslogIdentifier=djangoaiocelerybeat -PermissionsStartOnly=true -User=username -Group=username -Environment="DJANGO_SETTINGS_MODULE=config.settings.pro" -WorkingDirectory=/home/username/projects/django-aio -ExecStart=/home/username/projects/django-aio/env/bin/celery beat -A main.taskapp -l info -StandardOutput=file:/home/username/projects/django-aio/logs/beat.log -StandardError=file:/home/username/projects/django-aio/logs/beat.log -StandardOutput=journal+console -Restart=on-failure - -[Install] -WantedBy=multi-user.target \ No newline at end of file diff --git a/deployment/systemctl/djangoaio-daphne.service b/deployment/systemctl/djangoaio-daphne.service deleted file mode 100644 index 97de1ff..0000000 --- a/deployment/systemctl/djangoaio-daphne.service +++ /dev/null @@ -1,21 +0,0 @@ -[Unit] -Description=Django-AIO Daphne -Requires=postgresql.service -After=network.target postgresql.service - -[Service] -Type=simple -SyslogIdentifier=djangoaiodaphne -PermissionsStartOnly=true -User=username -Group=username -Environment="DJANGO_SETTINGS_MODULE=config.settings.pro" -WorkingDirectory=/home/username/projects/django-aio -ExecStart=/home/username/projects/django-aio/env/bin/daphne -b 0.0.0.0 -p 8000 config.asgi:application -StandardOutput=file:/home/username/projects/django-aio/logs/daphne.log -StandardError=file:/home/username/projects/django-aio/logs/daphne.log -StandardOutput=journal+console -Restart=on-failure - -[Install] -WantedBy=multi-user.target \ No newline at end of file diff --git a/deployment/systemctl/djangoaio-gunicorn.service b/deployment/systemctl/djangoaio-gunicorn.service deleted file mode 100644 index b3684d4..0000000 --- a/deployment/systemctl/djangoaio-gunicorn.service +++ /dev/null @@ -1,21 +0,0 @@ -[Unit] -Description=Django-AIO Gunicorn -Requires=postgresql.service -After=network.target postgresql.service - -[Service] -Type=simple -SyslogIdentifier=djangoaiogunicorn -PermissionsStartOnly=true -User=username -Group=username -Environment="DJANGO_SETTINGS_MODULE=config.settings.pro" -WorkingDirectory=/home/username/projects/django-aio -ExecStart=/home/username/projects/django-aio/env/bin/gunicorn --bind 0.0.0.0:8000 config.wsgi -w 3 --timeout=120 --log-level=error -StandardOutput=file:/home/username/projects/django-aio/logs/gunicorn.log -StandardError=file:/home/username/projects/django-aio/logs/gunicorn.log -StandardOutput=journal+console -Restart=on-failure - -[Install] -WantedBy=multi-user.target \ No newline at end of file diff --git a/docker-compose-alpine.yml b/docker-compose-alpine.yml deleted file mode 100644 index 2c96005..0000000 --- a/docker-compose-alpine.yml +++ /dev/null @@ -1,36 +0,0 @@ -version: '3.7' - -services: - jasmin_web: - #image: tarekaec/jasmin_web_panel:1.0 - image: tarekaec/jasmin_web_panel:1.0-alpine - ports: - - "8000:8000" - deploy: - replicas: 1 - env_file: - - .env - environment: - JASMIN_PORT: 8000 - healthcheck: - disable: true - volumes: - - ./public:/web/public - entrypoint: /jasmin/docker-entrypoint.sh - jasmin_celery: - # image: tarekaec/jasmin_web_panel:1.0 - image: tarekaec/jasmin_web_panel:1.0-alpine - deploy: - replicas: 1 - env_file: - - .env - environment: - DEBUG: 0 - healthcheck: - disable: true - depends_on: - - jasmin_redis - entrypoint: /jasmin/celery_run.sh - jasmin_redis: - image: redis:alpine - tty: true diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh deleted file mode 100755 index 26a277d..0000000 --- a/docker-entrypoint.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -cd $JASMIN_HOME - -python manage.py migrate -python manage.py load_new -python manage.py collectstatic --noinput --clear --no-post-process - -gunicorn config.wsgi:application --workers 4 -b :$JASMIN_PORT --log-level info --worker-class=gevent --reload diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 1a81bb2..0000000 --- a/docs/README.md +++ /dev/null @@ -1,240 +0,0 @@ -# Installing Jasmin SMS Gateway on Ubuntu 20.04 LTS Server - -- You need to install `python3.6` version to get Jasmin SMS Gateway works properly. - -```shell -sudo add-apt-repository ppa:deadsnakes/ppa -sudo apt update -sudo apt install python3.6 python3.6-dev python3.6-venv -``` - -## 1. Preparing your system -Login to your machine as a sudo user and update the system to the latest packages: - -```shell -sudo apt update && sudo apt -y upgrade -``` - -Install Git, PIP, NodeJS and the tools required to build dependencies: -```shell -sudo apt install git python3-pip build-essential wget python3-dev python3-venv python3-wheel python3-setuptools libffi-dev libssl-dev python3-twisted virtualenv -``` - -Install **RabbitMQ** and **Redis server** -```shell -sudo apt install rabbitmq-server redis-server -``` - -## 2. Create Jasmin user -Create a new system user named jasmin with home directory /jasmin using the following command: -```shell -sudo useradd -m -d /jasmin -U -r -s /bin/bash jasmin -``` - -## 3. Install and Configure Jasmin -Before starting with the installation process, change to user "jasmin": -```shell -sudo su - jasmin -cd ~ -``` -Create jasmin virtualenv -```shell -virtualenv -p python3.6 jasmin -``` -you will get path `/jasmin/jasmin` - -Next, activate the environment with the following command: -```shell -source jasmin/bin/activate -``` -Install all required Python modules with pip: -```shell -pip install jasmin -``` -> Note: If you encounter any compilation errors during the installation, make sure that you installed all of the required dependencies listed in the Before you begin section. - - -## 4. Create a Systemd Unit File -To run jasmin as a service we need to create a service unit file in the `/etc/systemd/system/` directory. - -Open your text editor and paste the following configuration: - -### a. Create `jasmind.service` Service - -```shell -sudo nano /etc/systemd/system/jasmind.service -``` -In directory: /etc/systemd/system -```editorconfig -[Unit] -Description=Jasmin SMS Gateway -Requires=network.target jasmin-interceptord.service jasmin-dlrd.service jasmin-dlrlookupd.service -After=network.target - -[Service] -SyslogIdentifier=jasmind -PIDFile=/run/jasmind.pid -User=jasmin -Group=jasmin -ExecStart=/jasmin/jasmin/bin/jasmind.py --username jcliadmin --password jclipwd - -[Install] -WantedBy=multi-user.target -``` -### b. Create `jasmin-celery.service` Service - -```shell -sudo nano /etc/systemd/system/jasmin-celery.service -``` -In directory: /etc/systemd/system -```editorconfig -[Unit] -Description=Jasmin Celery server -Requires=network.target jasmin-restapi.service -After=network.target -Before=jasmin-restapi.service - -[Service] -SyslogIdentifier=jasmin-celery -PIDFile=/run/jasmin-celery.pid -User=jasmin -Group=jasmin -ExecStart=/bin/sh -c "/jasmin/jasmin/bin/celery -A jasmin.protocols.rest.tasks worker -l INFO -c 4 --autoscale=10,3" - -[Install] -WantedBy=multi-user.target -``` - -### c. Create `jasmin-dlrd` Service - -```shell -sudo nano /etc/systemd/system/jasmin-dlrd.service -``` -In directory: /etc/systemd/system -```editorconfig -[Unit] -Description=Jasmin SMS Gateway DLR throwing standalone daemon -Requires=network.target jasmind.service -After=network.target jasmind.service - -[Service] -SyslogIdentifier=jasmin-dlrd -PIDFile=/run/jasmin-dlrd.pid -User=jasmin -Group=jasmin -ExecStart=/jasmin/jasmin/bin/dlrd.py - -[Install] -WantedBy=multi-user.target -``` - -### d. Create `jasmin-dlrlookupd.service` Service - -```shell -sudo nano /etc/systemd/system/jasmin-dlrlookupd.service -``` -In directory: /etc/systemd/system -```editorconfig -[Unit] -Description=Jasmin SMS Gateway DLR lookup standalone daemon -Requires=network.target jasmind.service -After=network.target jasmind.service - -[Service] -SyslogIdentifier=jasmin-dlrlookupd -PIDFile=/run/jasmin-dlrlookupd.pid -User=jasmin -Group=jasmin -ExecStart=/jasmin/jasmin/bin/dlrlookupd.py - -[Install] -WantedBy=multi-user.target -``` - -### e. Create `jasmin-interceptord.service` Service - -```shell -sudo nano /etc/systemd/system/jasmin-interceptord.service -``` -In directory: /etc/systemd/system -```editorconfig -[Unit] -Description=Jasmin SMS Gateway interceptor -Requires=network.target jasmind.service -After=network.target -Before=jasmind.service - -[Service] -SyslogIdentifier=interceptord -PIDFile=/run/interceptord.pid -User=jasmin -Group=jasmin -ExecStart=/jasmin/jasmin/bin/interceptord.py - -[Install] -WantedBy=multi-user.target -``` - -### f. Create `jasmin-restapi.service` Service - -Create symlink for twisted main file. -```shell -sudo -u jasmin ln -s /jasmin/jasmin/bin/twistd /jasmin/jasmin/twistd3 -``` -In directory: /etc/systemd/system -```shell -sudo nano /etc/systemd/system/jasmin-restapi.service -``` - -```editorconfig -[Unit] -Description=Jasmin SMS Restful API server -Requires=network.target jasmind.service jasmin-celery.service -After=network.target jasmind.service - -[Service] -SyslogIdentifier=jasmin-restapi -PIDFile=/run/jasmin-restapi.pid -User=jasmin -Group=jasmin -ExecStart=/bin/sh -c "/jasmin/jasmin/twistd3 -n --pidfile=/tmp/twistd-web-restapi.pid web --wsgi=jasmin.protocols.rest.api" - -[Install] -WantedBy=multi-user.target -``` - -Make directory for logs: - -```shell -mkdir /var/log/jasmin && chown -R jasmin:jasmin /var/log/jasmin -``` - -Reload systemctl - -```shell -sudo systemctl daemon-reload -``` - -Now, you can enable Jasmin services: - -```shell -systemctl enable jasmin-{celery,dlrd,dlrlookupd,interceptord,restapi}.service jasmind.service -``` - -You could start all Jasmin services: - -```shell -systemctl start jasmin-{celery,dlrd,dlrlookupd,interceptord,restapi}.service jasmind.service -``` - -To ensure web app running without issue: - -```shell -systemctl list-unit-files | grep jasmin -``` - -To check the running and failed daemons: - -```shell -systemctl list-units | grep jasmin -``` diff --git a/load_data.sh b/load_data.sh deleted file mode 100755 index b0e2907..0000000 --- a/load_data.sh +++ /dev/null @@ -1,21 +0,0 @@ -ENVIROO=./env/bin/python -MANAGER=manage.py - -if [ "$1" == "--init" ] || [ "$1" == "-i" ]; then - echo "- Deleting sqlite database if exist ..." - rm -rf db.sqlite3 - echo "- Reset all migrations files ..." - $ENVIROO $MANAGER reseter - echo "- Make new migrations files ..." - $ENVIROO $MANAGER makemigrations - echo "- Migrate database ..." - $ENVIROO $MANAGER migrate - echo "- Loading new data samples ..." - $ENVIROO $MANAGER load_new -fi - -if [ "$2" == "--start" ] || [ "$2" == "-s" ]; then - RUN_PROJECT="$ENVIROO $MANAGER runserver 0.0.0.0:8000" - echo $RUN_PROJECT - $RUN_PROJECT -fi \ No newline at end of file diff --git a/load_data_win.bat b/load_data_win.bat deleted file mode 100644 index 5dbe1a8..0000000 --- a/load_data_win.bat +++ /dev/null @@ -1,34 +0,0 @@ -@ECHO OFF -set ENVIROO=python - -set MANAGER=manage.py - -CALL env\Scripts\activate - -set init_result=F -if "%~1"=="--init" set init_result=T -if "%~1"=="-i" set init_result=T -if "%init_result%"=="T" ( - echo - Deleting sqlite database if exist ... - del /F db.sqlite3 - echo - Reset all migrations files ... - %ENVIROO% %MANAGER% reseter - echo - Make new migrations files ... - %ENVIROO% %MANAGER% makemigrations core - %ENVIROO% %MANAGER% makemigrations users - %ENVIROO% %MANAGER% makemigrations notify - %ENVIROO% %MANAGER% makemigrations - echo - Migrate database ... - %ENVIROO% %MANAGER% migrate - echo - Loading new data samples ... - %ENVIROO% %MANAGER% load_new -) - -set start_result=F -if "%~2"=="--start" set start_result=T -if "%~2"=="-s" set start_result=T -if "%start_result%"=="T" ( - set RUN_PROJECT=%ENVIROO% %MANAGER% runserver 0.0.0.0:8000 - echo %RUN_PROJECT% - CALL %RUN_PROJECT% -) \ No newline at end of file diff --git a/main/api/urls.py b/main/api/urls.py index c1a4bde..9682b76 100644 --- a/main/api/urls.py +++ b/main/api/urls.py @@ -12,4 +12,5 @@ path('/', view=views.groups_detail, name='groups_detail'), path('', view=views.groups_list, name='groups_list'), ])), + path('health_check', view=views.health_check, name="health_check") ]) diff --git a/main/api/views/__init__.py b/main/api/views/__init__.py index 1923002..e8a1afe 100644 --- a/main/api/views/__init__.py +++ b/main/api/views/__init__.py @@ -3,4 +3,5 @@ groups_detail, groups_enable, groups_disable, -) \ No newline at end of file +) +from .health_check import health_check \ No newline at end of file diff --git a/main/api/views/health_check.py b/main/api/views/health_check.py new file mode 100644 index 0000000..462f190 --- /dev/null +++ b/main/api/views/health_check.py @@ -0,0 +1,7 @@ +from django.http import JsonResponse + +from config.version import VERSION + + +def health_check(request): + return JsonResponse({"version": VERSION}) diff --git a/main/core/context_processors.py b/main/core/context_processors.py index b1adbb3..fe1c64e 100644 --- a/main/core/context_processors.py +++ b/main/core/context_processors.py @@ -1,10 +1,10 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals, absolute_import -from django.utils.translation import gettext_lazy as _ from django.conf import settings +from config.version import VERSION + def site(request): return { "SETTINGS": settings, + "VERSION": VERSION, } diff --git a/main/core/middleware.py b/main/core/middleware.py index e419abe..2694ab3 100644 --- a/main/core/middleware.py +++ b/main/core/middleware.py @@ -18,7 +18,7 @@ def __init__(self, get_response): def __call__(self, request): def is_ajax(self): # noqa - return request.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest' + return request.headers.get('x-requested-with') == 'XMLHttpRequest' request.is_ajax = is_ajax.__get__(request) response = self.get_response(request) diff --git a/main/static/assets/img/django-aio.png b/main/static/assets/img/django-aio.png deleted file mode 100644 index 0f586df32890e44d0fa3b230c3cda918288b6331..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3862 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 Ye^C@6>v)lYrT_o{07*qoM6N<$g3gCnMF0Q* diff --git a/main/static/assets/img/jasmin.jpg b/main/static/assets/img/jasmin.jpg new file mode 100644 index 0000000000000000000000000000000000000000..37aa4233447e9be2f3e29e5ea84623ca488cfde4 GIT binary patch literal 5450 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 literal 0 HcmV?d00001 diff --git a/main/static/assets/img/jasmin.png b/main/static/assets/img/jasmin.png new file mode 100644 index 0000000000000000000000000000000000000000..4543c68e5a58fd036bc8604bc6cfe5bed6fff891 GIT binary patch literal 29234 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 - + {% block title %}{% trans "Sign In" %}{% endblock title %} @@ -8,6 +8,10 @@ + {% block extracss %}{% endblock extracss %} @@ -16,7 +20,17 @@
- {% block content %}{% endblock content %} +
+
+
+ Login +

v{{ VERSION }}

+
+
+
+ {% block content %}{% endblock content %} +
+
diff --git a/main/web/templates/auth/reset.html b/main/web/templates/auth/reset.html index edd9f0d..6e76ab4 100644 --- a/main/web/templates/auth/reset.html +++ b/main/web/templates/auth/reset.html @@ -1,7 +1,19 @@ {% extends "auth/base.html" %} {% load static i18n %} -{% block title %}{% trans "Reset Password" %}{% endblock title %} -{% block extracss %}{% endblock extracss %} +{% block title %}{% trans "Reset Password" %} - Jasmin Web Panel{% endblock title %} {% block content %} +
+
+

{% trans 'Reset your password' %}

+
+ {% include "web/includes/message.html" %} +
+ {% csrf_token %} +
+ +
+ +
+
+
{% trans "Have an account?" %} {% trans 'Sign in' %}
{% endblock content %} -{% block extrajs %}{% endblock extrajs %} \ No newline at end of file diff --git a/main/web/templates/auth/signin.html b/main/web/templates/auth/signin.html index c6ed2bb..c3cad68 100644 --- a/main/web/templates/auth/signin.html +++ b/main/web/templates/auth/signin.html @@ -1,49 +1,26 @@ {% extends "auth/base.html" %} {% load static i18n %} -{% block title %}{% trans "Sign In" %}{% endblock title %} +{% block title %}{% trans "Sign In" %} - Jasmin Web Panel{% endblock title %} {% block extracss %}{% endblock extracss %} {% block content %} -
-
+
+
+
+ {% trans "Jasmin SMS Gateway" %} +
+ {{ SETTINGS.TELNET_HOST }} - {{ SETTINGS.TELNET_PORT }} - +
+
+
+
{% endblock content %} -{% block extrajs %}{% endblock extrajs %} \ No newline at end of file +{% block extrajs %} + +{% endblock extrajs %} \ No newline at end of file diff --git a/main/web/urls.py b/main/web/urls.py index df4b08c..5c7bf98 100644 --- a/main/web/urls.py +++ b/main/web/urls.py @@ -1,4 +1,3 @@ -# -*- encoding: utf-8 -*- from django.urls import path from .views import * @@ -6,23 +5,22 @@ app_name = 'web' urlpatterns = [ - path('filters/manage/', filters_view_manage, name='filters_view_manage'), - path('filters/', filters_view, name='filters_view'), - path('groups/manage/', groups_view_manage, name='groups_view_manage'), - path('groups/', groups_view, name='groups_view'), - path('httpccm/manage/', httpccm_view_manage, name='httpccm_view_manage'), - path('httpccm/', httpccm_view, name='httpccm_view'), - path('morouter/manage/', morouter_view_manage, name='morouter_view_manage'), - path('morouter/', morouter_view, name='morouter_view'), - path('mtrouter/manage/', mtrouter_view_manage, name='mtrouter_view_manage'), - path('mtrouter/', mtrouter_view, name='mtrouter_view'), - path('smppccm/manage/', smppccm_view_manage, name='smppccm_view_manage'), - path('smppccm/', smppccm_view, name='smppccm_view'), - path('submit_logs/manage/', submit_logs_view_manage, name='submit_logs_view_manage'), - path('submit_logs/', submit_logs_view, name='submit_logs_view'), - path('users/manage/', users_view_manage, name='users_view_manage'), - path('users/', users_view, name='users_view'), - path('manage/', global_manage, name='global_manage'), - path('', dashboard_view, name='dashboard_view'), - # path('', welcome_view, name='welcome_view'), -] \ No newline at end of file + path('filters/manage/', filters_view_manage, name='filters_view_manage'), + path('filters/', filters_view, name='filters_view'), + path('groups/manage/', groups_view_manage, name='groups_view_manage'), + path('groups/', groups_view, name='groups_view'), + path('httpccm/manage/', httpccm_view_manage, name='httpccm_view_manage'), + path('httpccm/', httpccm_view, name='httpccm_view'), + path('morouter/manage/', morouter_view_manage, name='morouter_view_manage'), + path('morouter/', morouter_view, name='morouter_view'), + path('mtrouter/manage/', mtrouter_view_manage, name='mtrouter_view_manage'), + path('mtrouter/', mtrouter_view, name='mtrouter_view'), + path('smppccm/manage/', smppccm_view_manage, name='smppccm_view_manage'), + path('smppccm/', smppccm_view, name='smppccm_view'), + path('submit_logs/manage/', submit_logs_view_manage, name='submit_logs_view_manage'), + path('submit_logs/', submit_logs_view, name='submit_logs_view'), + path('users/manage/', users_view_manage, name='users_view_manage'), + path('users/', users_view, name='users_view'), + path('manage/', global_manage, name='global_manage'), + path('', dashboard_view, name='dashboard_view'), +] diff --git a/main/web/views/__init__.py b/main/web/views/__init__.py index b6a786f..9677990 100644 --- a/main/web/views/__init__.py +++ b/main/web/views/__init__.py @@ -1,2 +1,2 @@ from .content import * -from .home import dashboard_view, welcome_view, global_manage \ No newline at end of file +from .home import dashboard_view, global_manage diff --git a/main/web/views/content/filters.py b/main/web/views/content/filters.py index 19148a6..05a723e 100644 --- a/main/web/views/content/filters.py +++ b/main/web/views/content/filters.py @@ -5,7 +5,7 @@ from django.http import JsonResponse, HttpResponse from django.contrib.auth.decorators import login_required from django.contrib import messages -from django.utils import timezone as djtz +from django.utils import timezone from django.conf import settings import json diff --git a/main/web/views/content/groups.py b/main/web/views/content/groups.py index 4b978c2..deca663 100644 --- a/main/web/views/content/groups.py +++ b/main/web/views/content/groups.py @@ -5,7 +5,7 @@ from django.http import JsonResponse, HttpResponse from django.contrib.auth.decorators import login_required from django.contrib import messages -from django.utils import timezone as djtz +from django.utils import timezone from django.conf import settings import json diff --git a/main/web/views/content/httpccm.py b/main/web/views/content/httpccm.py index 7c5d0cd..a585586 100644 --- a/main/web/views/content/httpccm.py +++ b/main/web/views/content/httpccm.py @@ -1,12 +1,7 @@ -# -*- encoding: utf-8 -*- -from __future__ import unicode_literals from django.utils.translation import gettext_lazy as _ from django.shortcuts import render, redirect, get_object_or_404 from django.http import JsonResponse, HttpResponse from django.contrib.auth.decorators import login_required -from django.contrib import messages -from django.utils import timezone as djtz -from django.conf import settings import json diff --git a/main/web/views/content/morouter.py b/main/web/views/content/morouter.py index e64933a..62a8f59 100644 --- a/main/web/views/content/morouter.py +++ b/main/web/views/content/morouter.py @@ -5,7 +5,7 @@ from django.http import JsonResponse, HttpResponse from django.contrib.auth.decorators import login_required from django.contrib import messages -from django.utils import timezone as djtz +from django.utils import timezone from django.conf import settings import json diff --git a/main/web/views/content/mtrouter.py b/main/web/views/content/mtrouter.py index 96cdf2d..a1505cd 100644 --- a/main/web/views/content/mtrouter.py +++ b/main/web/views/content/mtrouter.py @@ -5,7 +5,7 @@ from django.http import JsonResponse, HttpResponse from django.contrib.auth.decorators import login_required from django.contrib import messages -from django.utils import timezone as djtz +from django.utils import timezone from django.conf import settings import json diff --git a/main/web/views/home.py b/main/web/views/home.py index 50ccb45..32bef91 100644 --- a/main/web/views/home.py +++ b/main/web/views/home.py @@ -1,17 +1,16 @@ -# -*- encoding: utf-8 -*- -import json import traceback +import logging +import pexpect from subprocess import getoutput from django.utils.translation import gettext_lazy as _ -from django.shortcuts import render, redirect, get_object_or_404 -from django.http import JsonResponse, HttpResponse +from django.shortcuts import render +from django.http import JsonResponse from django.contrib.auth.decorators import login_required from django.conf import settings -from main.core.utils import get_client_ip +from main.core.utils import get_client_ip, is_online -import logging logger = logging.getLogger(__name__) @@ -21,21 +20,12 @@ def dashboard_view(request): return render(request, "web/dashboard.html", dict(ip_address=ip_address)) -def welcome_view(request): - import django, sys - python_version = "{}.{}.{}".format(sys.version_info.major, sys.version_info.minor, sys.version_info.micro) - return render(request, "web/welcome.html", dict( - django_version=django.get_version(), - python_version=python_version, - platform=sys.platform, - )) - - def global_manage(request): - args, res_status, res_message = {}, 400, _("Sorry, Command does not matched.") + ctx, res_status, res_message = {}, 400, _("Sorry, Command does not matched.") if request.GET and request.is_ajax(): s = request.GET.get("s") if s == "systemctl_services_state" and settings.SYSCTL_HEALTH_CHECK: + # THIS ONLY WORKS WITH SYSTEMCTL service_states = dict() for service in settings.SYSCTL_HEALTH_CHECK_SERVICES: try: @@ -46,10 +36,11 @@ def global_manage(request): service_states[service] = status except Exception as e: logger.info(f"Error occurred: {e}") - args["service_states"] = service_states - if isinstance(args, dict): - args["status"] = res_status - args["message"] = str(res_message) - else: - res_status = 200 - return HttpResponse(json.dumps(args), status=res_status, content_type="application/json") + ctx["service_states"] = service_states + if s == "gw_state": + # CHECK GATEWAY BINDING OK + res_status, res_message = is_online(host=settings.TELNET_HOST, port=settings.TELNET_PORT) + if isinstance(ctx, dict): + ctx["status"] = res_status + ctx["message"] = res_message + return JsonResponse(ctx, status=200) From 672874db9f7ba417e4c62957c638d347c7c42829 Mon Sep 17 00:00:00 2001 From: Tarek Kalaji Date: Wed, 28 Jun 2023 11:48:21 +0300 Subject: [PATCH 07/14] fixing url broker for celery --- config/settings/com.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/settings/com.py b/config/settings/com.py index ed43ffb..3732d20 100644 --- a/config/settings/com.py +++ b/config/settings/com.py @@ -210,3 +210,6 @@ SYSCTL_HEALTH_CHECK_SERVICES = os.environ.get("SYSCTL_HEALTH_CHECK_SERVICES", default="jasmind") DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' + +CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", default="redis://localhost:6379/0") +CELERY_RESULT_BACKEND = os.environ.get("CELERY_RESULT_BACKEND", default="redis://localhost:6379/0") From 3d794ebd77800d52c595969c2edf33c57e9b95c8 Mon Sep 17 00:00:00 2001 From: Tarek Kalaji Date: Sat, 1 Jul 2023 02:02:54 +0300 Subject: [PATCH 08/14] addings django.sites, adding reset password and email endpoints --- config/settings/com.py | 9 ++- main/core/notify/__init__.py | 1 + main/core/notify/mail_sender.py | 55 ++++++++++++++ main/core/templates/core/mail_base.html | 45 +++++++++++ .../core/templates/core/mail_reset_email.html | 66 ++++++++++++++++ .../templates/core/mail_reset_password.html | 68 +++++++++++++++++ main/core/utils/__init__.py | 23 +++--- main/core/utils/common.py | 16 +++- main/core/utils/tokens.py | 22 +++--- main/users/urls.py | 2 + main/users/views/__init__.py | 2 +- main/users/views/reset.py | 75 +++++++++++++++++++ 12 files changed, 357 insertions(+), 27 deletions(-) create mode 100644 main/core/notify/__init__.py create mode 100644 main/core/notify/mail_sender.py create mode 100644 main/core/templates/core/mail_base.html create mode 100644 main/core/templates/core/mail_reset_email.html create mode 100644 main/core/templates/core/mail_reset_password.html diff --git a/config/settings/com.py b/config/settings/com.py index 3732d20..0781d34 100644 --- a/config/settings/com.py +++ b/config/settings/com.py @@ -28,6 +28,7 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.humanize', + "django.contrib.sites", # 'channels', 'crequest', # noqa @@ -48,6 +49,7 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.contrib.sites.middleware.CurrentSiteMiddleware", 'crequest.middleware.CrequestMiddleware', # noqa 'main.core.middleware.AjaxMiddleware', 'main.core.middleware.TelnetConnectionMiddleware', @@ -114,10 +116,13 @@ USE_TZ = True -SITE_TITLE = "Jasmin Web site admin" -SITE_HEADER = "Jasmin Web administration" +SITE_TITLE = "Jasmin site admin" +SITE_HEADER = "Jasmin administration" INDEX_TITLE = "Dashboard administration" +SITE_NAME = os.environ.get("SITE_NAME", default="Jasmin Panel") +SITE_NAME_HTML = os.environ.get("SITE_NAME_HTML", default="Jasmin Panel") + MESSAGE_TAGS = { message_constants.DEBUG: 'info', message_constants.INFO: 'info', diff --git a/main/core/notify/__init__.py b/main/core/notify/__init__.py new file mode 100644 index 0000000..83bb226 --- /dev/null +++ b/main/core/notify/__init__.py @@ -0,0 +1 @@ +from .mail_sender import send_mail_reset_password, send_mail_reset_email diff --git a/main/core/notify/mail_sender.py b/main/core/notify/mail_sender.py new file mode 100644 index 0000000..be47724 --- /dev/null +++ b/main/core/notify/mail_sender.py @@ -0,0 +1,55 @@ +import logging + +from django.utils.translation import gettext_lazy as _ +from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode +from django.utils.encoding import force_bytes +from django.urls import reverse +from django.conf import settings + +from main.core.tasks import mail_html_mails +from main.core.utils import get_current_site, reset_password_token, email_active_token + +logger = logging.getLogger(__name__) + + +def send_mail_reset_email(request) -> bool: + if request.is_authenticated: + uidb64 = urlsafe_base64_encode(force_bytes(request.user.pk)).decode() # noqa + token = email_active_token.make_token(request.user) + subject = str(_("Verify your email address")) + template_name = "core/mail_reset_email.html" + current_site = get_current_site(request=request) + reset_email_url = reverse("users:email_verification_view", kwargs={"uidb64": uidb64, "token": token}) # noqa + reset_email_confirm = f"{current_site}{reset_email_url}" + details = { + "email": request.user.email, + "welcome_message": _("Verify your email address"), + "site_url": current_site, + "reset_email_confirm": reset_email_confirm, + "current_site": current_site, + } + mail_html_mails.delay([request.user.email], subject, template_name, details, settings.LANGUAGE_CODE) + return True + else: + return False + + +def send_mail_reset_password(request, user) -> bool: + current_site = get_current_site(request=request) + uidb64 = urlsafe_base64_encode(force_bytes(user.pk)).decode() # noqa + token = reset_password_token.make_token(user=user) + reset_password_url = reverse("users:reset_password_view", kwargs={"uidb64": uidb64, "token": token}) # noqa + reset_password_confirm = f"{current_site}{reset_password_url}" + site_name = settings.SITE_NAME + details = { + "email": user.email, + "welcome_message": _("Reset your password on %(site_name)s") % {"site_name": site_name}, + "site_url": current_site, + "site_name": site_name, + "reset_password_confirm": reset_password_confirm, + "current_site": current_site, + } + subject = str(_("%(site_name)s Reset Password") % {"site_name": site_name}) + template_name = "core/mail_reset_password.html" + mail_html_mails.delay([user.email], subject, template_name, details, settings.LANGUAGE_CODE) + return True diff --git a/main/core/templates/core/mail_base.html b/main/core/templates/core/mail_base.html new file mode 100644 index 0000000..8350e95 --- /dev/null +++ b/main/core/templates/core/mail_base.html @@ -0,0 +1,45 @@ +{% load i18n %} + + + + + Email + + + + + + {% block content1 %}{% endblock content1 %} + +
+ + \ No newline at end of file diff --git a/main/core/templates/core/mail_reset_email.html b/main/core/templates/core/mail_reset_email.html new file mode 100644 index 0000000..bf4f639 --- /dev/null +++ b/main/core/templates/core/mail_reset_email.html @@ -0,0 +1,66 @@ +{% extends "core/mail_base.html" %} +{% load i18n %} +{% block content1 %} + + + +
+ + + + + + + + + +
+ LOGO GOES HERE +
+ + + + + + + + + + + + + + + + + + +
+

{{ welcome_message }}

+
+ {% trans "Please confirm your account by clicking the button below:" %} +
+ {% trans "Confirm Your account" %} +
+

{% trans "Once confirmed, you can login to the website anytime " %}

+
+
+ {% trans "Best wishes," %}
{{ site_name }} +
+
+
+ + + + + + +
+ {{ site_name }} +
+
+
+ + + +{% endblock content1 %} diff --git a/main/core/templates/core/mail_reset_password.html b/main/core/templates/core/mail_reset_password.html new file mode 100644 index 0000000..6263a7e --- /dev/null +++ b/main/core/templates/core/mail_reset_password.html @@ -0,0 +1,68 @@ +{% extends "core/mail_base.html" %} +{% load i18n %} +{% block content1 %} + + + +
+ + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + +
+

{{ welcome_message }}

+ {% trans "Glad to have you on board." %} +
+ {% trans "Please confirm your account by clicking the button below:" %} +
+ {% trans "Confirm Email" %} +
+

{% trans "Once confirmed, you'll be able to log in to system. with your new account." %}

+
+
+ {% trans "Best wishes," %}
{{ site_name }} +
+
+
+ + + + + + +
+ {{ site_name }} +
+
+
+ + + +{% endblock content1 %} + \ No newline at end of file diff --git a/main/core/utils/__init__.py b/main/core/utils/__init__.py index 07978e8..438b7bd 100644 --- a/main/core/utils/__init__.py +++ b/main/core/utils/__init__.py @@ -1,18 +1,19 @@ from .boolean import is_date, is_decimal, is_float, is_int, is_json from .common import ( - get_query, - timestamp2datetime, - readable_date_format, - get_client_ip, - str2date, - display_form_validations, - shorten_large_number, - password_generator, - paginate, - is_online, + get_query, + timestamp2datetime, + readable_date_format, + get_client_ip, + str2date, + display_form_validations, + shorten_large_number, + password_generator, + paginate, + is_online, + get_current_site, ) from .cryptograph import * from .json_encoder import LazyEncoder from .tokens import email_active_token, reset_password_token from .vars import USER_SEARCH_FIELDS -from .user_agent import get_user_agent, get_and_set_user_agent \ No newline at end of file +from .user_agent import get_user_agent, get_and_set_user_agent diff --git a/main/core/utils/common.py b/main/core/utils/common.py index a75a42b..bbc6d72 100644 --- a/main/core/utils/common.py +++ b/main/core/utils/common.py @@ -2,7 +2,7 @@ import re import string import socket -from typing import Tuple +from typing import Tuple, Optional from dateutil.parser import parse from django.contrib import messages @@ -10,6 +10,8 @@ from django.db.models import Q from django.utils import timezone from django.utils.dateformat import DateFormat +from django.core.exceptions import ImproperlyConfigured +from django.contrib.sites.models import Site from .boolean import is_date @@ -37,7 +39,8 @@ def str2date(date_string, lang="en"): "tr": True, "ar": False, } - return timezone.make_aware(parse(date_string, dayfirst=lang_day_first[lang])) if is_date(date_string) else timezone.now() + return timezone.make_aware(parse(date_string, dayfirst=lang_day_first[lang])) if is_date( + date_string) else timezone.now() def paginate(objects, per_page=24, page=1): @@ -127,3 +130,12 @@ def is_online(host: str, port: int) -> Tuple[bool, str]: except Exception as e: msg = str(e) return False, msg + + +def get_current_site(request=None) -> Optional[str]: + try: + current_site = Site.objects.get_current() + return current_site.domain.strip("/") + except (Site.DoestNotExist, ImproperlyConfigured,): + pass + return None diff --git a/main/core/utils/tokens.py b/main/core/utils/tokens.py index 203fab4..bfdfa9e 100644 --- a/main/core/utils/tokens.py +++ b/main/core/utils/tokens.py @@ -1,26 +1,26 @@ -# -*- encoding: utf-8 -*- -from __future__ import unicode_literals -from django.utils.translation import gettext_lazy as _ from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.conf import settings + class EmailActiveTokenGenerator(PasswordResetTokenGenerator): def _make_hash_value(self, user, timestamp): return ( - str(user.pk) + str(timestamp) + - str(user.email) + str(user.pin) + - str(user.last_name) + str(user.first_name) + str(user.pk) + str(timestamp) + + str(user.email) + str(user.pin) + + str(user.last_name) + str(user.first_name) ) + email_active_token = EmailActiveTokenGenerator() -class RestPassswordTokenGenertorclass(PasswordResetTokenGenerator): +class RestPasswordTokenGenerator(PasswordResetTokenGenerator): def _make_hash_value(self, user, timestamp): return ( - str(user.pk) + str(timestamp) + - str(user.email) + str(user.pin) + - str(user.last_name) + str(settings.SECRET_KEY) + str(timestamp) + str(user.pk) + str(timestamp) + + str(user.email) + str(user.pin) + + str(user.last_name) + str(settings.SECRET_KEY) + str(timestamp) ) -reset_password_token = RestPassswordTokenGenertorclass() + +reset_password_token = RestPasswordTokenGenerator() diff --git a/main/users/urls.py b/main/users/urls.py index 7b5cc7b..b8cd760 100644 --- a/main/users/urls.py +++ b/main/users/urls.py @@ -9,5 +9,7 @@ path(route='activity_log/', view=activity_log_view, name='activity_log_view'), path(route='login/', view=signin_view, name="signin_view"), path(route='logout/', view=logout_view, name="logout_view"), + path(route='verify-email///', view=email_verification_view, name='email_verification_view'), + path(route='reset///', view=reset_password_view, name="reset_password_view"), # noqa path(route='reset/', view=reset_view, name="reset_view"), ] diff --git a/main/users/views/__init__.py b/main/users/views/__init__.py index a8abb30..103de96 100644 --- a/main/users/views/__init__.py +++ b/main/users/views/__init__.py @@ -1,3 +1,3 @@ from .profile import profile_view, settings_view, activity_log_view -from .reset import reset_view +from .reset import reset_view, reset_password_view, email_verification_view from .signin import signin_view, logout_view diff --git a/main/users/views/reset.py b/main/users/views/reset.py index 4cd8272..030c659 100644 --- a/main/users/views/reset.py +++ b/main/users/views/reset.py @@ -1,5 +1,80 @@ +from django.utils.translation import gettext as _ +from django.db.models import Q from django.shortcuts import HttpResponseRedirect, render, redirect, HttpResponse +from django.utils.http import urlsafe_base64_decode +from django.utils.encoding import force_str +from django.contrib import messages +from django.urls import reverse + +from main.core.utils import reset_password_token, email_active_token, display_form_validations +from main.core.notify import send_mail_reset_password +from main.users.models import User +from main.users.forms import ResetPasswordConfirmForm, ResetPasswordForm def reset_view(request): + if request.POST: + form = ResetPasswordForm(request.POST) + email = request.POST.get("email") + if form.is_valid(): + user = User.objects.get(Q(username=email) | Q(email=email)) + if send_mail_reset_password(request=request, user=user): + messages.success( + request, + _("Success, reset password email has been sent, please check your email inbox") + ) + return redirect(reverse("users:login_view")) + else: + display_form_validations(form=form, request=request) + else: + messages.error(request, _("Error, an error occurred while sending reset email")) return render(request, "auth/reset.html") + + +def reset_password_view(request, uidb64: str = None, token: str = None): # noqa + user, show_confirm_form = None, False + try: + uid = force_str(urlsafe_base64_decode(uidb64)) + user = User.objects.get(pk=uid) + show_confirm_form = True + except User.DoesNotExist: # noqa + messages.error(request, _("Error, User does not match")) + except (TypeError, ValueError, OverflowError): + messages.error(request, _("Error, Unknown error occurred, please reset password and try again")) + if request.POST: + form = ResetPasswordConfirmForm(request.POST) + if user and reset_password_token.check_token(user, token=token): + password = request.POST.get("password") + if form.is_valid(): + user.set_password(str(password)) + user.save() + messages.success(request, _("Success, Your password has been reset successfully")) + return redirect(reverse("users:login_view")) + else: + display_form_validations(form=form, request=request) + else: + messages.error(request, _("Error, Invalid link, please try again")) + return render(request, "auth/reset.html", {"show_confirm_form": show_confirm_form}) + + +def email_verification_view(request, uidb64: str = None, token: str = None): # noqa + user = None + try: + uid = force_str(urlsafe_base64_decode(uidb64)) + user = User.objects.get(pk=uid) + except User.DoesNotExist: # noqa + messages.error(request, _("Error, User does not match")) + except (TypeError, ValueError, OverflowError): + messages.error(request, _("Error, Unknown error occurred, please reset password and try again")) + if user: + if user.is_email: + messages.warning(request, _("Warning, Your email address already verified")) + elif email_active_token.check_token(user, token=token): + user.is_verified = True + user.is_active = True + user.save() + messages.success(request, _("Your email hase been verified successfully")) + else: + messages.error(request, _("Unknown error occurred!")) + return redirect(reverse("users:login_view")) + From 34730a89fd6a29490061162e526b832cbf26813e Mon Sep 17 00:00:00 2001 From: Tarek Kalaji Date: Sat, 1 Jul 2023 16:28:40 +0300 Subject: [PATCH 09/14] show message bind error --- main/static/assets/css/base.css | 4 +++- main/web/static/web/SAMPLE.js | 4 ++-- main/web/static/web/content/filters.js | 10 ++++---- main/web/static/web/content/groups.js | 10 ++++---- main/web/static/web/content/httpccm.js | 9 ++++---- main/web/static/web/content/morouter.js | 10 ++++---- main/web/static/web/content/mtrouter.js | 10 ++++---- main/web/static/web/content/smppccm.js | 10 ++++---- main/web/static/web/content/submit_logs.js | 2 +- main/web/static/web/content/users.js | 10 ++++---- main/web/static/web/global.js | 24 ++++++++++++++++---- main/web/templates/web/base.html | 15 ++++++++++++ main/web/templates/web/includes/sidebar.html | 2 +- 13 files changed, 75 insertions(+), 45 deletions(-) diff --git a/main/static/assets/css/base.css b/main/static/assets/css/base.css index 8ace970..8743852 100644 --- a/main/static/assets/css/base.css +++ b/main/static/assets/css/base.css @@ -3,4 +3,6 @@ @import url("sb-admin-2.min.css"); @import url("../vendor/fa5/css/all.min.css"); @import url("../vendor/toastr/toastr.min.css"); -@import url("../vendor/sweetalert/sweetalert.css"); \ No newline at end of file +@import url("../vendor/sweetalert/sweetalert.css"); + +.iframe_error {width:100%;height:100%;border: #efefef 1px solid;} \ No newline at end of file diff --git a/main/web/static/web/SAMPLE.js b/main/web/static/web/SAMPLE.js index 9152fc1..addcd22 100644 --- a/main/web/static/web/SAMPLE.js +++ b/main/web/static/web/SAMPLE.js @@ -1,5 +1,5 @@ (function($){ - var localpath = window.location.pathname, csrfmiddlewaretoken = document.getElementsByName('csrfmiddlewaretoken')[0].value; + var local_path = window.location.pathname, csrfmiddlewaretoken = document.getElementsByName('csrfmiddlewaretoken')[0].value; var tbody_html = function(val, i){ return ` ${1} @@ -11,7 +11,7 @@ $("[name*=q], #per_page").on("keyup paste change", function(){collection_check(tbody_html, 1, true);}); var collection_manage = function(cmd, index){ if (cmd == "edit") { - //window.location = localpath + index + '/edit/'; + //window.location = local_path + index + '/edit/'; } else if (cmd == "delete") { } diff --git a/main/web/static/web/content/filters.js b/main/web/static/web/content/filters.js index 858cc14..2f07bfb 100644 --- a/main/web/static/web/content/filters.js +++ b/main/web/static/web/content/filters.js @@ -1,16 +1,16 @@ (function($){ - var localpath = window.location.pathname, csrfmiddlewaretoken = document.getElementsByName('csrfmiddlewaretoken')[0].value; + var local_path = window.location.pathname, csrfmiddlewaretoken = document.getElementsByName('csrfmiddlewaretoken')[0].value; var add_modal_form = "#add_modal_form", edit_modal_form = "#edit_modal_form", service_modal_form = "#service_modal_form"; var variant_boxes = [add_modal_form, edit_modal_form, service_modal_form]; var FILTERS_DICT = {}; var collectionlist_check = function(){ $.ajax({ - url: localpath + 'manage/', + url: local_path + 'manage/', type: "POST", data: { csrfmiddlewaretoken: csrfmiddlewaretoken, s: "list", - //q: $("#search_filter").val(), + }, dataType: "json", success: function(data){ @@ -33,7 +33,7 @@ return html; }); $("#collectionlist").html(datalist.length > 0 ? output : $(".isEmpty").html()); - } + }, error: function(jqXHR, textStatus, errorThrown){quick_display_modal_error(jqXHR.responseText);} }); } collectionlist_check(); @@ -57,7 +57,7 @@ var data = FILTERS_DICT[index]; $.ajax({ type: "POST", - url: localpath + 'manage/', + url: local_path + 'manage/', data: { csrfmiddlewaretoken: csrfmiddlewaretoken, s: cmd, diff --git a/main/web/static/web/content/groups.js b/main/web/static/web/content/groups.js index 5f80126..4bdbf5c 100644 --- a/main/web/static/web/content/groups.js +++ b/main/web/static/web/content/groups.js @@ -1,16 +1,16 @@ (function($){ - var localpath = window.location.pathname, csrfmiddlewaretoken = document.getElementsByName('csrfmiddlewaretoken')[0].value; + var local_path = window.location.pathname, csrfmiddlewaretoken = document.getElementsByName('csrfmiddlewaretoken')[0].value; var add_modal_form = "#add_modal_form", edit_modal_form = "#edit_modal_form", service_modal_form = "#service_modal_form"; var variant_boxes = [add_modal_form, edit_modal_form, service_modal_form]; var GROUPS_DICT = {}; var collectionlist_check = function(){ $.ajax({ - url: localpath + 'manage/', + url: local_path + 'manage/', type: "POST", data: { csrfmiddlewaretoken: csrfmiddlewaretoken, s: "list", - //q: $("#search_filter").val(), + }, dataType: "json", success: function(data){ @@ -33,7 +33,7 @@ return html; }); $("#collectionlist").html(datalist.length > 0 ? output : $(".isEmpty").html()); - } + }, error: function(jqXHR, textStatus, errorThrown){quick_display_modal_error(jqXHR.responseText);} }); } collectionlist_check(); @@ -67,7 +67,7 @@ var data = GROUPS_DICT[index]; $.ajax({ type: "POST", - url: localpath + 'manage/', + url: local_path + 'manage/', data: { csrfmiddlewaretoken: csrfmiddlewaretoken, s: cmd, diff --git a/main/web/static/web/content/httpccm.js b/main/web/static/web/content/httpccm.js index c252dc9..b625b35 100644 --- a/main/web/static/web/content/httpccm.js +++ b/main/web/static/web/content/httpccm.js @@ -1,16 +1,15 @@ (function($){ - var localpath = window.location.pathname, csrfmiddlewaretoken = document.getElementsByName('csrfmiddlewaretoken')[0].value; + var local_path = window.location.pathname, csrfmiddlewaretoken = document.getElementsByName('csrfmiddlewaretoken')[0].value; var add_modal_form = "#add_modal_form", edit_modal_form = "#edit_modal_form", service_modal_form = "#service_modal_form"; var variant_boxes = [add_modal_form, edit_modal_form, service_modal_form]; var HTTPCCM_DICT = {}; var collectionlist_check = function(){ $.ajax({ - url: localpath + 'manage/', + url: local_path + 'manage/', type: "POST", data: { csrfmiddlewaretoken: csrfmiddlewaretoken, s: "list", - //q: $("#search_filter").val(), }, dataType: "json", success: function(data){ @@ -32,7 +31,7 @@ return html; }); $("#collectionlist").html(datalist.length > 0 ? output : $(".isEmpty").html()); - } + }, error: function(jqXHR, textStatus, errorThrown){quick_display_modal_error(jqXHR.responseText);} }); } collectionlist_check(); @@ -56,7 +55,7 @@ var data = HTTPCCM_DICT[index]; $.ajax({ type: "POST", - url: localpath + 'manage/', + url: local_path + 'manage/', data: { csrfmiddlewaretoken: csrfmiddlewaretoken, s: cmd, diff --git a/main/web/static/web/content/morouter.js b/main/web/static/web/content/morouter.js index 5916023..abb2360 100644 --- a/main/web/static/web/content/morouter.js +++ b/main/web/static/web/content/morouter.js @@ -1,16 +1,16 @@ (function($){ - var localpath = window.location.pathname, csrfmiddlewaretoken = document.getElementsByName('csrfmiddlewaretoken')[0].value; + var local_path = window.location.pathname, csrfmiddlewaretoken = document.getElementsByName('csrfmiddlewaretoken')[0].value; var add_modal_form = "#add_modal_form", edit_modal_form = "#edit_modal_form", service_modal_form = "#service_modal_form"; var variant_boxes = [add_modal_form, edit_modal_form, service_modal_form]; var MOROUTER_DICT = {}, SMPPCCM_DICT = {}, HTTPCCM_DICT = {}, FILTERS_DICT = {}; var collectionlist_check = function(){ $.ajax({ - url: localpath + 'manage/', + url: local_path + 'manage/', type: "POST", data: { csrfmiddlewaretoken: csrfmiddlewaretoken, s: "list", - //q: $("#search_filter").val(), + }, dataType: "json", success: function(data){ @@ -33,7 +33,7 @@ return html; }); $("#collectionlist").html(datalist.length > 0 ? output : $(".isEmpty").html()); - } + }, error: function(jqXHR, textStatus, errorThrown){quick_display_modal_error(jqXHR.responseText);} }); } collectionlist_check(); @@ -57,7 +57,7 @@ var data = MOROUTER_DICT[index]; $.ajax({ type: "POST", - url: localpath + 'manage/', + url: local_path + 'manage/', data: { csrfmiddlewaretoken: csrfmiddlewaretoken, s: cmd, diff --git a/main/web/static/web/content/mtrouter.js b/main/web/static/web/content/mtrouter.js index 2401e4f..3cf6753 100644 --- a/main/web/static/web/content/mtrouter.js +++ b/main/web/static/web/content/mtrouter.js @@ -1,16 +1,16 @@ (function($){ - var localpath = window.location.pathname, csrfmiddlewaretoken = document.getElementsByName('csrfmiddlewaretoken')[0].value; + var local_path = window.location.pathname, csrfmiddlewaretoken = document.getElementsByName('csrfmiddlewaretoken')[0].value; var add_modal_form = "#add_modal_form", edit_modal_form = "#edit_modal_form", service_modal_form = "#service_modal_form"; var variant_boxes = [add_modal_form, edit_modal_form, service_modal_form]; var MTROUTER_DICT = {}, SMPPCCM_DICT = {}, HTTPCCM_DICT = {}, FILTERS_DICT = {}; var collectionlist_check = function(){ $.ajax({ - url: localpath + 'manage/', + url: local_path + 'manage/', type: "POST", data: { csrfmiddlewaretoken: csrfmiddlewaretoken, s: "list", - //q: $("#search_filter").val(), + }, dataType: "json", success: function(data){ @@ -34,7 +34,7 @@ return html; }); $("#collectionlist").html(datalist.length > 0 ? output : $(".isEmpty").html()); - } + }, error: function(jqXHR, textStatus, errorThrown){quick_display_modal_error(jqXHR.responseText);} }); } collectionlist_check(); @@ -58,7 +58,7 @@ var data = MTROUTER_DICT[index]; $.ajax({ type: "POST", - url: localpath + 'manage/', + url: local_path + 'manage/', data: { csrfmiddlewaretoken: csrfmiddlewaretoken, s: cmd, diff --git a/main/web/static/web/content/smppccm.js b/main/web/static/web/content/smppccm.js index e0c7d5c..491e4c2 100644 --- a/main/web/static/web/content/smppccm.js +++ b/main/web/static/web/content/smppccm.js @@ -1,16 +1,16 @@ (function($){ - var localpath = window.location.pathname, csrfmiddlewaretoken = document.getElementsByName('csrfmiddlewaretoken')[0].value; + var local_path = window.location.pathname, csrfmiddlewaretoken = document.getElementsByName('csrfmiddlewaretoken')[0].value; var add_modal_form = "#add_modal_form", edit_modal_form = "#edit_modal_form", service_modal_form = "#service_modal_form"; var variant_boxes = [add_modal_form, edit_modal_form, service_modal_form]; var SMPPCCM_DICT = {}; var collectionlist_check = function() { $.ajax({ - url: localpath + 'manage/', + url: local_path + 'manage/', type: "POST", data: { csrfmiddlewaretoken: csrfmiddlewaretoken, s: "list", - //q: $("#search_filter").val(), + }, dataType: "json", success: function(data){ @@ -37,7 +37,7 @@ return html; }); $("#collectionlist").html(datalist.length > 0 ? output : $(".isEmpty").html()); - } + }, error: function(jqXHR, textStatus, errorThrown){quick_display_modal_error(jqXHR.responseText);} }) } collectionlist_check(); @@ -108,7 +108,7 @@ var data = SMPPCCM_DICT[index]; $.ajax({ type: "POST", - url: localpath + 'manage/', + url: local_path + 'manage/', data: { csrfmiddlewaretoken: csrfmiddlewaretoken, s: cmd, diff --git a/main/web/static/web/content/submit_logs.js b/main/web/static/web/content/submit_logs.js index 1990902..183219f 100644 --- a/main/web/static/web/content/submit_logs.js +++ b/main/web/static/web/content/submit_logs.js @@ -1,4 +1,4 @@ (function($){ - var localpath = window.location.pathname, csrfmiddlewaretoken = document.getElementsByName('csrfmiddlewaretoken')[0].value; + var local_path = window.location.pathname, csrfmiddlewaretoken = document.getElementsByName('csrfmiddlewaretoken')[0].value; $("li.nav-item.submit_logs-menu").addClass("active"); })(jQuery); \ No newline at end of file diff --git a/main/web/static/web/content/users.js b/main/web/static/web/content/users.js index f5cfe2b..51fc6be 100644 --- a/main/web/static/web/content/users.js +++ b/main/web/static/web/content/users.js @@ -1,16 +1,16 @@ (function($){ - var localpath = window.location.pathname, csrfmiddlewaretoken = document.getElementsByName('csrfmiddlewaretoken')[0].value; + var local_path = window.location.pathname, csrfmiddlewaretoken = document.getElementsByName('csrfmiddlewaretoken')[0].value; var add_modal_form = "#add_modal_form", edit_modal_form = "#edit_modal_form", service_modal_form = "#service_modal_form"; var variant_boxes = [add_modal_form, edit_modal_form, service_modal_form]; var USERS_DICT = {}, GROUPS_DICT = {}; var collectionlist_check = function(){ $.ajax({ - url: localpath + 'manage/', + url: local_path + 'manage/', type: "POST", data: { csrfmiddlewaretoken: csrfmiddlewaretoken, s: "list", - //q: $("#search_filter").val(), + }, dataType: "json", success: function(data){ @@ -37,7 +37,7 @@ return html; }); $("#collectionlist").html(datalist.length > 0 ? output : $(".isEmpty").html()); - } + }, error: function(jqXHR, textStatus, errorThrown){quick_display_modal_error(jqXHR.responseText);} }); } collectionlist_check(); @@ -91,7 +91,7 @@ var data = USERS_DICT[index]; $.ajax({ type: "POST", - url: localpath + 'manage/', + url: local_path + 'manage/', data: { csrfmiddlewaretoken: csrfmiddlewaretoken, s: cmd, diff --git a/main/web/static/web/global.js b/main/web/static/web/global.js index fb771f6..5482275 100644 --- a/main/web/static/web/global.js +++ b/main/web/static/web/global.js @@ -42,7 +42,7 @@ $(box).show(); } try { - var localpath = window.location.pathname, csrfmiddlewaretoken = document.getElementsByName('csrfmiddlewaretoken')[0].value; + var local_path = window.location.pathname, csrfmiddlewaretoken = document.getElementsByName('csrfmiddlewaretoken')[0].value; } catch {} window.collection_check = function(tbody_html, page_no, destroy_paginate){ tbody_html = tbody_html || function(val, i){return `${val}`;}; @@ -50,12 +50,12 @@ destroy_paginate = destroy_paginate || false; var per_page = parseInt($("#per_page").val()); $.ajax({ - url: localpath + 'manage/', + url: local_path + 'manage/', type: "POST", data: { csrfmiddlewaretoken: csrfmiddlewaretoken, s: "list", - //q: $("#search_filter").val(), + page: page_no, per_page: per_page, }, @@ -120,12 +120,12 @@ window.collection_check_nopaginate = function(tbody_html) { tbody_html = tbody_html || function(val, i){return `${val}`;}; $.ajax({ - url: localpath + 'manage/', + url: local_path + 'manage/', type: "POST", data: { csrfmiddlewaretoken: csrfmiddlewaretoken, s: "list", - //q: $("#search_filter").val(), + page: page_no, per_page: per_page, }, @@ -141,6 +141,20 @@ } }) } + window.quick_display_modal_error = function(html_response) { + var html_message = ` +

An error occurred,
+ +

+ `; + toastr.error(html_message, {closeButton: true, progressBar: true, enableHtml: true,}); + var html_source = html_response; + html_source = html_source.replace(/["]/g, '"') + $("#quick_display_modal").closest('div').find('.modal-body').html(''); + if($("#collectionlist").length){ + $("#collectionlist").html($(".isEmpty").html()); + } + } $("form").validate({ errorClass: "text-danger", errorElementClass: 'text-success', diff --git a/main/web/templates/web/base.html b/main/web/templates/web/base.html index 69bd4bd..1a58f3d 100644 --- a/main/web/templates/web/base.html +++ b/main/web/templates/web/base.html @@ -23,6 +23,21 @@ {% include 'web/includes/footer.html' %} +

diff --git a/main/web/templates/web/includes/sidebar.html b/main/web/templates/web/includes/sidebar.html index b6eafce..42c7c5f 100644 --- a/main/web/templates/web/includes/sidebar.html +++ b/main/web/templates/web/includes/sidebar.html @@ -6,7 +6,7 @@ - + From b16f2038f87db9daf5e079ab018aa59197a1f9f4 Mon Sep 17 00:00:00 2001 From: Tarek Kalaji Date: Sat, 1 Jul 2023 18:04:56 +0300 Subject: [PATCH 10/14] reset email and reset password ok, bug fixes --- main/core/mailer/mail_modules.py | 8 ++++++-- main/core/notify/mail_sender.py | 6 +++--- main/users/forms/reset.py | 16 ++++++++++------ main/users/views/profile.py | 14 ++++++++------ main/users/views/reset.py | 18 +++++++++--------- main/web/templates/auth/profile.html | 2 +- main/web/templates/auth/reset.html | 17 ++++++++++++++--- 7 files changed, 51 insertions(+), 30 deletions(-) diff --git a/main/core/mailer/mail_modules.py b/main/core/mailer/mail_modules.py index d3ecb69..c47d9da 100644 --- a/main/core/mailer/mail_modules.py +++ b/main/core/mailer/mail_modules.py @@ -71,10 +71,14 @@ def send(self, mails: list, kwargs=None): html_part = MIMEText(render_to_string(self.html_template, kwargs), 'html') msg.attach(html_part) if self.email_server.ssl: - mail_obj = smtplib.SMTP_SSL(self.email_server.server, self.email_server.port) + mail_obj = smtplib.SMTP_SSL( + host=self.email_server.server, port=self.email_server.port, timeout=120.0, + ) mail_obj.ehlo() else: - mail_obj = smtplib.SMTP(self.email_server.server, self.email_server.port) + mail_obj = smtplib.SMTP( + host=self.email_server.server, port=self.email_server.port, timeout=120.0, + ) mail_obj.starttls() mail_obj.login(self.email_server.username, self.email_server.password) for to_mail in mails: diff --git a/main/core/notify/mail_sender.py b/main/core/notify/mail_sender.py index be47724..6d2100b 100644 --- a/main/core/notify/mail_sender.py +++ b/main/core/notify/mail_sender.py @@ -13,8 +13,8 @@ def send_mail_reset_email(request) -> bool: - if request.is_authenticated: - uidb64 = urlsafe_base64_encode(force_bytes(request.user.pk)).decode() # noqa + if request.user.is_authenticated: + uidb64 = urlsafe_base64_encode(force_bytes(request.user.pk)) # noqa token = email_active_token.make_token(request.user) subject = str(_("Verify your email address")) template_name = "core/mail_reset_email.html" @@ -36,7 +36,7 @@ def send_mail_reset_email(request) -> bool: def send_mail_reset_password(request, user) -> bool: current_site = get_current_site(request=request) - uidb64 = urlsafe_base64_encode(force_bytes(user.pk)).decode() # noqa + uidb64 = urlsafe_base64_encode(force_bytes(user.pk)) # noqa token = reset_password_token.make_token(user=user) reset_password_url = reverse("users:reset_password_view", kwargs={"uidb64": uidb64, "token": token}) # noqa reset_password_confirm = f"{current_site}{reset_password_url}" diff --git a/main/users/forms/reset.py b/main/users/forms/reset.py index e96559d..b14fc0e 100644 --- a/main/users/forms/reset.py +++ b/main/users/forms/reset.py @@ -1,34 +1,38 @@ -# -*- encoding: utf-8 -*- from django.utils.translation import gettext_lazy as _ -from django.core.validators import MaxLengthValidator, MinLengthValidator, EmailValidator +from django.core.validators import MaxLengthValidator, MinLengthValidator from django import forms + class ResetPasswordForm(forms.Form): default_attrs = {'class': 'form-control'} username_validators = [ - MaxLengthValidator(limit_value=255, message=_("Maximum length allowed is %(max_length)s") % dict(max_length=255)), + MaxLengthValidator(limit_value=255, + message=_("Maximum length allowed is %(max_length)s") % dict(max_length=255)), MinLengthValidator(limit_value=2, message=_("Minimum length allowed is %(min_length)s") % dict(min_length=2)), ] username = forms.CharField(widget=forms.TextInput( attrs=default_attrs.update(dict(placeholder=_("Email or Username"))) ), validators=username_validators, required=True, label=_("Email or Username")) + class ResetPasswordConfirmForm(forms.Form): default_attrs = {'class': 'form-control'} password_validators = [ - MaxLengthValidator(limit_value=255, message=_("Maximum length allowed is %(max_length)s") % dict(max_length=255)), + MaxLengthValidator(limit_value=255, + message=_("Maximum length allowed is %(max_length)s") % dict(max_length=255)), MinLengthValidator(limit_value=5, message=_("Minimum length allowed is %(min_length)s") % dict(min_length=5)), ] - password = forms.CharField(widget=forms.PasswordInput( + password = forms.CharField(widget=forms.PasswordInput( attrs=default_attrs.update(dict(placeholder=_("New Password"))) ), validators=password_validators, required=True, label=_("Password")) password2 = forms.CharField(widget=forms.PasswordInput( attrs=default_attrs.update(dict(placeholder=_("New Password Confirmation"))) ), validators=password_validators, required=True, label=_("Password Confirmation")) + def clean(self): cleaned_data = super(ResetPasswordConfirmForm, self).clean() password = cleaned_data.get("password") password2 = cleaned_data.get("password2") if not password or not password2 or not password == password2: self.add_error(field="password2", error=_("The two password fields did not matched.")) - return password \ No newline at end of file + return password diff --git a/main/users/views/profile.py b/main/users/views/profile.py index f75f8b0..8884170 100644 --- a/main/users/views/profile.py +++ b/main/users/views/profile.py @@ -1,5 +1,3 @@ -# -*- encoding: utf-8 -*- -from __future__ import unicode_literals from django.utils.translation import gettext as _ from django.utils import timezone from django.contrib.auth.decorators import login_required @@ -7,7 +5,7 @@ from django.contrib import messages from django.http import JsonResponse from django.shortcuts import HttpResponseRedirect, render, redirect, HttpResponse -from django.db.models import Q + from django.urls import reverse from django.conf import settings @@ -15,10 +13,12 @@ from main.users.forms import ChangePhotoForm, ChangePasswordForm, ProfileForm from main.core.utils import display_form_validations, is_json, get_query, paginate from main.core.models import ActivityLog, EmailServer +from main.core.notify import send_mail_reset_email from PIL import Image import json, os + @login_required def profile_view(request): if request.POST: @@ -31,13 +31,13 @@ def profile_view(request): email = email.strip() user = User.objects.get(pk=request.user.pk) user.first_name = request.POST.get("first_name") - user.last_name = request.POST.get("last_name") + user.last_name = request.POST.get("last_name") if email and user.email != email: user.email = email if EmailServer.objects.filter(active=True).exists(): user.is_email = False - #TODO SEND EMAIL TO CLIENT - messages.info(request, _("Please, check your email inbox to verify your email address")) + send_mail_reset_email(request=request) + messages.info(request, _("Please check your email inbox to verify your email address")) user.save() messages.success(request, _("Congrats!, Your profile has been updated successfully")) else: @@ -113,10 +113,12 @@ def profile_view(request): return redirect(reverse("users:profile_view")) return render(request, "auth/profile.html") + @login_required def settings_view(request): return render(request, "auth/settings.html") + @login_required def activity_log_view(request): activitylogs = ActivityLog.objects.filter(user=request.user) diff --git a/main/users/views/reset.py b/main/users/views/reset.py index 030c659..e784389 100644 --- a/main/users/views/reset.py +++ b/main/users/views/reset.py @@ -15,19 +15,19 @@ def reset_view(request): if request.POST: form = ResetPasswordForm(request.POST) - email = request.POST.get("email") + username = request.POST.get("username") if form.is_valid(): - user = User.objects.get(Q(username=email) | Q(email=email)) - if send_mail_reset_password(request=request, user=user): + user = User.objects.filter(Q(username=username) | Q(email=username)).first() + if user and send_mail_reset_password(request=request, user=user): messages.success( request, _("Success, reset password email has been sent, please check your email inbox") ) - return redirect(reverse("users:login_view")) + return redirect(reverse("users:signin_view")) + else: + messages.warning(request, _("Warning, invalid username or email address")) else: display_form_validations(form=form, request=request) - else: - messages.error(request, _("Error, an error occurred while sending reset email")) return render(request, "auth/reset.html") @@ -49,7 +49,7 @@ def reset_password_view(request, uidb64: str = None, token: str = None): # noqa user.set_password(str(password)) user.save() messages.success(request, _("Success, Your password has been reset successfully")) - return redirect(reverse("users:login_view")) + return redirect(reverse("users:signin_view")) else: display_form_validations(form=form, request=request) else: @@ -70,11 +70,11 @@ def email_verification_view(request, uidb64: str = None, token: str = None): # if user.is_email: messages.warning(request, _("Warning, Your email address already verified")) elif email_active_token.check_token(user, token=token): + user.is_email = True user.is_verified = True - user.is_active = True user.save() messages.success(request, _("Your email hase been verified successfully")) else: messages.error(request, _("Unknown error occurred!")) - return redirect(reverse("users:login_view")) + return redirect(reverse("users:signin_view")) diff --git a/main/web/templates/auth/profile.html b/main/web/templates/auth/profile.html index cdc0097..547b1b4 100644 --- a/main/web/templates/auth/profile.html +++ b/main/web/templates/auth/profile.html @@ -61,7 +61,7 @@

{% trans "Personal Information" %}

- {% trans "You need to activate your email address when changed" %} + {% trans "You need to activate your email address when changed" %}
diff --git a/main/web/templates/auth/reset.html b/main/web/templates/auth/reset.html index 6e76ab4..07b54a7 100644 --- a/main/web/templates/auth/reset.html +++ b/main/web/templates/auth/reset.html @@ -7,13 +7,24 @@

{% trans 'Reset your password' %}

{% include "web/includes/message.html" %} -
- {% csrf_token %} + {% if show_confirm_form %} + {% csrf_token %}
- + +
+
+
+ {% else %} +
{% csrf_token %} +
+ +
+ +
+ {% endif %}
{% trans "Have an account?" %} {% trans 'Sign in' %}
{% endblock content %} From 82f9f8e253735584b3e5be5ec59ae35af5b6e352 Mon Sep 17 00:00:00 2001 From: Tarek Kalaji Date: Sun, 2 Jul 2023 02:24:31 +0300 Subject: [PATCH 11/14] adding check mark to verified email --- main/web/templates/auth/profile.html | 3 +++ main/web/templates/web/includes/message.html | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/main/web/templates/auth/profile.html b/main/web/templates/auth/profile.html index 547b1b4..0780a5e 100644 --- a/main/web/templates/auth/profile.html +++ b/main/web/templates/auth/profile.html @@ -63,6 +63,9 @@

{% trans "Personal Information" %}

{% trans "You need to activate your email address when changed" %} +
+ {% if user.is_email %}{% else %}{% endif %} +
diff --git a/main/web/templates/web/includes/message.html b/main/web/templates/web/includes/message.html index 9299cac..6961ee4 100644 --- a/main/web/templates/web/includes/message.html +++ b/main/web/templates/web/includes/message.html @@ -1,8 +1,8 @@ {% load i18n %} {% for message in messages %}
-
-

{{ message|safe }}

+
{% endfor %} \ No newline at end of file From c1a28f401c4e9f697154bcb21e8f626a56d85509 Mon Sep 17 00:00:00 2001 From: Tarek Kalaji Date: Sun, 2 Jul 2023 12:19:04 +0300 Subject: [PATCH 12/14] small fixes --- main/users/forms/profile.py | 36 +++++++++++++++++++++--------------- main/users/views/profile.py | 28 ++++++++++++++-------------- 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/main/users/forms/profile.py b/main/users/forms/profile.py index 2824117..00253ab 100644 --- a/main/users/forms/profile.py +++ b/main/users/forms/profile.py @@ -1,8 +1,5 @@ -# -*- encoding: utf-8 -*- -from __future__ import unicode_literals from django.utils.translation import gettext_lazy as _ -from django.core.validators import MaxLengthValidator, MinLengthValidator, EmailValidator -from django.conf import settings +from django.core.validators import MaxLengthValidator, MinLengthValidator from django import forms from main.users.models import User @@ -13,25 +10,29 @@ class Meta: model = User fields = ('img',) + class ChangePasswordForm(forms.Form): default_attrs = {'class': 'form-control'} password0_validators = [ - MaxLengthValidator(limit_value=255, message=_("Maximum length allowed is %(max_length)s") % dict(max_length=255)), + MaxLengthValidator(limit_value=255, + message=_("Maximum length allowed is %(max_length)s") % dict(max_length=255)), MinLengthValidator(limit_value=1, message=_("Minimum length allowed is %(min_length)s") % dict(min_length=5)), ] password_validators = [ - MaxLengthValidator(limit_value=255, message=_("Maximum length allowed is %(max_length)s") % dict(max_length=255)), + MaxLengthValidator(limit_value=255, + message=_("Maximum length allowed is %(max_length)s") % dict(max_length=255)), MinLengthValidator(limit_value=5, message=_("Minimum length allowed is %(min_length)s") % dict(min_length=5)), ] password = forms.CharField(widget=forms.PasswordInput( attrs=default_attrs.update(dict(placeholder=_("Current Password"))) ), validators=password0_validators, required=True, label=_("Password")) - password1 = forms.CharField(widget=forms.PasswordInput( + password1 = forms.CharField(widget=forms.PasswordInput( attrs=default_attrs.update(dict(placeholder=_("New Password"))) ), validators=password_validators, required=True, label=_("Password")) password2 = forms.CharField(widget=forms.PasswordInput( attrs=default_attrs.update(dict(placeholder=_("New Password Confirmation"))) ), validators=password_validators, required=True, label=_("Password Confirmation")) + def clean(self): cleaned_data = super(ChangePasswordForm, self).clean() password1 = cleaned_data.get("password1") @@ -42,22 +43,27 @@ def clean(self): class ProfileForm(forms.Form): first_name_validators = [ - MaxLengthValidator(limit_value=255, message=_("Maximum length allowed is %(max_length)s") % dict(max_length=255)), + MaxLengthValidator(limit_value=255, + message=_("Maximum length allowed is %(max_length)s") % dict(max_length=255)), MinLengthValidator(limit_value=2, message=_("Minimum length allowed is %(min_length)s") % dict(min_length=2)), ] last_name_validators = [ - MaxLengthValidator(limit_value=255, message=_("Maximum length allowed is %(max_length)s") % dict(max_length=255)), + MaxLengthValidator(limit_value=255, + message=_("Maximum length allowed is %(max_length)s") % dict(max_length=255)), MinLengthValidator(limit_value=2, message=_("Minimum length allowed is %(min_length)s") % dict(min_length=2)), ] email_validators = [ - MaxLengthValidator(limit_value=255, message=_("Maximum length allowed is %(max_length)s") % dict(max_length=255)), + MaxLengthValidator(limit_value=255, + message=_("Maximum length allowed is %(max_length)s") % dict(max_length=255)), MinLengthValidator(limit_value=10, message=_("Minimum length allowed is %(min_length)s") % dict(min_length=10)), ] url_validators = [ - MaxLengthValidator(limit_value=255, message=_("Maximum length allowed is %(max_length)s") % dict(max_length=255)), + MaxLengthValidator(limit_value=255, + message=_("Maximum length allowed is %(max_length)s") % dict(max_length=255)), MinLengthValidator(limit_value=10, message=_("Minimum length allowed is %(min_length)s") % dict(min_length=10)), ] - first_name = forms.CharField(widget=forms.TextInput(), validators=first_name_validators, required=True, label=_("First Name")) - last_name = forms.CharField(widget=forms.TextInput(), validators=last_name_validators, required=True, label=_("Last Name")) - email = forms.EmailField(widget=forms.EmailInput(), validators=email_validators, required=True, label=_("Email")) - \ No newline at end of file + first_name = forms.CharField(widget=forms.TextInput(), validators=first_name_validators, required=True, + label=_("First Name")) + last_name = forms.CharField(widget=forms.TextInput(), validators=last_name_validators, required=True, + label=_("Last Name")) + email = forms.EmailField(widget=forms.EmailInput(), validators=email_validators, required=True, label=_("Email")) diff --git a/main/users/views/profile.py b/main/users/views/profile.py index 8884170..301b52e 100644 --- a/main/users/views/profile.py +++ b/main/users/views/profile.py @@ -1,22 +1,22 @@ -from django.utils.translation import gettext as _ -from django.utils import timezone -from django.contrib.auth.decorators import login_required -from django.contrib.auth import update_session_auth_hash +import json +import os + +from PIL import Image +from django.conf import settings from django.contrib import messages +from django.contrib.auth import update_session_auth_hash +from django.contrib.auth.decorators import login_required from django.http import JsonResponse -from django.shortcuts import HttpResponseRedirect, render, redirect, HttpResponse - +from django.shortcuts import render, redirect from django.urls import reverse -from django.conf import settings +from django.utils import timezone +from django.utils.translation import gettext as _ -from ..models import User -from main.users.forms import ChangePhotoForm, ChangePasswordForm, ProfileForm -from main.core.utils import display_form_validations, is_json, get_query, paginate from main.core.models import ActivityLog, EmailServer from main.core.notify import send_mail_reset_email - -from PIL import Image -import json, os +from main.core.utils import display_form_validations, is_json, get_query, paginate +from main.users.forms import ChangePhotoForm, ChangePasswordForm, ProfileForm +from main.users.models import User @login_required @@ -97,7 +97,7 @@ def profile_view(request): elif s == "password": password = request.POST.get("password") password1 = request.POST.get("password1") - password2 = request.POST.get("password2") + # password2 = request.POST.get("password2") form = ChangePasswordForm(request.POST) user = User.objects.get(pk=request.user.pk) if user.check_password(password): From 716a91690ac8f5e075c0b53c8a55f242462cb911 Mon Sep 17 00:00:00 2001 From: Tarek Kalaji Date: Sat, 8 Jul 2023 11:42:05 +0300 Subject: [PATCH 13/14] fixing docker-entrypoint.sh path missin --- config/docker/slim/docker-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/docker/slim/docker-entrypoint.sh b/config/docker/slim/docker-entrypoint.sh index d0ab299..74a2143 100755 --- a/config/docker/slim/docker-entrypoint.sh +++ b/config/docker/slim/docker-entrypoint.sh @@ -18,7 +18,7 @@ python manage.py migrate python manage.py load_new python manage.py collectstatic --noinput --clear --no-post-process -"$APP_DIR"/gunicorn config."$APP_WSGI":application \ +"$APP_DIR"/env/bin/gunicorn config."$APP_WSGI":application \ --workers "$APP_WORKERS" \ --bing :"$APP_PORT" \ --log-level "$APP_LOG_LEVEL" \ From 233bc7493248829c45003836d99d9d88ebd75f48 Mon Sep 17 00:00:00 2001 From: Tarek Kalaji Date: Thu, 13 Jul 2023 01:09:24 +0300 Subject: [PATCH 14/14] bug fixes, Dockerfile, sms_logger dockerized, many fixes around, update README.md --- .dockerignore | 4 + .gitignore | 1 + README.md | 2 +- Sample.env | 4 +- config/docker/jasmin/README.md | 6 + config/docker/jasmin/docker-compose.yml | 82 + config/docker/jasmin/jasmin/config/dlr.cfg | 75 + .../jasmin/jasmin/config/dlrlookupd.cfg | 142 + .../jasmin/jasmin/config/interceptor.cfg | 57 + config/docker/jasmin/jasmin/config/jasmin.cfg | 662 ++++ config/docker/jasmin/jasmin/logs/.gitignore | 1 + .../resource/amqp0-8.stripped.rabbitmq.xml | 771 +++++ .../jasmin/jasmin/resource/amqp0-9-1.xml | 2843 +++++++++++++++++ config/docker/jasmin/jasmin/store/.gitignore | 0 config/docker/jasmin/redis/.gitignore | 1 + config/docker/slim/Dockerfile | 15 +- config/docker/slim/docker-entrypoint.sh | 4 +- config/docker/sms_logger/Dockerfile | 50 + config/docker/sms_logger/docker-entrypoint.sh | 11 + config/docker/sms_logger/requirements.txt | 9 + config/docker/sms_logger/sms_logger.py | 434 +++ config/settings/com.py | 2 +- docker-compose.yml | 22 +- main/core/utils/common.py | 2 +- main/web/static/web/global.js | 1 + 25 files changed, 5183 insertions(+), 18 deletions(-) create mode 100644 config/docker/jasmin/README.md create mode 100644 config/docker/jasmin/docker-compose.yml create mode 100644 config/docker/jasmin/jasmin/config/dlr.cfg create mode 100644 config/docker/jasmin/jasmin/config/dlrlookupd.cfg create mode 100644 config/docker/jasmin/jasmin/config/interceptor.cfg create mode 100644 config/docker/jasmin/jasmin/config/jasmin.cfg create mode 100644 config/docker/jasmin/jasmin/logs/.gitignore create mode 100644 config/docker/jasmin/jasmin/resource/amqp0-8.stripped.rabbitmq.xml create mode 100644 config/docker/jasmin/jasmin/resource/amqp0-9-1.xml create mode 100644 config/docker/jasmin/jasmin/store/.gitignore create mode 100644 config/docker/jasmin/redis/.gitignore create mode 100644 config/docker/sms_logger/Dockerfile create mode 100755 config/docker/sms_logger/docker-entrypoint.sh create mode 100644 config/docker/sms_logger/requirements.txt create mode 100644 config/docker/sms_logger/sms_logger.py diff --git a/.dockerignore b/.dockerignore index fee4b36..91e2311 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,5 +10,9 @@ Dockerfile .cache *.md !README*.md +env/ +venv/ +logs/ +public/ docker-compose.yml diff --git a/.gitignore b/.gitignore index ac0fdfb..501c9c9 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,7 @@ staticfiles # virtual environments .env env/ +venv/ db.sqlite3 # Hitch directory diff --git a/README.md b/README.md index f0ed61b..f003b03 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,7 @@ docker pull tarekaec/jasmin_web_panel ``` also you could build it on you local machine by navigating to project directory ```shell -docker build -t jasmin_web_panel:1.0 . +docker build -f config/docker/slim/Dockerfile -t jasmin_web_panel:1.0 . ``` You need to configure the environment variable in `.env` file ```shell diff --git a/Sample.env b/Sample.env index 65d5ada..0f08c6f 100644 --- a/Sample.env +++ b/Sample.env @@ -10,10 +10,10 @@ SITE_ID=1 # mysql://root:123456@127.0.0.1/jasmin_web_db # sqlite:///db.sqlite3 -# postgres://postgres:123@127.0.0.1:5432/jasmin_web_db +# postgres://jasmin:jasmin@127.0.0.1:5432/jasmin DEVDB_URL=sqlite:///db.sqlite3 -PRODB_URL=postgres://postgres:123@127.0.0.1:5432/jasmin_web_db +PRODB_URL=postgres://jasmin:jasmin@127.0.0.1:5432/jasmin # REDIS_HOST=localhost # REDIS_PORT=6379 diff --git a/config/docker/jasmin/README.md b/config/docker/jasmin/README.md new file mode 100644 index 0000000..1c17067 --- /dev/null +++ b/config/docker/jasmin/README.md @@ -0,0 +1,6 @@ +# Jasmin SMS Gateway +This is the docker version of jasmin sms gateway + +```shell +docker stack deploy -c config/docker/jasmin/docker-compose.yml jasmin +``` \ No newline at end of file diff --git a/config/docker/jasmin/docker-compose.yml b/config/docker/jasmin/docker-compose.yml new file mode 100644 index 0000000..ff78140 --- /dev/null +++ b/config/docker/jasmin/docker-compose.yml @@ -0,0 +1,82 @@ +version: "3" + +services: + redis: + image: redis:alpine + restart: always + volumes: + - /data/jasmin/redis:/data + healthcheck: + test: redis-cli ping | grep PONG + deploy: + resources: + limits: + cpus: '0.2' + memory: 128M + + rabbit-mq: + image: rabbitmq:3.10-management-alpine + restart: always + volumes: + - /data/jasmin/rabbitmq:/var/lib/rabbitmq + healthcheck: + test: rabbitmq-diagnostics -q ping + deploy: + resources: + limits: + cpus: '0.5' + memory: 525M + + jasmin: + image: tarekaec/jasmin:0.10.13 +# command: > +# bash -c " +# sed -i "s/.*publish_submit_sm_resp\s*=.*/publish_submit_sm_resp=True/g" /etc/jasmin/jasmin.cfg +# /docker-entrypoint.sh +# " + ports: + - '${FORWARD_JASMIN_SMPP_PORT:-2776}:2775' + - '${FORWARD_JASMIN_CLI_PORT:-8991}:8990' + - '${FORWARD_JASMIN_HTTP_PORT:-1402}:1401' + volumes: + # - /data/jasmin/jasmin:/usr/jasmin/jasmin + - /data/jasmin/jasmin_config:/etc/jasmin + - /data/jasmin/jasmin_logs:/var/log/jasmin + - /data/jasmin/jasmin_resource:/etc/jasmin/resource + - /data/jasmin/jasmin_store:/etc/jasmin/store + depends_on: + - redis + - rabbit-mq + environment: + REDIS_CLIENT_HOST: ${REDIS_CLIENT_HOST:-redis} + REDIS_CLIENT_PORT: ${REDIS_CLIENT_PORT:-6379} + AMQP_BROKER_HOST: ${AMQP_BROKER_HOST:-rabbit-mq} + AMQP_BROKER_PORT: ${AMQP_BROKER_PORT:-5672} + ENABLE_PUBLISH_SUBMIT_SM_RESP: ${ENABLE_PUBLISH_SUBMIT_SM_RESP:-1} + RESTAPI_MODE: ${RESTAPI_MODE:-0} + deploy: + restart_policy: + condition: on-failure + resources: + limits: + cpus: '1' + memory: 256M + sms_logger: + image: tarekaec/jasmin_log:1.1 + volumes: + - /data/jasmin/jasmin_resource:/app/resource + environment: + DB_TYPE_MYSQL: ${DB_TYPE_MYSQL:-0} + AMQP_BROKER_HOST: ${AMQP_BROKER_HOST:-rabbit-mq} + AMQP_BROKER_PORT: ${AMQP_BROKER_PORT:-5672} + AMQP_SPEC_FILE: '/app/resource/amqp0-9-1.xml' + DB_HOST: ${DB_HOST:-172.17.0.1} + DB_DATABASE: ${DB_DATABASE:-jasmin} + DB_TABLE: ${DB_TABLE:-submit_log} + DB_USER: ${DB_USER:-jasmin} + DB_PASS: ${DB_PASS:-jasmin} + depends_on: + - rabbit-mq + restart: on-failure + healthcheck: + disable: true \ No newline at end of file diff --git a/config/docker/jasmin/jasmin/config/dlr.cfg b/config/docker/jasmin/jasmin/config/dlr.cfg new file mode 100644 index 0000000..060b236 --- /dev/null +++ b/config/docker/jasmin/jasmin/config/dlr.cfg @@ -0,0 +1,75 @@ +# +# This is the Jasmin DLR Daemon configuration file. +# DLR Daemon will start DLR throwers (http+smpp) and connect to SMPPServerPB +# +# For any modifications to this file, refer to Jasmin Documentation. +# If that does not help, post your question on Jasmin's web forum +# hosted at Google Groups: https://groups.google.com/group/jasmin-sms-gateway +# +# Do NOT simply read the instructions in here without understanding +# what they do. They're here only as hints or reminders. If you are unsure +# consult the online docs. + +[dlr-thrower] +# The following directives define the process of delivering delivery-receipts through http to third party +# application, it is explained in "HTTP API" documentation +# Sets socket timeout in seconds for outgoing client http connections. +#http_timeout = 30 +# Define how many seconds should pass within the queuing system for retrying a failed throw. +#retry_delay = 30 +# Define how many retries should be performed for failing throws of DLR. +#max_retries = 3 + +# Specify the pdu type to consider when throwing a receipt through SMPPs, possible values: +# - data_sm +# - deliver_sm (default pdu) +#dlr_pdu = deliver_sm + +# Specify the server verbosity level. +# This can be one of: +# NOTSET (disable logging) +# DEBUG (a lot of information, useful for development/testing) +# INFO (moderately verbose, what you want in production probably) +# WARNING (only very important / critical messages and errors are logged) +# ERROR (only errors / critical messages are logged) +# CRITICAL (only critical messages are logged) +#log_level = INFO + +# Specify the log file path +#log_file = /var/log/jasmin/dlr-thrower.log + +# When to rotate the log file, possible values: +# S: Seconds +# M: Minutes +# H: Hours +# D: Days +# W0-W6: Weekday (0=Monday) +# midnight: Roll over at midnight +#log_rotate = W6 + +# The following directives define logging patterns including: +# - log_format: using python logging's attributes +# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes +# -log_date_format: using python strftime formating directives +# refer to https://docs.python.org/2/library/time.html#time.strftime +#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s +#log_date_format = %Y-%m-%d %H:%M:%S + +[smpp-server-pb-client] +# The following directives define client connector to SMPPServerPB +#host = 127.0.0.1 +#port = 14000 +#username = smppsadmin +#password = smppspwd + +[amqp-broker] +# The following directives define the way how Jasmin is connecting to the AMQP Broker, +# default values must work with a freshly installed RabbitMQ server. +#host = 127.0.0.1 +host = rabbit-mq +vhost = / +spec = /etc/jasmin/resource/amqp0-9-1.xml +port = 5672 +username = guest +password = guest +#heartbeat = 0 \ No newline at end of file diff --git a/config/docker/jasmin/jasmin/config/dlrlookupd.cfg b/config/docker/jasmin/jasmin/config/dlrlookupd.cfg new file mode 100644 index 0000000..241edac --- /dev/null +++ b/config/docker/jasmin/jasmin/config/dlrlookupd.cfg @@ -0,0 +1,142 @@ +# +# This is the Jasmin DLR Lookup Daemon configuration file. +# DLR Lookup Daemon will fetch dlr mappings from Redis and publish DLRContent +# to the right AMQP route. +# +# For any modifications to this file, refer to Jasmin Documentation. +# If that does not help, post your question on Jasmin's web forum +# hosted at Google Groups: https://groups.google.com/group/jasmin-sms-gateway +# +# Do NOT simply read the instructions in here without understanding +# what they do. They're here only as hints or reminders. If you are unsure +# consult the online docs. + +[amqp-broker] +# The following directives define the way how Jasmin is connecting to the AMQP Broker, +# default values must work with a freshly installed RabbitMQ server. +#host = 127.0.0.1 +host = rabbit-mq +vhost = / +spec = /etc/jasmin/resource/amqp0-9-1.xml +port = 5672 +username = guest +password = guest +#heartbeat = 0 + +# Specify the server verbosity level. +# This can be one of: +# NOTSET (disable logging) +# DEBUG (a lot of information, useful for development/testing) +# INFO (moderately verbose, what you want in production probably) +# WARNING (only very important / critical messages and errors are logged) +# ERROR (only errors / critical messages are logged) +# CRITICAL (only critical messages are logged) +#log_level = INFO + +# Specify the log file path +#log_file = /var/log/jasmin/amqp-client.log + +# When to rotate the log file, possible values: +# S: Seconds +# M: Minutes +# H: Hours +# D: Days +# W0-W6: Weekday (0=Monday) +# midnight: Roll over at midnight +#log_rotate = W6 + +# The following directives define logging patterns including: +# - log_format: using python logging's attributes +# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes +# -log_date_format: using python strftime formating directives +# refer to https://docs.python.org/2/library/time.html#time.strftime +#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s +#log_date_format = %Y-%m-%d %H:%M:%S + +#connection_loss_retry = True +#connection_failure_retry = True +#connection_loss_retry_delay = 10 +#connection_loss_failure_delay = 10 + +[redis-client] +# The following directives define the way how Jasmin is connecting to the redis server, +# default values must work with a freshly installed redis server. +#host = 127.0.0.1 +#port = 6379 +#dbid = 0 +#password = None +#poolsize = 10 + +# Specify the server verbosity level. +# This can be one of: +# NOTSET (disable logging) +# DEBUG (a lot of information, useful for development/testing) +# INFO (moderately verbose, what you want in production probably) +# WARNING (only very important / critical messages and errors are logged) +# ERROR (only errors / critical messages are logged) +# CRITICAL (only critical messages are logged) +#log_level = INFO + +# Specify the log file path +#log_file = /var/log/jasmin/redis-client.log + +# When to rotate the log file, possible values: +# S: Seconds +# M: Minutes +# H: Hours +# D: Days +# W0-W6: Weekday (0=Monday) +# midnight: Roll over at midnight +#log_rotate = W6 + +# The following directives define logging patterns including: +# - log_format: using python logging's attributes +# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes +# -log_date_format: using python strftime formating directives +# refer to https://docs.python.org/2/library/time.html#time.strftime +#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s +#log_date_format = %Y-%m-%d %H:%M:%S + +[dlr] +# DLRLookup process id +pid = dlrlookupd-01 + +# DLRLookup mechanism configuration +#dlr_lookup_retry_delay = 10 +#dlr_lookup_max_retries = 2 + +# If smpp_receipt_on_success_submit_sm_resp is True, every connected user to smpp server will +# receive a receipt (data_sm or deliver_sm) whenever a submit_sm_resp is received +# for a message he sent and requested receipt for it. +#smpp_receipt_on_success_submit_sm_resp = False + +# Specify the server verbosity level. +# This can be one of: +# NOTSET (disable logging) +# DEBUG (a lot of information, useful for development/testing) +# INFO (moderately verbose, what you want in production probably) +# WARNING (only very important / critical messages and errors are logged) +# ERROR (only errors / critical messages are logged) +# CRITICAL (only critical messages are logged) +#log_level = INFO + +# Specify the log file path +#log_file = /var/log/jasmin/messages.log + +# When to rotate the log file, possible values: +# S: Seconds +# M: Minutes +# H: Hours +# D: Days +# W0-W6: Weekday (0=Monday) +# midnight: Roll over at midnight +#log_rotate = midnight + +# The following directives define logging patterns including: +# - log_format: using python logging's attributes +# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes +# -log_date_format: using python strftime formating directives +# refer to https://docs.python.org/2/library/time.html#time.strftime +#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s +#log_date_format = %Y-%m-%d %H:%M:%S +#log_privacy = False \ No newline at end of file diff --git a/config/docker/jasmin/jasmin/config/interceptor.cfg b/config/docker/jasmin/jasmin/config/interceptor.cfg new file mode 100644 index 0000000..43d2c97 --- /dev/null +++ b/config/docker/jasmin/jasmin/config/interceptor.cfg @@ -0,0 +1,57 @@ +# +# This is the Jasmin interceptor configuration file. +# For any modifications to this file, refer to Jasmin Documentation. +# If that does not help, post your question on Jasmin's web forum +# hosted at Google Groups: https://groups.google.com/group/jasmin-sms-gateway +# +# Do NOT simply read the instructions in here without understanding +# what they do. They're here only as hints or reminders. If you are unsure +# consult the online docs. + +[interceptor] +# If you want you can bind a single interface, you can specify its IP here +#bind = 0.0.0.0 + +# Accept connections on the specified port, default is 8987 +#port = 8987 + +# If authentication is True, access will require entering a username and password +# as defined in admin_username and admin_password, you can disable this security +# layer by setting authentication to False, in this case admin_* values are ignored. +#authentication = True +#admin_username = iadmin +# This is a MD5 password digest hex encoded +#admin_password = dd8b84cdb60655fed3b9b2d668c5bd9e + +# Specify the server verbosity level. +# This can be one of: +# NOTSET (disable logging) +# DEBUG (a lot of information, useful for development/testing) +# INFO (moderately verbose, what you want in production probably) +# WARNING (only very important / critical messages and errors are logged) +# ERROR (only errors / critical messages are logged) +# CRITICAL (only critical messages are logged) +#log_level = INFO + +# Specify the log file path +#log_file = /var/log/jasmin/interceptor.log + +# When to rotate the log file, possible values: +# S: Seconds +# M: Minutes +# H: Hours +# D: Days +# W0-W6: Weekday (0=Monday) +# midnight: Roll over at midnight +#log_rotate = W6 + +# The following directives define logging patterns including: +# - log_format: using python logging's attributes +# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes +# -log_date_format: using python strftime formating directives +# refer to https://docs.python.org/2/library/time.html#time.strftime +#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s +#log_date_format = %Y-%m-%d %H:%M:%S + +# This is a duration threshold (seconds) for logging slow scripts. +#log_slow_script = 1 diff --git a/config/docker/jasmin/jasmin/config/jasmin.cfg b/config/docker/jasmin/jasmin/config/jasmin.cfg new file mode 100644 index 0000000..1f06a6f --- /dev/null +++ b/config/docker/jasmin/jasmin/config/jasmin.cfg @@ -0,0 +1,662 @@ +# +# This is the main Jasmin SMS gateway configuration file. +# For any modifications to this file, refer to Jasmin Documentation. +# If that does not help, post your question on Jasmin's web forum +# hosted at Google Groups: https://groups.google.com/group/jasmin-sms-gateway +# +# Do NOT simply read the instructions in here without understanding +# what they do. They're here only as hints or reminders. If you are unsure +# consult the online docs. + +[smpp-server] + +# SMPP Server identifier +#id = "smpps_01" + +# If you want you can bind a single interface, you can specify its IP here +#bind = 0.0.0.0 + +# Accept connections on the specified port, default is 2775 +#port = 2775 + +# Activate billing feature +# May be disabled if not needed/used +#billing_feature = True + +# Timeout for response to bind request +#sessionInitTimerSecs = 30 + +# Enquire link interval +#enquireLinkTimerSecs = 30 + +# Maximum time lapse allowed between transactions, after which, +# the connection is considered as inactive +#inactivityTimerSecs = 300 + +# Timeout for responses to any request PDU +#responseTimerSecs = 60 + +# Timeout for reading a single PDU, this is the maximum lapse of time between +# receiving PDU's header and its complete read, if the PDU reading timed out, +# the connection is considered as 'corrupt' and will reconnect +#pduReadTimerSecs = 10 + +# When message is routed to a SMPP Client connecter: How much time it is kept in +# redis waiting for receipt +#dlr_expiry = 86400 + +# Specify the server verbosity level. +# This can be one of: +# NOTSET (disable logging) +# DEBUG (a lot of information, useful for development/testing) +# INFO (moderately verbose, what you want in production probably) +# WARNING (only very important / critical messages and errors are logged) +# ERROR (only errors / critical messages are logged) +# CRITICAL (only critical messages are logged) +#log_level = INFO + +# Specify the log file path +#log_file = /var/log/jasmin/default-smpps_01.log + +# When to rotate the log file, possible values: +# S: Seconds +# M: Minutes +# H: Hours +# D: Days +# W0-W6: Weekday (0=Monday) +# midnight: Roll over at midnight +#log_rotate = midnight + +# The following directives define logging patterns including: +# - log_format: using python logging's attributes +# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes +# -log_date_format: using python strftime formating directives +# refer to https://docs.python.org/2/library/time.html#time.strftime +#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s +#log_date_format = %Y-%m-%d %H:%M:%S +#log_privacy = False + +[smpp-server-pb] +# If you want you can bind a single interface, you can specify its IP here +#bind = 0.0.0.0 + +# Accept connections on the specified port, default is 14000 +#port = 14000 + +# If authentication is True, access will require entering a username and password +# as defined in admin_username and admin_password, you can disable this security +# layer by setting authentication to False, in this case admin_* values are ignored. +#authentication = True +#admin_username = smppsadmin +# This is a MD5 password digest hex encoded +#admin_password = e97ab122faa16beea8682d84f3d2eea4 + +# Specify the server verbosity level. +# This can be one of: +# NOTSET (disable logging) +# DEBUG (a lot of information, useful for development/testing) +# INFO (moderately verbose, what you want in production probably) +# WARNING (only very important / critical messages and errors are logged) +# ERROR (only errors / critical messages are logged) +# CRITICAL (only critical messages are logged) +#log_level = INFO + +# Specify the log file path +#log_file = /var/log/jasmin/smpp-server-pb.log + +# When to rotate the log file, possible values: +# S: Seconds +# M: Minutes +# H: Hours +# D: Days +# W0-W6: Weekday (0=Monday) +# midnight: Roll over at midnight +#log_rotate = W6 + +# The following directives define logging patterns including: +# - log_format: using python logging's attributes +# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes +# -log_date_format: using python strftime formating directives +# refer to https://docs.python.org/2/library/time.html#time.strftime +#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s +#log_date_format = %Y-%m-%d %H:%M:%S + +[client-management] +# Jasmin persists its configuration profiles in /etc/jasmin/store by +# default. You can specify a custom location here +#store_path = /etc/jasmin/store + +# If you want you can bind a single interface, you can specify its IP here +#bind = 0.0.0.0 + +# Accept connections on the specified port, default is 8989 +#port = 8989 + +# If authentication is True, access will require entering a username and password +# as defined in admin_username and admin_password, you can disable this security +# layer by setting authentication to False, in this case admin_* values are ignored. +#authentication = True +#admin_username = cmadmin +# This is a MD5 password digest hex encoded +#admin_password = e1c5136acafb7016bc965597c992eb82 + +# Specify the server verbosity level. +# This can be one of: +# NOTSET (disable logging) +# DEBUG (a lot of information, useful for development/testing) +# INFO (moderately verbose, what you want in production probably) +# WARNING (only very important / critical messages and errors are logged) +# ERROR (only errors / critical messages are logged) +# CRITICAL (only critical messages are logged) +#log_level = INFO + +# Specify the log file path +#log_file = /var/log/jasmin/smppclient-manager.log + +# When to rotate the log file, possible values: +# S: Seconds +# M: Minutes +# H: Hours +# D: Days +# W0-W6: Weekday (0=Monday) +# midnight: Roll over at midnight +#log_rotate = W6 + +# The following directives define logging patterns including: +# - log_format: using python logging's attributes +# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes +# -log_date_format: using python strftime formating directives +# refer to https://docs.python.org/2/library/time.html#time.strftime +#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s +#log_date_format = %Y-%m-%d %H:%M:%S + +# The protocol version used to pickle objects before transfering +# them to client side, this is used in the client manager only, +# the pickle protocol defined in SMPPClientManagerPBProxy is set +# to 2 and is not configurable +#pickle_protocol = 2 + +[service-smppclient] +# For each smppclient connector a service is associated +# refer to "Message flows" documentation for more details + +# Specify the server verbosity level. +# This can be one of: +# NOTSET (disable logging) +# DEBUG (a lot of information, useful for development/testing) +# INFO (moderately verbose, what you want in production probably) +# WARNING (only very important / critical messages and errors are logged) +# ERROR (only errors / critical messages are logged) +# CRITICAL (only critical messages are logged) +#log_level = INFO + +# Specify the log file path +#log_file = /var/log/jasmin/service-smppclients.log + +# When to rotate the log file, possible values: +# S: Seconds +# M: Minutes +# H: Hours +# D: Days +# W0-W6: Weekday (0=Monday) +# midnight: Roll over at midnight +#log_rotate = W6 + +# The following directives define logging patterns including: +# - log_format: using python logging's attributes +# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes +# -log_date_format: using python strftime formating directives +# refer to https://docs.python.org/2/library/time.html#time.strftime +#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s +#log_date_format = %Y-%m-%d %H:%M:%S + +[sm-listener] +# SM listener consumes submit_sm and deliver_sm messages from amqp broker +# refer to "Message flows" documentation for more details + +# If publish_submit_sm_resp is True, any received SubmitSm PDU will be published +# to the 'messaging' exchange on 'submit.sm.resp.CID' route, useful when you have +# a third party application waiting for these messages. +#publish_submit_sm_resp = False +publish_submit_sm_resp = True + +# If the error is defined in submit_error_retrial, Jasmin will retry sending submit_sm if it +# gets one of these errors. +# submit_sm retrial will be executed 'count' times and delayed for 'delay' seconds each time. +#submit_error_retrial = { +# 'ESME_RSYSERR': {'count': 2, 'delay': 30}, +# 'ESME_RTHROTTLED': {'count': 20, 'delay': 30}, +# 'ESME_RMSGQFUL': {'count': 2, 'delay': 180}, +# 'ESME_RINVSCHED': {'count': 2, 'delay': 300}, +# } + +# The maximum number of seconds a message can stay in queue waiting for SMPPC to get ready for +# delivey (connected and bound). +#submit_max_age_smppc_not_ready = 1200 + +# Delay (seconds) when retrying a submit with a not-yet ready SMPPc +# Hint: for large scale messaging deployment, it is advised to set this value to few seconds +# in order to keep Jasmin free. +#submit_retrial_delay_smppc_not_ready = 30 + +# Specify the server verbosity level. +# This can be one of: +# NOTSET (disable logging) +# DEBUG (a lot of information, useful for development/testing) +# INFO (moderately verbose, what you want in production probably) +# WARNING (only very important / critical messages and errors are logged) +# ERROR (only errors / critical messages are logged) +# CRITICAL (only critical messages are logged) +#log_level = INFO + +# Specify the log file path +#log_file = /var/log/jasmin/messages.log + +# When to rotate the log file, possible values: +# S: Seconds +# M: Minutes +# H: Hours +# D: Days +# W0-W6: Weekday (0=Monday) +# midnight: Roll over at midnight +#log_rotate = midnight + +# The following directives define logging patterns including: +# - log_format: using python logging's attributes +# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes +# -log_date_format: using python strftime formating directives +# refer to https://docs.python.org/2/library/time.html#time.strftime +#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s +#log_date_format = %Y-%m-%d %H:%M:%S +#log_privacy = False + +[dlr] +# DLRLookup process id +#pid = main + +# DLRLookup mechanism configuration +#dlr_lookup_retry_delay = 10 +#dlr_lookup_max_retries = 2 + +# If smpp_receipt_on_success_submit_sm_resp is True, every connected user to smpp server will +# receive a receipt (data_sm or deliver_sm) whenever a submit_sm_resp is received +# for a message he sent and requested receipt for it. +#smpp_receipt_on_success_submit_sm_resp = False + +# Specify the server verbosity level. +# This can be one of: +# NOTSET (disable logging) +# DEBUG (a lot of information, useful for development/testing) +# INFO (moderately verbose, what you want in production probably) +# WARNING (only very important / critical messages and errors are logged) +# ERROR (only errors / critical messages are logged) +# CRITICAL (only critical messages are logged) +#log_level = INFO + +# Specify the log file path +#log_file = /var/log/jasmin/messages.log + +# When to rotate the log file, possible values: +# S: Seconds +# M: Minutes +# H: Hours +# D: Days +# W0-W6: Weekday (0=Monday) +# midnight: Roll over at midnight +#log_rotate = midnight + +# The following directives define logging patterns including: +# - log_format: using python logging's attributes +# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes +# -log_date_format: using python strftime formating directives +# refer to https://docs.python.org/2/library/time.html#time.strftime +#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s +#log_date_format = %Y-%m-%d %H:%M:%S +#log_privacy = False + +[amqp-broker] +# The following directives define the way how Jasmin is connecting to the AMQP Broker, +# default values must work with a freshly installed RabbitMQ server. +host = rabbit-mq +vhost = / +spec = /etc/jasmin/resource/amqp0-9-1.xml +port = 5672 +username = guest +password = guest +heartbeat = 0 +#host = 127.0.0.1 +#vhost = / +#spec = /etc/jasmin/resource/amqp0-9-1.xml +#port = 5672 +#username = guest +#password = guest +#heartbeat = 0 + +# Specify the server verbosity level. +# This can be one of: +# NOTSET (disable logging) +# DEBUG (a lot of information, useful for development/testing) +# INFO (moderately verbose, what you want in production probably) +# WARNING (only very important / critical messages and errors are logged) +# ERROR (only errors / critical messages are logged) +# CRITICAL (only critical messages are logged) +#log_level = INFO + +# Specify the log file path +#log_file = /var/log/jasmin/amqp-client.log + +# When to rotate the log file, possible values: +# S: Seconds +# M: Minutes +# H: Hours +# D: Days +# W0-W6: Weekday (0=Monday) +# midnight: Roll over at midnight +#log_rotate = W6 + +# The following directives define logging patterns including: +# - log_format: using python logging's attributes +# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes +# -log_date_format: using python strftime formating directives +# refer to https://docs.python.org/2/library/time.html#time.strftime +#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s +#log_date_format = %Y-%m-%d %H:%M:%S + +#connection_loss_retry = True +#connection_failure_retry = True +#connection_loss_retry_delay = 10 +#connection_loss_failure_delay = 10 + +[http-api] +# If you want you can bind a single interface, you can specify its IP here +#bind = 0.0.0.0 + +# Accept connections on the specified port, default is 1401 +#port = 1401 + +# Activate billing feature +# May be disabled if not needed/used +#billing_feature = True + +# How many message parts you can get for a long message, default is 5 so you +# can't exceed 800 characters (160x5) when sending a long latin message. +#long_content_max_parts = 5 + +# Splitting long content can be made through SAR options or UDH +# Possible values are: sar and udh +#long_content_split = udh + +# Specify the access log file path +#access_log = /var/log/jasmin/http-access.log + +# Specify the server verbosity level. +# This can be one of: +# NOTSET (disable logging) +# DEBUG (a lot of information, useful for development/testing) +# INFO (moderately verbose, what you want in production probably) +# WARNING (only very important / critical messages and errors are logged) +# ERROR (only errors / critical messages are logged) +# CRITICAL (only critical messages are logged) +#log_level = INFO + +# Specify the log file path +#log_file = /var/log/jasmin/http-api.log + +# When to rotate the log file, possible values: +# S: Seconds +# M: Minutes +# H: Hours +# D: Days +# W0-W6: Weekday (0=Monday) +# midnight: Roll over at midnight +#log_rotate = W6 + +# The following directives define logging patterns including: +# - log_format: using python logging's attributes +# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes +# -log_date_format: using python strftime formating directives +# refer to https://docs.python.org/2/library/time.html#time.strftime +#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s +#log_date_format = %Y-%m-%d %H:%M:%S +#log_privacy = False + +[router] +# Jasmin router persists its routing configuration profiles in /etc/jasmin/store by +# default. You can specify a custom location here +#store_path = /etc/jasmin/store + +# Router will automatically persist users and groups to disk whenever a critical information +# is updated (ex: user balance), persistence is executed every persistence_timer_secs +#persistence_timer_secs = 60 + +# If you want you can bind a single interface, you can specify its IP here +#bind = 0.0.0.0 + +# Accept connections on the specified port, default is 8988 +#port = 8988 + +# If authentication is True, access will require entering a username and password +# as defined in admin_username and admin_password, you can disable this security +# layer by setting authentication to False, in this case admin_* values are ignored. +#authentication = True +#admin_username = radmin +# This is a MD5 password digest hex encoded +#admin_password = 82a606ca5a0deea2b5777756788af5c8 + +# Specify the server verbosity level. +# This can be one of: +# NOTSET (disable logging) +# DEBUG (a lot of information, useful for development/testing) +# INFO (moderately verbose, what you want in production probably) +# WARNING (only very important / critical messages and errors are logged) +# ERROR (only errors / critical messages are logged) +# CRITICAL (only critical messages are logged) +#log_level = INFO + +# Specify the log file path +#log_file = /var/log/jasmin/router.log + +# When to rotate the log file, possible values: +# S: Seconds +# M: Minutes +# H: Hours +# D: Days +# W0-W6: Weekday (0=Monday) +# midnight: Roll over at midnight +#log_rotate = W6 + +# The following directives define logging patterns including: +# - log_format: using python logging's attributes +# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes +# -log_date_format: using python strftime formating directives +# refer to https://docs.python.org/2/library/time.html#time.strftime +#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s +#log_date_format = %Y-%m-%d %H:%M:%S + +# The protocol version used to pickle objects before transfering +# them to client side, this is used in the client manager only, +# the pickle protocol defined in SMPPClientManagerPBProxy is set +# to 2 and is not configurable +#pickle_protocol = 2 + +[deliversm-thrower] +# The following directives define the process of delivery SMS-MO through http to third party +# application, it is explained in "HTTP API" documentation +# Sets socket timeout in seconds for outgoing client http connections. +#http_timeout = 30 +# Define how many seconds should pass within the queuing system for retrying a failed throw. +#retry_delay = 30 +# Define how many retries should be performed for failing throws of SMS-MO. +#max_retries = 3 + +# Specify the server verbosity level. +# This can be one of: +# NOTSET (disable logging) +# DEBUG (a lot of information, useful for development/testing) +# INFO (moderately verbose, what you want in production probably) +# WARNING (only very important / critical messages and errors are logged) +# ERROR (only errors / critical messages are logged) +# CRITICAL (only critical messages are logged) +#log_level = INFO + +# Specify the log file path +#log_file = /var/log/jasmin/deliversm-thrower.log + +# When to rotate the log file, possible values: +# S: Seconds +# M: Minutes +# H: Hours +# D: Days +# W0-W6: Weekday (0=Monday) +# midnight: Roll over at midnight +#log_rotate = W6 + +# The following directives define logging patterns including: +# - log_format: using python logging's attributes +# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes +# -log_date_format: using python strftime formating directives +# refer to https://docs.python.org/2/library/time.html#time.strftime +#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s +#log_date_format = %Y-%m-%d %H:%M:%S + +[dlr-thrower] +# The following directives define the process of delivering delivery-receipts through http to third party +# application, it is explained in "HTTP API" documentation +# Sets socket timeout in seconds for outgoing client http connections. +#http_timeout = 30 +# Define how many seconds should pass within the queuing system for retrying a failed throw. +#retry_delay = 30 +# Define how many retries should be performed for failing throws of DLR. +#max_retries = 3 + +# Specify the pdu type to consider when throwing a receipt through SMPPs, possible values: +# - data_sm +# - deliver_sm (default pdu) +#dlr_pdu = deliver_sm + +# Specify the server verbosity level. +# This can be one of: +# NOTSET (disable logging) +# DEBUG (a lot of information, useful for development/testing) +# INFO (moderately verbose, what you want in production probably) +# WARNING (only very important / critical messages and errors are logged) +# ERROR (only errors / critical messages are logged) +# CRITICAL (only critical messages are logged) +#log_level = INFO + +# Specify the log file path +#log_file = /var/log/jasmin/dlr-thrower.log + +# When to rotate the log file, possible values: +# S: Seconds +# M: Minutes +# H: Hours +# D: Days +# W0-W6: Weekday (0=Monday) +# midnight: Roll over at midnight +#log_rotate = W6 + +# The following directives define logging patterns including: +# - log_format: using python logging's attributes +# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes +# -log_date_format: using python strftime formating directives +# refer to https://docs.python.org/2/library/time.html#time.strftime +#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s +#log_date_format = %Y-%m-%d %H:%M:%S + +[redis-client] +# The following directives define the way how Jasmin is connecting to the redis server, +# default values must work with a freshly installed redis server. +host = redis +port = 6379 +dbid = 0 +password = None +poolsize = 10 +#host = 127.0.0.1 +#port = 6379 +#dbid = 0 +#password = None +#poolsize = 10 + +# Specify the server verbosity level. +# This can be one of: +# NOTSET (disable logging) +# DEBUG (a lot of information, useful for development/testing) +# INFO (moderately verbose, what you want in production probably) +# WARNING (only very important / critical messages and errors are logged) +# ERROR (only errors / critical messages are logged) +# CRITICAL (only critical messages are logged) +#log_level = INFO + +# Specify the log file path +#log_file = /var/log/jasmin/redis-client.log + +# When to rotate the log file, possible values: +# S: Seconds +# M: Minutes +# H: Hours +# D: Days +# W0-W6: Weekday (0=Monday) +# midnight: Roll over at midnight +#log_rotate = W6 + +# The following directives define logging patterns including: +# - log_format: using python logging's attributes +# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes +# -log_date_format: using python strftime formating directives +# refer to https://docs.python.org/2/library/time.html#time.strftime +#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s +#log_date_format = %Y-%m-%d %H:%M:%S + +[jcli] +# If you want you can bind a single interface, you can specify its IP here +#bind = 127.0.0.1 + +# Accept connections on the specified port, default is 8990 +#port = 8990 + +# If authentication is True, access will require entering a username and password +# as defined in admin_username and admin_password, you can disable this security +# layer by setting authentication to False, in this case admin_* values are ignored. +#authentication = True +#admin_username = jcliadmin +# This is a MD5 password digest hex encoded +#admin_password = 79e9b0aa3f3e7c53e916f7ac47439bcb + +# Specify the server verbosity level. +# This can be one of: +# NOTSET (disable logging) +# DEBUG (a lot of information, useful for development/testing) +# INFO (moderately verbose, what you want in production probably) +# WARNING (only very important / critical messages and errors are logged) +# ERROR (only errors / critical messages are logged) +# CRITICAL (only critical messages are logged) +#log_level = INFO + +# Specify the log file path +#log_file = /var/log/jasmin/jcli.log + +# When to rotate the log file, possible values: +# S: Seconds +# M: Minutes +# H: Hours +# D: Days +# W0-W6: Weekday (0=Monday) +# midnight: Roll over at midnight +#log_rotate = W6 + +# The following directives define logging patterns including: +# - log_format: using python logging's attributes +# refer to https://docs.python.org/2/library/logging.html#logrecord-attributes +# -log_date_format: using python strftime formating directives +# refer to https://docs.python.org/2/library/time.html#time.strftime +#log_format = %(asctime)s %(levelname)-8s %(process)d %(message)s +#log_date_format = %Y-%m-%d %H:%M:%S + +[interceptor-client] +# The following directives define client connector to InterceptorPB, it's used when jasmind +# is started with --enable-interceptor-client +#host = 127.0.0.1 +#port = 8987 +#username = iadmin +#password = ipwd diff --git a/config/docker/jasmin/jasmin/logs/.gitignore b/config/docker/jasmin/jasmin/logs/.gitignore new file mode 100644 index 0000000..bf0824e --- /dev/null +++ b/config/docker/jasmin/jasmin/logs/.gitignore @@ -0,0 +1 @@ +*.log \ No newline at end of file diff --git a/config/docker/jasmin/jasmin/resource/amqp0-8.stripped.rabbitmq.xml b/config/docker/jasmin/jasmin/resource/amqp0-8.stripped.rabbitmq.xml new file mode 100644 index 0000000..d1fd2c0 --- /dev/null +++ b/config/docker/jasmin/jasmin/resource/amqp0-8.stripped.rabbitmq.xml @@ -0,0 +1,771 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/docker/jasmin/jasmin/resource/amqp0-9-1.xml b/config/docker/jasmin/jasmin/resource/amqp0-9-1.xml new file mode 100644 index 0000000..da785eb --- /dev/null +++ b/config/docker/jasmin/jasmin/resource/amqp0-9-1.xml @@ -0,0 +1,2843 @@ + + + + + + + + + + + + + + + + + + + + + + + Indicates that the method completed successfully. This reply code is + reserved for future use - the current protocol design does not use positive + confirmation and reply codes are sent only in case of an error. + + + + + + The client attempted to transfer content larger than the server could accept + at the present time. The client may retry at a later time. + + + + + + When the exchange cannot deliver to a consumer when the immediate flag is + set. As a result of pending data on the queue or the absence of any + consumers of the queue. + + + + + + An operator intervened to close the connection for some reason. The client + may retry at some later date. + + + + + + The client tried to work with an unknown virtual host. + + + + + + The client attempted to work with a server entity to which it has no + access due to security settings. + + + + + + The client attempted to work with a server entity that does not exist. + + + + + + The client attempted to work with a server entity to which it has no + access because another client is working with it. + + + + + + The client requested a method that was not allowed because some precondition + failed. + + + + + + The sender sent a malformed frame that the recipient could not decode. + This strongly implies a programming error in the sending peer. + + + + + + The sender sent a frame that contained illegal values for one or more + fields. This strongly implies a programming error in the sending peer. + + + + + + The client sent an invalid sequence of frames, attempting to perform an + operation that was considered invalid by the server. This usually implies + a programming error in the client. + + + + + + The client attempted to work with a channel that had not been correctly + opened. This most likely indicates a fault in the client layer. + + + + + + The peer sent a frame that was not expected, usually in the context of + a content header and body. This strongly indicates a fault in the peer's + content processing. + + + + + + The server could not complete the method because it lacked sufficient + resources. This may be due to the client creating too many of some type + of entity. + + + + + + The client tried to work with some entity in a manner that is prohibited + by the server, due to security settings or by some other criteria. + + + + + + The client tried to use functionality that is not implemented in the + server. + + + + + + The server could not complete the method because of an internal error. + The server may require intervention by an operator in order to resume + normal operations. + + + + + + + + + + Identifier for the consumer, valid within the current channel. + + + + + + The server-assigned and channel-specific delivery tag + + + + The delivery tag is valid only within the channel from which the message was + received. I.e. a client MUST NOT receive a message on one channel and then + acknowledge it on another. + + + + + The server MUST NOT use a zero value for delivery tags. Zero is reserved + for client use, meaning "all messages so far received". + + + + + + + The exchange name is a client-selected string that identifies the exchange for + publish methods. + + + + + + + + + + If this field is set the server does not expect acknowledgements for + messages. That is, when a message is delivered to the client the server + assumes the delivery will succeed and immediately dequeues it. This + functionality may increase performance but at the cost of reliability. + Messages can get lost if a client dies before they are delivered to the + application. + + + + + + If the no-local field is set the server will not send messages to the connection that + published them. + + + + + + If set, the server will not respond to the method. The client should not wait + for a reply method. If the server could not complete the method it will raise a + channel or connection exception. + + + + + + Unconstrained. + + + + + + + + This table provides a set of peer properties, used for identification, debugging, + and general information. + + + + + + The queue name identifies the queue within the vhost. In methods where the queue + name may be blank, and that has no specific significance, this refers to the + 'current' queue for the channel, meaning the last queue that the client declared + on the channel. If the client did not declare a queue, and the method needs a + queue name, this will result in a 502 (syntax error) channel exception. + + + + + + + + This indicates that the message has been previously delivered to this or + another client. + + + + The server SHOULD try to signal redelivered messages when it can. When + redelivering a message that was not successfully acknowledged, the server + SHOULD deliver it to the original client if possible. + + + Declare a shared queue and publish a message to the queue. Consume the + message using explicit acknowledgements, but do not acknowledge the + message. Close the connection, reconnect, and consume from the queue + again. The message should arrive with the redelivered flag set. + + + + + The client MUST NOT rely on the redelivered field but should take it as a + hint that the message may already have been processed. A fully robust + client must be able to track duplicate received messages on non-transacted, + and locally-transacted channels. + + + + + + + The number of messages in the queue, which will be zero for newly-declared + queues. This is the number of messages present in the queue, and committed + if the channel on which they were published is transacted, that are not + waiting acknowledgement. + + + + + + The reply code. The AMQ reply codes are defined as constants at the start + of this formal specification. + + + + + + + The localised reply text. This text can be logged as an aid to resolving + issues. + + + + + + + + + + + + + + + + + + + + The connection class provides methods for a client to establish a network connection to + a server, and for both peers to operate the connection thereafter. + + + + connection = open-connection *use-connection close-connection + open-connection = C:protocol-header + S:START C:START-OK + *challenge + S:TUNE C:TUNE-OK + C:OPEN S:OPEN-OK + challenge = S:SECURE C:SECURE-OK + use-connection = *channel + close-connection = C:CLOSE S:CLOSE-OK + / S:CLOSE C:CLOSE-OK + + + + + + + + + + This method starts the connection negotiation process by telling the client the + protocol version that the server proposes, along with a list of security mechanisms + which the client can use for authentication. + + + + + If the server cannot support the protocol specified in the protocol header, + it MUST respond with a valid protocol header and then close the socket + connection. + + + The client sends a protocol header containing an invalid protocol name. + The server MUST respond by sending a valid protocol header and then closing + the connection. + + + + + The server MUST provide a protocol version that is lower than or equal to + that requested by the client in the protocol header. + + + The client requests a protocol version that is higher than any valid + implementation, e.g. 2.0. The server must respond with a protocol header + indicating its supported protocol version, e.g. 1.0. + + + + + If the client cannot handle the protocol version suggested by the server + it MUST close the socket connection without sending any further data. + + + The server sends a protocol version that is lower than any valid + implementation, e.g. 0.1. The client must respond by closing the + connection without sending any further data. + + + + + + + + + The major version number can take any value from 0 to 99 as defined in the + AMQP specification. + + + + + + The minor version number can take any value from 0 to 99 as defined in the + AMQP specification. + + + + + + + The properties SHOULD contain at least these fields: "host", specifying the + server host name or address, "product", giving the name of the server product, + "version", giving the name of the server version, "platform", giving the name + of the operating system, "copyright", if appropriate, and "information", giving + other general information. + + + Client connects to server and inspects the server properties. It checks for + the presence of the required fields. + + + + + + + A list of the security mechanisms that the server supports, delimited by spaces. + + + + + + + A list of the message locales that the server supports, delimited by spaces. The + locale defines the language in which the server will send reply texts. + + + + The server MUST support at least the en_US locale. + + + Client connects to server and inspects the locales field. It checks for + the presence of the required locale(s). + + + + + + + + + This method selects a SASL security mechanism. + + + + + + + + + The properties SHOULD contain at least these fields: "product", giving the name + of the client product, "version", giving the name of the client version, "platform", + giving the name of the operating system, "copyright", if appropriate, and + "information", giving other general information. + + + + + + + A single security mechanisms selected by the client, which must be one of those + specified by the server. + + + + The client SHOULD authenticate using the highest-level security profile it + can handle from the list provided by the server. + + + + + If the mechanism field does not contain one of the security mechanisms + proposed by the server in the Start method, the server MUST close the + connection without sending any further data. + + + Client connects to server and sends an invalid security mechanism. The + server must respond by closing the connection (a socket close, with no + connection close negotiation). + + + + + + + + A block of opaque data passed to the security mechanism. The contents of this + data are defined by the SASL security mechanism. + + + + + + + A single message locale selected by the client, which must be one of those + specified by the server. + + + + + + + + + + The SASL protocol works by exchanging challenges and responses until both peers have + received sufficient information to authenticate each other. This method challenges + the client to provide more information. + + + + + + + + Challenge information, a block of opaque binary data passed to the security + mechanism. + + + + + + + This method attempts to authenticate, passing a block of SASL data for the security + mechanism at the server side. + + + + + + + A block of opaque data passed to the security mechanism. The contents of this + data are defined by the SASL security mechanism. + + + + + + + + + + This method proposes a set of connection configuration values to the client. The + client can accept and/or adjust these. + + + + + + + + + Specifies highest channel number that the server permits. Usable channel numbers + are in the range 1..channel-max. Zero indicates no specified limit. + + + + + + The largest frame size that the server proposes for the connection, including + frame header and end-byte. The client can negotiate a lower value. Zero means + that the server does not impose any specific limit but may reject very large + frames if it cannot allocate resources for them. + + + + Until the frame-max has been negotiated, both peers MUST accept frames of up + to frame-min-size octets large, and the minimum negotiated value for frame-max + is also frame-min-size. + + + Client connects to server and sends a large properties field, creating a frame + of frame-min-size octets. The server must accept this frame. + + + + + + + The delay, in seconds, of the connection heartbeat that the server wants. + Zero means the server does not want a heartbeat. + + + + + + + This method sends the client's connection tuning parameters to the server. + Certain fields are negotiated, others provide capability information. + + + + + + + The maximum total number of channels that the client will use per connection. + + + + If the client specifies a channel max that is higher than the value provided + by the server, the server MUST close the connection without attempting a + negotiated close. The server may report the error in some fashion to assist + implementors. + + + + + + + + + The largest frame size that the client and server will use for the connection. + Zero means that the client does not impose any specific limit but may reject + very large frames if it cannot allocate resources for them. Note that the + frame-max limit applies principally to content frames, where large contents can + be broken into frames of arbitrary size. + + + + Until the frame-max has been negotiated, both peers MUST accept frames of up + to frame-min-size octets large, and the minimum negotiated value for frame-max + is also frame-min-size. + + + + + If the client specifies a frame max that is higher than the value provided + by the server, the server MUST close the connection without attempting a + negotiated close. The server may report the error in some fashion to assist + implementors. + + + + + + + The delay, in seconds, of the connection heartbeat that the client wants. Zero + means the client does not want a heartbeat. + + + + + + + + + This method opens a connection to a virtual host, which is a collection of + resources, and acts to separate multiple application domains within a server. + The server may apply arbitrary limits per virtual host, such as the number + of each type of entity that may be used, per connection and/or in total. + + + + + + + + The name of the virtual host to work with. + + + + If the server supports multiple virtual hosts, it MUST enforce a full + separation of exchanges, queues, and all associated entities per virtual + host. An application, connected to a specific virtual host, MUST NOT be able + to access resources of another virtual host. + + + + + The server SHOULD verify that the client has permission to access the + specified virtual host. + + + + + + + + + + + + This method signals to the client that the connection is ready for use. + + + + + + + + + + + This method indicates that the sender wants to close the connection. This may be + due to internal conditions (e.g. a forced shut-down) or due to an error handling + a specific method, i.e. an exception. When a close is due to an exception, the + sender provides the class and method id of the method which caused the exception. + + + + After sending this method, any received methods except Close and Close-OK MUST + be discarded. The response to receiving a Close after sending Close must be to + send Close-Ok. + + + + + + + + + + + + + When the close is provoked by a method exception, this is the class of the + method. + + + + + + When the close is provoked by a method exception, this is the ID of the method. + + + + + + + This method confirms a Connection.Close method and tells the recipient that it is + safe to release resources for the connection and close the socket. + + + + A peer that detects a socket closure without having received a Close-Ok + handshake method SHOULD log the error. + + + + + + + + + + + + The channel class provides methods for a client to establish a channel to a + server and for both peers to operate the channel thereafter. + + + + channel = open-channel *use-channel close-channel + open-channel = C:OPEN S:OPEN-OK + use-channel = C:FLOW S:FLOW-OK + / S:FLOW C:FLOW-OK + / functional-class + close-channel = C:CLOSE S:CLOSE-OK + / S:CLOSE C:CLOSE-OK + + + + + + + + + + This method opens a channel to the server. + + + + The client MUST NOT use this method on an already-opened channel. + + + Client opens a channel and then reopens the same channel. + + + + + + + + + + + This method signals to the client that the channel is ready for use. + + + + + + + + + + + This method asks the peer to pause or restart the flow of content data sent by + a consumer. This is a simple flow-control mechanism that a peer can use to avoid + overflowing its queues or otherwise finding itself receiving more messages than + it can process. Note that this method is not intended for window control. It does + not affect contents returned by Basic.Get-Ok methods. + + + + + When a new channel is opened, it is active (flow is active). Some applications + assume that channels are inactive until started. To emulate this behaviour a + client MAY open the channel, then pause it. + + + + + + When sending content frames, a peer SHOULD monitor the channel for incoming + methods and respond to a Channel.Flow as rapidly as possible. + + + + + + A peer MAY use the Channel.Flow method to throttle incoming content data for + internal reasons, for example, when exchanging data over a slower connection. + + + + + + The peer that requests a Channel.Flow method MAY disconnect and/or ban a peer + that does not respect the request. This is to prevent badly-behaved clients + from overwhelming a server. + + + + + + + + + + + If 1, the peer starts sending content frames. If 0, the peer stops sending + content frames. + + + + + + + Confirms to the peer that a flow command was received and processed. + + + + + + Confirms the setting of the processed flow method: 1 means the peer will start + sending or continue to send content frames; 0 means it will not. + + + + + + + + + This method indicates that the sender wants to close the channel. This may be due to + internal conditions (e.g. a forced shut-down) or due to an error handling a specific + method, i.e. an exception. When a close is due to an exception, the sender provides + the class and method id of the method which caused the exception. + + + + After sending this method, any received methods except Close and Close-OK MUST + be discarded. The response to receiving a Close after sending Close must be to + send Close-Ok. + + + + + + + + + + + + + When the close is provoked by a method exception, this is the class of the + method. + + + + + + When the close is provoked by a method exception, this is the ID of the method. + + + + + + + This method confirms a Channel.Close method and tells the recipient that it is safe + to release resources for the channel. + + + + A peer that detects a socket closure without having received a Channel.Close-Ok + handshake method SHOULD log the error. + + + + + + + + + + + + Exchanges match and distribute messages across queues. Exchanges can be configured in + the server or declared at runtime. + + + + exchange = C:DECLARE S:DECLARE-OK + / C:DELETE S:DELETE-OK + + + + + + + + The server MUST implement these standard exchange types: fanout, direct. + + + Client attempts to declare an exchange with each of these standard types. + + + + + The server SHOULD implement these standard exchange types: topic, headers. + + + Client attempts to declare an exchange with each of these standard types. + + + + + The server MUST, in each virtual host, pre-declare an exchange instance + for each standard exchange type that it implements, where the name of the + exchange instance, if defined, is "amq." followed by the exchange type name. + + + The server MUST, in each virtual host, pre-declare at least two direct + exchange instances: one named "amq.direct", the other with no public name + that serves as a default exchange for Publish methods. + + + Client declares a temporary queue and attempts to bind to each required + exchange instance ("amq.fanout", "amq.direct", "amq.topic", and "amq.headers" + if those types are defined). + + + + + The server MUST pre-declare a direct exchange with no public name to act as + the default exchange for content Publish methods and for default queue bindings. + + + Client checks that the default exchange is active by specifying a queue + binding with no exchange name, and publishing a message with a suitable + routing key but without specifying the exchange name, then ensuring that + the message arrives in the queue correctly. + + + + + The server MUST NOT allow clients to access the default exchange except + by specifying an empty exchange name in the Queue.Bind and content Publish + methods. + + + + + The server MAY implement other exchange types as wanted. + + + + + + + + This method creates an exchange if it does not already exist, and if the exchange + exists, verifies that it is of the correct and expected class. + + + + The server SHOULD support a minimum of 16 exchanges per virtual host and + ideally, impose no limit except as defined by available resources. + + + The client declares as many exchanges as it can until the server reports + an error; the number of exchanges successfully declared must be at least + sixteen. + + + + + + + + + + + + + Exchange names starting with "amq." are reserved for pre-declared and + standardised exchanges. The client MAY declare an exchange starting with + "amq." if the passive option is set, or the exchange already exists. + + + The client attempts to declare a non-existing exchange starting with + "amq." and with the passive option set to zero. + + + + + The exchange name consists of a non-empty sequence of these characters: + letters, digits, hyphen, underscore, period, or colon. + + + The client attempts to declare an exchange with an illegal name. + + + + + + + + Each exchange belongs to one of a set of exchange types implemented by the + server. The exchange types define the functionality of the exchange - i.e. how + messages are routed through it. It is not valid or meaningful to attempt to + change the type of an existing exchange. + + + + Exchanges cannot be redeclared with different types. The client MUST not + attempt to redeclare an existing exchange with a different type than used + in the original Exchange.Declare method. + + + TODO. + + + + + The client MUST NOT attempt to declare an exchange with a type that the + server does not support. + + + TODO. + + + + + + + If set, the server will reply with Declare-Ok if the exchange already + exists with the same name, and raise an error if not. The client can + use this to check whether an exchange exists without modifying the + server state. When set, all other method fields except name and no-wait + are ignored. A declare with both passive and no-wait has no effect. + Arguments are compared for semantic equivalence. + + + + If set, and the exchange does not already exist, the server MUST + raise a channel exception with reply code 404 (not found). + + + TODO. + + + + + If not set and the exchange exists, the server MUST check that the + existing exchange has the same values for type, durable, and arguments + fields. The server MUST respond with Declare-Ok if the requested + exchange matches these fields, and MUST raise a channel exception if + not. + + + TODO. + + + + + + + If set when creating a new exchange, the exchange will be marked as durable. + Durable exchanges remain active when a server restarts. Non-durable exchanges + (transient exchanges) are purged if/when a server restarts. + + + + The server MUST support both durable and transient exchanges. + + + TODO. + + + + + + + + + + + + + A set of arguments for the declaration. The syntax and semantics of these + arguments depends on the server implementation. + + + + + + + This method confirms a Declare method and confirms the name of the exchange, + essential for automatically-named exchanges. + + + + + + + + + This method deletes an exchange. When an exchange is deleted all queue bindings on + the exchange are cancelled. + + + + + + + + + + + + The client MUST NOT attempt to delete an exchange that does not exist. + + + + + + + + If set, the server will only delete the exchange if it has no queue bindings. If + the exchange has queue bindings the server does not delete it but raises a + channel exception instead. + + + + The server MUST NOT delete an exchange that has bindings on it, if the if-unused + field is true. + + + The client declares an exchange, binds a queue to it, then tries to delete it + setting if-unused to true. + + + + + + + + + This method confirms the deletion of an exchange. + + + + + + + + + Queues store and forward messages. Queues can be configured in the server or created at + runtime. Queues must be attached to at least one exchange in order to receive messages + from publishers. + + + + queue = C:DECLARE S:DECLARE-OK + / C:BIND S:BIND-OK + / C:UNBIND S:UNBIND-OK + / C:PURGE S:PURGE-OK + / C:DELETE S:DELETE-OK + + + + + + + + + + This method creates or checks a queue. When creating a new queue the client can + specify various properties that control the durability of the queue and its + contents, and the level of sharing for the queue. + + + + + The server MUST create a default binding for a newly-declared queue to the + default exchange, which is an exchange of type 'direct' and use the queue + name as the routing key. + + + Client declares a new queue, and then without explicitly binding it to an + exchange, attempts to send a message through the default exchange binding, + i.e. publish a message to the empty exchange, with the queue name as routing + key. + + + + + + The server SHOULD support a minimum of 256 queues per virtual host and ideally, + impose no limit except as defined by available resources. + + + Client attempts to declare as many queues as it can until the server reports + an error. The resulting count must at least be 256. + + + + + + + + + + + + + The queue name MAY be empty, in which case the server MUST create a new + queue with a unique generated name and return this to the client in the + Declare-Ok method. + + + Client attempts to declare several queues with an empty name. The client then + verifies that the server-assigned names are unique and different. + + + + + Queue names starting with "amq." are reserved for pre-declared and + standardised queues. The client MAY declare a queue starting with + "amq." if the passive option is set, or the queue already exists. + + + The client attempts to declare a non-existing queue starting with + "amq." and with the passive option set to zero. + + + + + The queue name can be empty, or a sequence of these characters: + letters, digits, hyphen, underscore, period, or colon. + + + The client attempts to declare a queue with an illegal name. + + + + + + + If set, the server will reply with Declare-Ok if the queue already + exists with the same name, and raise an error if not. The client can + use this to check whether a queue exists without modifying the + server state. When set, all other method fields except name and no-wait + are ignored. A declare with both passive and no-wait has no effect. + Arguments are compared for semantic equivalence. + + + + The client MAY ask the server to assert that a queue exists without + creating the queue if not. If the queue does not exist, the server + treats this as a failure. + + + Client declares an existing queue with the passive option and expects + the server to respond with a declare-ok. Client then attempts to declare + a non-existent queue with the passive option, and the server must close + the channel with the correct reply-code. + + + + + If not set and the queue exists, the server MUST check that the + existing queue has the same values for durable, exclusive, auto-delete, + and arguments fields. The server MUST respond with Declare-Ok if the + requested queue matches these fields, and MUST raise a channel exception + if not. + + + TODO. + + + + + + + If set when creating a new queue, the queue will be marked as durable. Durable + queues remain active when a server restarts. Non-durable queues (transient + queues) are purged if/when a server restarts. Note that durable queues do not + necessarily hold persistent messages, although it does not make sense to send + persistent messages to a transient queue. + + + + The server MUST recreate the durable queue after a restart. + + + Client declares a durable queue. The server is then restarted. The client + then attempts to send a message to the queue. The message should be successfully + delivered. + + + + + The server MUST support both durable and transient queues. + + A client declares two named queues, one durable and one transient. + + + + + + + Exclusive queues may only be accessed by the current connection, and are + deleted when that connection closes. Passive declaration of an exclusive + queue by other connections are not allowed. + + + + + The server MUST support both exclusive (private) and non-exclusive (shared) + queues. + + + A client declares two named queues, one exclusive and one non-exclusive. + + + + + + The client MAY NOT attempt to use a queue that was declared as exclusive + by another still-open connection. + + + One client declares an exclusive queue. A second client on a different + connection attempts to declare, bind, consume, purge, delete, or declare + a queue of the same name. + + + + + + + If set, the queue is deleted when all consumers have finished using it. The last + consumer can be cancelled either explicitly or because its channel is closed. If + there was no consumer ever on the queue, it won't be deleted. Applications can + explicitly delete auto-delete queues using the Delete method as normal. + + + + + The server MUST ignore the auto-delete field if the queue already exists. + + + Client declares two named queues, one as auto-delete and one explicit-delete. + Client then attempts to declare the two queues using the same names again, + but reversing the value of the auto-delete field in each case. Verify that the + queues still exist with the original auto-delete flag values. + + + + + + + + + A set of arguments for the declaration. The syntax and semantics of these + arguments depends on the server implementation. + + + + + + + This method confirms a Declare method and confirms the name of the queue, essential + for automatically-named queues. + + + + + + + Reports the name of the queue. If the server generated a queue name, this field + contains that name. + + + + + + + + + Reports the number of active consumers for the queue. Note that consumers can + suspend activity (Channel.Flow) in which case they do not appear in this count. + + + + + + + + + This method binds a queue to an exchange. Until a queue is bound it will not + receive any messages. In a classic messaging model, store-and-forward queues + are bound to a direct exchange and subscription queues are bound to a topic + exchange. + + + + + A server MUST allow ignore duplicate bindings - that is, two or more bind + methods for a specific queue, with identical arguments - without treating these + as an error. + + + A client binds a named queue to an exchange. The client then repeats the bind + (with identical arguments). + + + + + + A server MUST not deliver the same message more than once to a queue, even if + the queue has multiple bindings that match the message. + + + A client declares a named queue and binds it using multiple bindings to the + amq.topic exchange. The client then publishes a message that matches all its + bindings. + + + + + + The server MUST allow a durable queue to bind to a transient exchange. + + + A client declares a transient exchange. The client then declares a named durable + queue and then attempts to bind the transient exchange to the durable queue. + + + + + + Bindings of durable queues to durable exchanges are automatically durable + and the server MUST restore such bindings after a server restart. + + + A server declares a named durable queue and binds it to a durable exchange. The + server is restarted. The client then attempts to use the queue/exchange combination. + + + + + + The server SHOULD support at least 4 bindings per queue, and ideally, impose no + limit except as defined by available resources. + + + A client declares a named queue and attempts to bind it to 4 different + exchanges. + + + + + + + + + + + + Specifies the name of the queue to bind. + + + The client MUST either specify a queue name or have previously declared a + queue on the same channel + + + The client opens a channel and attempts to bind an unnamed queue. + + + + + The client MUST NOT attempt to bind a queue that does not exist. + + + The client attempts to bind a non-existent queue. + + + + + + + + A client MUST NOT be allowed to bind a queue to a non-existent exchange. + + + A client attempts to bind an named queue to a undeclared exchange. + + + + + The server MUST accept a blank exchange name to mean the default exchange. + + + The client declares a queue and binds it to a blank exchange name. + + + + + + + Specifies the routing key for the binding. The routing key is used for routing + messages depending on the exchange configuration. Not all exchanges use a + routing key - refer to the specific exchange documentation. If the queue name + is empty, the server uses the last queue declared on the channel. If the + routing key is also empty, the server uses this queue name for the routing + key as well. If the queue name is provided but the routing key is empty, the + server does the binding with that empty routing key. The meaning of empty + routing keys depends on the exchange implementation. + + + + If a message queue binds to a direct exchange using routing key K and a + publisher sends the exchange a message with routing key R, then the message + MUST be passed to the message queue if K = R. + + + + + + + + + A set of arguments for the binding. The syntax and semantics of these arguments + depends on the exchange class. + + + + + + This method confirms that the bind was successful. + + + + + + + + This method unbinds a queue from an exchange. + + If a unbind fails, the server MUST raise a connection exception. + + + + + + + + + Specifies the name of the queue to unbind. + + + The client MUST either specify a queue name or have previously declared a + queue on the same channel + + + The client opens a channel and attempts to unbind an unnamed queue. + + + + + The client MUST NOT attempt to unbind a queue that does not exist. + + + The client attempts to unbind a non-existent queue. + + + + + + The name of the exchange to unbind from. + + + The client MUST NOT attempt to unbind a queue from an exchange that + does not exist. + + + The client attempts to unbind a queue from a non-existent exchange. + + + + + The server MUST accept a blank exchange name to mean the default exchange. + + + The client declares a queue and binds it to a blank exchange name. + + + + + + Specifies the routing key of the binding to unbind. + + + + Specifies the arguments of the binding to unbind. + + + + + This method confirms that the unbind was successful. + + + + + + + + This method removes all messages from a queue which are not awaiting + acknowledgment. + + + + + The server MUST NOT purge messages that have already been sent to a client + but not yet acknowledged. + + + + + + The server MAY implement a purge queue or log that allows system administrators + to recover accidentally-purged messages. The server SHOULD NOT keep purged + messages in the same storage spaces as the live messages since the volumes of + purged messages may get very large. + + + + + + + + + + + + Specifies the name of the queue to purge. + + + The client MUST either specify a queue name or have previously declared a + queue on the same channel + + + The client opens a channel and attempts to purge an unnamed queue. + + + + + The client MUST NOT attempt to purge a queue that does not exist. + + + The client attempts to purge a non-existent queue. + + + + + + + + + This method confirms the purge of a queue. + + + + + + Reports the number of messages purged. + + + + + + + + + This method deletes a queue. When a queue is deleted any pending messages are sent + to a dead-letter queue if this is defined in the server configuration, and all + consumers on the queue are cancelled. + + + + + The server SHOULD use a dead-letter queue to hold messages that were pending on + a deleted queue, and MAY provide facilities for a system administrator to move + these messages back to an active queue. + + + + + + + + + + + + Specifies the name of the queue to delete. + + + The client MUST either specify a queue name or have previously declared a + queue on the same channel + + + The client opens a channel and attempts to delete an unnamed queue. + + + + + The client MUST NOT attempt to delete a queue that does not exist. + + + The client attempts to delete a non-existent queue. + + + + + + + If set, the server will only delete the queue if it has no consumers. If the + queue has consumers the server does does not delete it but raises a channel + exception instead. + + + + The server MUST NOT delete a queue that has consumers on it, if the if-unused + field is true. + + + The client declares a queue, and consumes from it, then tries to delete it + setting if-unused to true. + + + + + + + If set, the server will only delete the queue if it has no messages. + + + + The server MUST NOT delete a queue that has messages on it, if the + if-empty field is true. + + + The client declares a queue, binds it and publishes some messages into it, + then tries to delete it setting if-empty to true. + + + + + + + + + This method confirms the deletion of a queue. + + + + + Reports the number of messages deleted. + + + + + + + + + The Basic class provides methods that support an industry-standard messaging model. + + + + basic = C:QOS S:QOS-OK + / C:CONSUME S:CONSUME-OK + / C:CANCEL S:CANCEL-OK + / C:PUBLISH content + / S:RETURN content + / S:DELIVER content + / C:GET ( S:GET-OK content / S:GET-EMPTY ) + / C:ACK + / C:REJECT + / C:RECOVER-ASYNC + / C:RECOVER S:RECOVER-OK + + + + + + + + The server SHOULD respect the persistent property of basic messages and + SHOULD make a best-effort to hold persistent basic messages on a reliable + storage mechanism. + + + Send a persistent message to queue, stop server, restart server and then + verify whether message is still present. Assumes that queues are durable. + Persistence without durable queues makes no sense. + + + + + + The server MUST NOT discard a persistent basic message in case of a queue + overflow. + + + Declare a queue overflow situation with persistent messages and verify that + messages do not get lost (presumably the server will write them to disk). + + + + + + The server MAY use the Channel.Flow method to slow or stop a basic message + publisher when necessary. + + + Declare a queue overflow situation with non-persistent messages and verify + whether the server responds with Channel.Flow or not. Repeat with persistent + messages. + + + + + + The server MAY overflow non-persistent basic messages to persistent + storage. + + + + + + + The server MAY discard or dead-letter non-persistent basic messages on a + priority basis if the queue size exceeds some configured limit. + + + + + + + The server MUST implement at least 2 priority levels for basic messages, + where priorities 0-4 and 5-9 are treated as two distinct levels. + + + Send a number of priority 0 messages to a queue. Send one priority 9 + message. Consume messages from the queue and verify that the first message + received was priority 9. + + + + + + The server MAY implement up to 10 priority levels. + + + Send a number of messages with mixed priorities to a queue, so that all + priority values from 0 to 9 are exercised. A good scenario would be ten + messages in low-to-high priority. Consume from queue and verify how many + priority levels emerge. + + + + + + The server MUST deliver messages of the same priority in order irrespective of + their individual persistence. + + + Send a set of messages with the same priority but different persistence + settings to a queue. Consume and verify that messages arrive in same order + as originally published. + + + + + + The server MUST support un-acknowledged delivery of Basic content, i.e. + consumers with the no-ack field set to TRUE. + + + + + + The server MUST support explicitly acknowledged delivery of Basic content, + i.e. consumers with the no-ack field set to FALSE. + + + Declare a queue and a consumer using explicit acknowledgements. Publish a + set of messages to the queue. Consume the messages but acknowledge only + half of them. Disconnect and reconnect, and consume from the queue. + Verify that the remaining messages are received. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This method requests a specific quality of service. The QoS can be specified for the + current channel or for all channels on the connection. The particular properties and + semantics of a qos method always depend on the content class semantics. Though the + qos method could in principle apply to both peers, it is currently meaningful only + for the server. + + + + + + + + The client can request that messages be sent in advance so that when the client + finishes processing a message, the following message is already held locally, + rather than needing to be sent down the channel. Prefetching gives a performance + improvement. This field specifies the prefetch window size in octets. The server + will send a message in advance if it is equal to or smaller in size than the + available prefetch size (and also falls into other prefetch limits). May be set + to zero, meaning "no specific limit", although other prefetch limits may still + apply. The prefetch-size is ignored if the no-ack option is set. + + + + The server MUST ignore this setting when the client is not processing any + messages - i.e. the prefetch size does not limit the transfer of single + messages to a client, only the sending in advance of more messages while + the client still has one or more unacknowledged messages. + + + Define a QoS prefetch-size limit and send a single message that exceeds + that limit. Verify that the message arrives correctly. + + + + + + + Specifies a prefetch window in terms of whole messages. This field may be used + in combination with the prefetch-size field; a message will only be sent in + advance if both prefetch windows (and those at the channel and connection level) + allow it. The prefetch-count is ignored if the no-ack option is set. + + + + The server may send less data in advance than allowed by the client's + specified prefetch windows but it MUST NOT send more. + + + Define a QoS prefetch-size limit and a prefetch-count limit greater than + one. Send multiple messages that exceed the prefetch size. Verify that + no more than one message arrives at once. + + + + + + + By default the QoS settings apply to the current channel only. If this field is + set, they are applied to the entire connection. + + + + + + + This method tells the client that the requested QoS levels could be handled by the + server. The requested QoS applies to all active consumers until a new QoS is + defined. + + + + + + + + + This method asks the server to start a "consumer", which is a transient request for + messages from a specific queue. Consumers last as long as the channel they were + declared on, or until the client cancels them. + + + + + The server SHOULD support at least 16 consumers per queue, and ideally, impose + no limit except as defined by available resources. + + + Declare a queue and create consumers on that queue until the server closes the + connection. Verify that the number of consumers created was at least sixteen + and report the total number. + + + + + + + + + + + Specifies the name of the queue to consume from. + + + + + Specifies the identifier for the consumer. The consumer tag is local to a + channel, so two clients can use the same consumer tags. If this field is + empty the server will generate a unique tag. + + + + The client MUST NOT specify a tag that refers to an existing consumer. + + + Attempt to create two consumers with the same non-empty tag, on the + same channel. + + + + + The consumer tag is valid only within the channel from which the + consumer was created. I.e. a client MUST NOT create a consumer in one + channel and then use it in another. + + + Attempt to create a consumer in one channel, then use in another channel, + in which consumers have also been created (to test that the server uses + unique consumer tags). + + + + + + + + + + + Request exclusive consumer access, meaning only this consumer can access the + queue. + + + + + The client MAY NOT gain exclusive access to a queue that already has + active consumers. + + + Open two connections to a server, and in one connection declare a shared + (non-exclusive) queue and then consume from the queue. In the second + connection attempt to consume from the same queue using the exclusive + option. + + + + + + + + + A set of arguments for the consume. The syntax and semantics of these + arguments depends on the server implementation. + + + + + + + The server provides the client with a consumer tag, which is used by the client + for methods called on the consumer at a later stage. + + + + + Holds the consumer tag specified by the client or provided by the server. + + + + + + + + + This method cancels a consumer. This does not affect already delivered + messages, but it does mean the server will not send any more messages for + that consumer. The client may receive an arbitrary number of messages in + between sending the cancel method and receiving the cancel-ok reply. + + + + + If the queue does not exist the server MUST ignore the cancel method, so + long as the consumer tag is valid for that channel. + + + TODO. + + + + + + + + + + + + + This method confirms that the cancellation was completed. + + + + + + + + + + This method publishes a message to a specific exchange. The message will be routed + to queues as defined by the exchange configuration and distributed to any active + consumers when the transaction, if any, is committed. + + + + + + + + + + Specifies the name of the exchange to publish to. The exchange name can be + empty, meaning the default exchange. If the exchange name is specified, and that + exchange does not exist, the server will raise a channel exception. + + + + + The client MUST NOT attempt to publish a content to an exchange that + does not exist. + + + The client attempts to publish a content to a non-existent exchange. + + + + + The server MUST accept a blank exchange name to mean the default exchange. + + + The client declares a queue and binds it to a blank exchange name. + + + + + If the exchange was declared as an internal exchange, the server MUST raise + a channel exception with a reply code 403 (access refused). + + + TODO. + + + + + + The exchange MAY refuse basic content in which case it MUST raise a channel + exception with reply code 540 (not implemented). + + + TODO. + + + + + + + Specifies the routing key for the message. The routing key is used for routing + messages depending on the exchange configuration. + + + + + + This flag tells the server how to react if the message cannot be routed to a + queue. If this flag is set, the server will return an unroutable message with a + Return method. If this flag is zero, the server silently drops the message. + + + + + The server SHOULD implement the mandatory flag. + + + TODO. + + + + + + + This flag tells the server how to react if the message cannot be routed to a + queue consumer immediately. If this flag is set, the server will return an + undeliverable message with a Return method. If this flag is zero, the server + will queue the message, but with no guarantee that it will ever be consumed. + + + + + The server SHOULD implement the immediate flag. + + + TODO. + + + + + + + + This method returns an undeliverable message that was published with the "immediate" + flag set, or an unroutable message published with the "mandatory" flag set. The + reply code and text provide information about the reason that the message was + undeliverable. + + + + + + + + + + Specifies the name of the exchange that the message was originally published + to. May be empty, meaning the default exchange. + + + + + + Specifies the routing key name specified when the message was published. + + + + + + + + + This method delivers a message to the client, via a consumer. In the asynchronous + message delivery model, the client starts a consumer using the Consume method, then + the server responds with Deliver methods as and when messages arrive for that + consumer. + + + + + The server SHOULD track the number of times a message has been delivered to + clients and when a message is redelivered a certain number of times - e.g. 5 + times - without being acknowledged, the server SHOULD consider the message to be + unprocessable (possibly causing client applications to abort), and move the + message to a dead letter queue. + + + TODO. + + + + + + + + + + + + Specifies the name of the exchange that the message was originally published to. + May be empty, indicating the default exchange. + + + + + Specifies the routing key name specified when the message was published. + + + + + + + + This method provides a direct access to the messages in a queue using a synchronous + dialogue that is designed for specific types of application where synchronous + functionality is more important than performance. + + + + + + + + + + + Specifies the name of the queue to get a message from. + + + + + + + This method delivers a message to the client following a get method. A message + delivered by 'get-ok' must be acknowledged unless the no-ack option was set in the + get method. + + + + + + + + + Specifies the name of the exchange that the message was originally published to. + If empty, the message was published to the default exchange. + + + + + Specifies the routing key name specified when the message was published. + + + + + + + + This method tells the client that the queue has no messages available for the + client. + + + + + + + + + + + This method acknowledges one or more messages delivered via the Deliver or Get-Ok + methods. The client can ask to confirm a single message or a set of messages up to + and including a specific message. + + + + + + + + If set to 1, the delivery tag is treated as "up to and including", so that the + client can acknowledge multiple messages with a single method. If set to zero, + the delivery tag refers to a single message. If the multiple field is 1, and the + delivery tag is zero, tells the server to acknowledge all outstanding messages. + + + + The server MUST validate that a non-zero delivery-tag refers to a delivered + message, and raise a channel exception if this is not the case. On a transacted + channel, this check MUST be done immediately and not delayed until a Tx.Commit. + Specifically, a client MUST not acknowledge the same message more than once. + + + TODO. + + + + + + + + + + This method allows a client to reject a message. It can be used to interrupt and + cancel large incoming messages, or return untreatable messages to their original + queue. + + + + + The server SHOULD be capable of accepting and process the Reject method while + sending message content with a Deliver or Get-Ok method. I.e. the server should + read and process incoming methods while sending output frames. To cancel a + partially-send content, the server sends a content body frame of size 1 (i.e. + with no data except the frame-end octet). + + + + + + The server SHOULD interpret this method as meaning that the client is unable to + process the message at this time. + + + TODO. + + + + + + The client MUST NOT use this method as a means of selecting messages to process. + + + TODO. + + + + + + + + + + If requeue is true, the server will attempt to requeue the message. If requeue + is false or the requeue attempt fails the messages are discarded or dead-lettered. + + + + + The server MUST NOT deliver the message to the same client within the + context of the current channel. The recommended strategy is to attempt to + deliver the message to an alternative consumer, and if that is not possible, + to move the message to a dead-letter queue. The server MAY use more + sophisticated tracking to hold the message on the queue and redeliver it to + the same client at a later stage. + + + TODO. + + + + + + + + + + This method asks the server to redeliver all unacknowledged messages on a + specified channel. Zero or more messages may be redelivered. This method + is deprecated in favour of the synchronous Recover/Recover-Ok. + + + + The server MUST set the redelivered flag on all messages that are resent. + + + TODO. + + + + + + If this field is zero, the message will be redelivered to the original + recipient. If this bit is 1, the server will attempt to requeue the message, + potentially then delivering it to an alternative subscriber. + + + + + + + + + This method asks the server to redeliver all unacknowledged messages on a + specified channel. Zero or more messages may be redelivered. This method + replaces the asynchronous Recover. + + + + The server MUST set the redelivered flag on all messages that are resent. + + + TODO. + + + + + + If this field is zero, the message will be redelivered to the original + recipient. If this bit is 1, the server will attempt to requeue the message, + potentially then delivering it to an alternative subscriber. + + + + + + + This method acknowledges a Basic.Recover method. + + + + + + + + + + The Tx class allows publish and ack operations to be batched into atomic + units of work. The intention is that all publish and ack requests issued + within a transaction will complete successfully or none of them will. + Servers SHOULD implement atomic transactions at least where all publish + or ack requests affect a single queue. Transactions that cover multiple + queues may be non-atomic, given that queues can be created and destroyed + asynchronously, and such events do not form part of any transaction. + Further, the behaviour of transactions with respect to the immediate and + mandatory flags on Basic.Publish methods is not defined. + + + + + Applications MUST NOT rely on the atomicity of transactions that + affect more than one queue. + + + + + Applications MUST NOT rely on the behaviour of transactions that + include messages published with the immediate option. + + + + + Applications MUST NOT rely on the behaviour of transactions that + include messages published with the mandatory option. + + + + + tx = C:SELECT S:SELECT-OK + / C:COMMIT S:COMMIT-OK + / C:ROLLBACK S:ROLLBACK-OK + + + + + + + + + + This method sets the channel to use standard transactions. The client must use this + method at least once on a channel before using the Commit or Rollback methods. + + + + + + + + This method confirms to the client that the channel was successfully set to use + standard transactions. + + + + + + + + + This method commits all message publications and acknowledgments performed in + the current transaction. A new transaction starts immediately after a commit. + + + + + + + The client MUST NOT use the Commit method on non-transacted channels. + + + The client opens a channel and then uses Tx.Commit. + + + + + + + This method confirms to the client that the commit succeeded. Note that if a commit + fails, the server raises a channel exception. + + + + + + + + + This method abandons all message publications and acknowledgments performed in + the current transaction. A new transaction starts immediately after a rollback. + Note that unacked messages will not be automatically redelivered by rollback; + if that is required an explicit recover call should be issued. + + + + + + + The client MUST NOT use the Rollback method on non-transacted channels. + + + The client opens a channel and then uses Tx.Rollback. + + + + + + + This method confirms to the client that the rollback succeeded. Note that if an + rollback fails, the server raises a channel exception. + + + + + + diff --git a/config/docker/jasmin/jasmin/store/.gitignore b/config/docker/jasmin/jasmin/store/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/config/docker/jasmin/redis/.gitignore b/config/docker/jasmin/redis/.gitignore new file mode 100644 index 0000000..4ef4b7b --- /dev/null +++ b/config/docker/jasmin/redis/.gitignore @@ -0,0 +1 @@ +*.rdb \ No newline at end of file diff --git a/config/docker/slim/Dockerfile b/config/docker/slim/Dockerfile index 6f7faae..e99cd13 100644 --- a/config/docker/slim/Dockerfile +++ b/config/docker/slim/Dockerfile @@ -12,7 +12,12 @@ RUN apt-get update && apt-get -y upgrade RUN apt-get install --no-install-recommends -y \ python3-dev python3-wheel python3-setuptools virtualenv \ build-essential gcc curl \ - libpq-dev libpq5 telnet + libpq-dev libpq5 telnet \ + # Run python with jemalloc + # More on this: + # - https://zapier.com/engineering/celery-python-jemalloc/ + # - https://paste.pics/581cc286226407ab0be400b94951a7d9 + libjemalloc2 # Pillow dependencies RUN apt-get install --no-install-recommends -y \ @@ -29,6 +34,8 @@ ENV APP_DIR=/app ENV APP_USER=app ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONBUFFERED=1 +# Run python with jemalloc +ENV LD_PRELOAD /usr/lib/x86_64-linux-gnu/libjemalloc.so.2 RUN useradd -m -d ${APP_DIR} -U -r -s /bin/bash ${APP_USER} @@ -51,13 +58,15 @@ RUN pip install -r requirements.txt # copy code to image COPY --chown=$APP_USER . . -COPY --chown=$APP_USER config/docker/docker-entrypoint.sh docker-entrypoint.sh +COPY --chown=$APP_USER config/docker/slim/docker-entrypoint.sh docker-entrypoint.sh + +COPY --chown=$APP_USER config/docker/slim/docker-entrypoint-celery.sh docker-entrypoint-celery.sh RUN mkdir -p public/static && mkdir -p public/media && mkdir -p logs/ EXPOSE 8000 -ENTRYPOINT ["docker-entrypoint.sh"] +ENTRYPOINT ["bash", "docker-entrypoint.sh"] HEALTHCHECK --interval=10s --timeout=10s --retries=30 \ CMD curl -L http://127.0.0.1:8000/api/health_check > /dev/null diff --git a/config/docker/slim/docker-entrypoint.sh b/config/docker/slim/docker-entrypoint.sh index 74a2143..f382cb1 100755 --- a/config/docker/slim/docker-entrypoint.sh +++ b/config/docker/slim/docker-entrypoint.sh @@ -6,7 +6,7 @@ APP_LOG_LEVEL=${APP_LOG_LEVEL:-'warn'} # APP_WSGI: 'wsgi' or 'asgi' APP_WSGI=${APP_WSGI:-'wsgi'} # APP_WORKER_CLASS: 'gevent' or 'uvicorn.workers.UvicornWorker' -APP_WORKER_CLASS=${APP_WORKER_CLASS:-'gevent'} +APP_WORKER_CLASS=${APP_WORKER_CLASS:-'sync'} APP_WORKERS=${APP_WORKERS:-4} # shellcheck disable=SC2164 @@ -20,7 +20,7 @@ python manage.py collectstatic --noinput --clear --no-post-process "$APP_DIR"/env/bin/gunicorn config."$APP_WSGI":application \ --workers "$APP_WORKERS" \ - --bing :"$APP_PORT" \ + --bind :"$APP_PORT" \ --log-level "$APP_LOG_LEVEL" \ --worker-class="$APP_WORKER_CLASS" \ --reload \ No newline at end of file diff --git a/config/docker/sms_logger/Dockerfile b/config/docker/sms_logger/Dockerfile new file mode 100644 index 0000000..b8598bc --- /dev/null +++ b/config/docker/sms_logger/Dockerfile @@ -0,0 +1,50 @@ +FROM python:3.11-slim + +# disable debian interactive +ARG DEBIAN_FRONTEND=noninteractive +# suppress pip upgrade warning +ARG PIP_DISABLE_PIP_VERSION_CHECK=1 +# disable cache directory, image size 2.1GB to 1.9GB +ARG PIP_NO_CACHE_DIR=1 + +RUN apt-get update && apt-get -y upgrade + +RUN apt-get install --no-install-recommends -y \ + python3-dev python3-wheel python3-setuptools virtualenv \ + build-essential gcc curl \ + libpq-dev libpq5 telnet + +RUN apt-get clean autoclean && \ + apt-get autoremove -y && \ + rm -rf /var/lib/{apt,dpkg,cache,log}/ + +# -------------------------------------- +ENV APP_DIR=/app +ENV APP_USER=app +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONBUFFERED=1 + +RUN useradd -m -d ${APP_DIR} -U -r -s /bin/bash ${APP_USER} + +USER ${APP_USER} + +WORKDIR ${APP_DIR} + +# Create the virtual environment +RUN python -m venv /app/env +# Activate the virtual environment +ENV PATH="$APP_DIR/env/bin:$PATH" + +COPY config/docker/sms_logger/requirements.txt requirements.txt + +RUN pip install -U pip wheel + +RUN pip install -r requirements.txt + +COPY --chown=$APP_USER config/docker/sms_logger/*.py . +COPY --chown=$APP_USER config/docker/sms_logger/docker-entrypoint.sh docker-entrypoint.sh + +RUN mkdir -p $APP_DIR/resource + +ENTRYPOINT ["bash", "docker-entrypoint.sh"] + diff --git a/config/docker/sms_logger/docker-entrypoint.sh b/config/docker/sms_logger/docker-entrypoint.sh new file mode 100755 index 0000000..493e488 --- /dev/null +++ b/config/docker/sms_logger/docker-entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +APP_DIR=${APP_DIR:-'/app'} +APP_LOG_LEVEL=${APP_LOG_LEVEL:-'warn'} + +# shellcheck disable=SC2164 +cd "$APP_DIR" + +source "$APP_DIR"/env/bin/activate + +"$APP_DIR"/env/bin/python sms_logger.py \ No newline at end of file diff --git a/config/docker/sms_logger/requirements.txt b/config/docker/sms_logger/requirements.txt new file mode 100644 index 0000000..9eed332 --- /dev/null +++ b/config/docker/sms_logger/requirements.txt @@ -0,0 +1,9 @@ +Twisted~=22.1.0 +txAMQP3~=0.9.3 +smpp.pdu3~=0.6 +smpp.twisted3~=0.7 +service_identity~=18.1.0 +python-dotenv +jasmin +psycopg2 +mysql-connector-python diff --git a/config/docker/sms_logger/sms_logger.py b/config/docker/sms_logger/sms_logger.py new file mode 100644 index 0000000..356f1a7 --- /dev/null +++ b/config/docker/sms_logger/sms_logger.py @@ -0,0 +1,434 @@ +#!/usr/bin/env python +"""This script will log all sent sms through Jasmin with user information. + +Requirement: +- Activate publish_submit_sm_resp in jasmin.cfg +- Install psycopg2: # Used for PostgreSQL connection + + pip install psycopg2 +- Install mysql.connector: # Used for MySQL connection + + pip install mysql-connector-python + +Optional: +- SET ENVIRONMENT ENV: + + DB_TYPE_MYSQL # Default: 1 # 1 for MySQL, 0 for PostgreSQL + + DB_HOST # Default: 127.0.0.1 # IP or Docker container name + + DB_DATABASE # Default: jasmin # should Exist + + DB_TABLE # Default: submit_log # the script will create it if it doesn't Exist + + DB_USER # Default: jasmin # for the Database connection. + + DB_PASS # Default: jadmin # for the Database connection + + AMQP_BROKER_HOST # Default: 127.0.0.1 # RabbitMQ host used by Jasmin SMS Gateway. IP or Docker container name + + AMQP_BROKER_PORT # Default: 5672 # RabbitMQ port used by Jasmin SMS Gateway. IP or Docker container name + +Database Scheme: +- MySQL table: + CREATE TABLE ${DB_TABLE} ( + `msgid` VARCHAR(45) PRIMARY KEY, + `source_connector` VARCHAR(15), + `routed_cid` VARCHAR(30), + `source_addr` VARCHAR(40), + `destination_addr` VARCHAR(40) NOT NULL CHECK (`destination_addr` <> ''), + `rate` DECIMAL(12, 7), + `charge` DECIMAL(12, 7), + `pdu_count` TINYINT(3) DEFAULT 1, + `short_message` BLOB, + `binary_message` BLOB, + `status` VARCHAR(15) NOT NULL CHECK (`status` <> ''), + `uid` VARCHAR(15) NOT NULL CHECK (`uid` <> ''), + `trials` TINYINT(4) DEFAULT 1, + `created_at` DATETIME NOT NULL, + `status_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX (`source_connector`), + INDEX (`routed_cid`), + INDEX (`source_addr`), + INDEX (`destination_addr`), + INDEX (`status`), + INDEX (`uid`), + INDEX (`created_at`), + INDEX (`created_at`, `uid`), + INDEX (`created_at`, `uid`, `status`), + INDEX (`created_at`, `routed_cid`), + INDEX (`created_at`, `routed_cid`, `status`), + INDEX (`created_at`, `source_connector`), + INDEX (`created_at`, `source_connector`, `status`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +- PostgreSQL table: + CREATE TABLE IF NOT EXISTS ${DB_TABLE} ( + msgid VARCHAR(45) NOT NULL PRIMARY KEY, + source_connector VARCHAR(15) NULL DEFAULT NULL, + routed_cid VARCHAR(30) NULL DEFAULT NULL, + source_addr VARCHAR(40) NULL DEFAULT NULL, + destination_addr VARCHAR(40) NOT NULL CHECK (destination_addr <> ''), + rate DECIMAL(12,7) NULL DEFAULT NULL, + charge DECIMAL(12,7) NULL DEFAULT NULL, + pdu_count SMALLINT NULL DEFAULT '1', + short_message BYTEA NULL DEFAULT NULL, + binary_message BYTEA NULL DEFAULT NULL, + status VARCHAR(15) NOT NULL CHECK (status <> ''), + uid VARCHAR(15) NOT NULL CHECK (uid <> ''), + trials SMALLINT NULL DEFAULT '1', + created_at TIMESTAMP(0) NOT NULL, + status_at TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + CREATE INDEX ON ${DB_TABLE} (source_connector); + CREATE INDEX ON ${DB_TABLE} (routed_cid); + CREATE INDEX ON ${DB_TABLE} (source_addr); + CREATE INDEX ON ${DB_TABLE} (destination_addr); + CREATE INDEX ON ${DB_TABLE} (status); + CREATE INDEX ON ${DB_TABLE} (uid); + CREATE INDEX ON ${DB_TABLE} (created_at); + CREATE INDEX ON ${DB_TABLE} (created_at, uid); + CREATE INDEX ON ${DB_TABLE} (created_at, uid, status); + CREATE INDEX ON ${DB_TABLE} (created_at, routed_cid); + CREATE INDEX ON ${DB_TABLE} (created_at, routed_cid, status); + CREATE INDEX ON ${DB_TABLE} (created_at, source_connector); + CREATE INDEX ON ${DB_TABLE} (created_at, source_connector, status); +""" + +import os +from time import sleep +import pickle as pickle +import binascii +from datetime import datetime +from twisted.internet.defer import inlineCallbacks +from twisted.internet import reactor +from twisted.internet.protocol import ClientCreator +from twisted.python import log +from txamqp.protocol import AMQClient +from txamqp.client import TwistedDelegate +import txamqp.spec + +from smpp.pdu.pdu_types import DataCoding + +from mysql.connector import connect as _mysql_connect +from psycopg2 import pool as _postgres_pool +from psycopg2 import Error as _postgres_error + +q = {} + +# Database connection parameters +db_type_mysql = int(os.getenv('DB_TYPE_MYSQL', '1')) == 1 +db_host = os.getenv('DB_HOST', '127.0.0.1') +db_database = os.getenv('DB_DATABASE', 'jasmin') +db_table = os.getenv('DB_TABLE', 'submit_log') +db_user = os.getenv('DB_USER', 'jasmin') +db_pass = os.getenv('DB_PASS', 'jadmin') +# AMQB broker connection parameters +amqp_broker_host = os.getenv('AMQP_BROKER_HOST', '127.0.0.1') +amqp_broker_port = int(os.getenv('AMQP_BROKER_PORT', '5672')) + + +def get_psql_conn(): + psql_pool = _postgres_pool.SimpleConnectionPool( + 1, + 20, + user=db_user, + password=db_pass, + host=db_host, + database=db_database) + return psql_pool.getconn() + + +def get_mysql_conn(): + return _mysql_connect( + user=db_user, + password=db_pass, + host=db_host, + database=db_database, + pool_name="mypool", + pool_size=20) + + +@inlineCallbacks +def gotConnection(conn, username, password): + print("*** Connected to broker, authenticating: %s" % username, flush=True) + yield conn.start({"LOGIN": username, "PASSWORD": password}) + + print("*** Authenticated. Ready to receive messages", flush=True) + chan = yield conn.channel(1) + yield chan.channel_open() + + yield chan.queue_declare(queue="sms_logger_queue") + + # Bind to submit.sm.* and submit.sm.resp.* routes to track sent messages + yield chan.queue_bind(queue="sms_logger_queue", exchange="messaging", routing_key='submit.sm.*') + yield chan.queue_bind(queue="sms_logger_queue", exchange="messaging", routing_key='submit.sm.resp.*') + # Bind to dlr_thrower.* to track DLRs + yield chan.queue_bind(queue="sms_logger_queue", exchange="messaging", routing_key='dlr_thrower.*') + + yield chan.basic_consume(queue='sms_logger_queue', no_ack=False, consumer_tag="sms_logger") + queue = yield conn.queue("sms_logger") + + if db_type_mysql: + db_conn = get_mysql_conn() + if db_conn: + print("*** Pooling 20 connections", flush=True) + print("*** Connected to MySQL", flush=True) + else: + db_conn = get_psql_conn() + if db_conn: + print("*** Pooling 20 connections", flush=True) + print("*** Connected to psql", flush=True) + + cursor = db_conn.cursor() + + if db_type_mysql: + create_table = ("""CREATE TABLE IF NOT EXISTS {} ( + `msgid` VARCHAR(45) PRIMARY KEY, + `source_connector` VARCHAR(15), + `routed_cid` VARCHAR(30), + `source_addr` VARCHAR(40), + `destination_addr` VARCHAR(40) NOT NULL CHECK (`destination_addr` <> ''), + `rate` DECIMAL(12, 7), + `charge` DECIMAL(12, 7), + `pdu_count` TINYINT(3) DEFAULT 1, + `short_message` BLOB, + `binary_message` BLOB, + `status` VARCHAR(15) NOT NULL CHECK (`status` <> ''), + `uid` VARCHAR(15) NOT NULL CHECK (`uid` <> ''), + `trials` TINYINT(4) DEFAULT 1, + `created_at` DATETIME NOT NULL, + `status_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX (`source_connector`), + INDEX (`routed_cid`), + INDEX (`source_addr`), + INDEX (`destination_addr`), + INDEX (`status`), + INDEX (`uid`), + INDEX (`created_at`), + INDEX (`created_at`, `uid`), + INDEX (`created_at`, `uid`, `status`), + INDEX (`created_at`, `routed_cid`), + INDEX (`created_at`, `routed_cid`, `status`), + INDEX (`created_at`, `source_connector`), + INDEX (`created_at`, `source_connector`, `status`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;""".format(db_table)) + else: + create_table = ("""CREATE TABLE IF NOT EXISTS {} ( + msgid VARCHAR(45) NOT NULL PRIMARY KEY, + source_connector VARCHAR(15) NULL DEFAULT NULL, + routed_cid VARCHAR(30) NULL DEFAULT NULL, + source_addr VARCHAR(40) NULL DEFAULT NULL, + destination_addr VARCHAR(40) NOT NULL CHECK (destination_addr <> ''), + rate DECIMAL(12,7) NULL DEFAULT NULL, + charge DECIMAL(12,7) NULL DEFAULT NULL, + pdu_count SMALLINT NULL DEFAULT '1', + short_message BYTEA NULL DEFAULT NULL, + binary_message BYTEA NULL DEFAULT NULL, + status VARCHAR(15) NOT NULL CHECK (status <> ''), + uid VARCHAR(15) NOT NULL CHECK (uid <> ''), + trials SMALLINT NULL DEFAULT '1', + created_at TIMESTAMP(0) NOT NULL, + status_at TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + CREATE INDEX ON {} (source_connector); + CREATE INDEX ON {} (routed_cid); + CREATE INDEX ON {} (source_addr); + CREATE INDEX ON {} (destination_addr); + CREATE INDEX ON {} (status); + CREATE INDEX ON {} (uid); + CREATE INDEX ON {} (created_at); + CREATE INDEX ON {} (created_at, uid); + CREATE INDEX ON {} (created_at, uid, status); + CREATE INDEX ON {} (created_at, routed_cid); + CREATE INDEX ON {} (created_at, routed_cid, status); + CREATE INDEX ON {} (created_at, source_connector); + CREATE INDEX ON {} (created_at, source_connector, status); + """.format(db_table, db_table, db_table, + db_table, db_table, db_table, + db_table, db_table, db_table, + db_table, db_table, db_table, + db_table, db_table, )) + + cursor.execute(create_table) + if cursor.rowcount > 0: + print('*** {} table was created successfully'.format(db_table), flush=True) + else: + print('*** {} table already exist'.format(db_table), flush=True) + + db_conn.commit() + + # Wait for messages + # This can be done through a callback ... + while True: + msg = yield queue.get() + props = msg.content.properties + + if db_type_mysql: + db_conn.ping(reconnect=True, attempts=10, delay=1) + else: + check_connection = True + while check_connection: + try: + cursor = db_conn.cursor() + cursor.execute('SELECT 1') + check_connection = False + except _postgres_error: + print('*** PostgreSQL connection exception. Trying to reconnect', flush=True) + db_conn = get_psql_conn() + if db_conn: + print("*** Pooling 20 connections", flush=True) + print("*** Re-connected to psql", flush=True) + cursor = db_conn.cursor() + pass + + if msg.routing_key[:10] == 'submit.sm.' and msg.routing_key[:15] != 'submit.sm.resp.': + pdu = pickle.loads(msg.content.body) + pdu_count = 1 + short_message = pdu.params['short_message'] + billing = props['headers'] + billing_pickle = billing.get('submit_sm_resp_bill') + if not billing_pickle: + billing_pickle = billing.get('submit_sm_bill') + if billing_pickle is not None: + submit_sm_bill = pickle.loads(billing_pickle) + else: + submit_sm_bill = None + source_connector = props['headers']['source_connector'] + routed_cid = msg.routing_key[10:] + + # Is it a multipart message ? + while hasattr(pdu, 'nextPdu'): + # Remove UDH from first part + if pdu_count == 1: + short_message = short_message[6:] + + pdu = pdu.nextPdu + + # Update values: + pdu_count += 1 + short_message += pdu.params['short_message'][6:] + + # Save short_message bytes + binary_message = binascii.hexlify(short_message) + + # If it's a binary message, assume it's utf_16_be encoded + if pdu.params['data_coding'] is not None: + dc = pdu.params['data_coding'] + if (isinstance(dc, int) and dc == 8) or (isinstance(dc, DataCoding) and str(dc.schemeData) == 'UCS2'): + short_message = short_message.decode('utf_16_be', 'ignore').encode('utf_8') + + q[props['message-id']] = { + 'source_connector': source_connector, + 'routed_cid': routed_cid, + 'rate': 0, + 'charge': 0, + 'uid': 0, + 'destination_addr': pdu.params['destination_addr'], + 'source_addr': pdu.params['source_addr'], + 'pdu_count': pdu_count, + 'short_message': short_message, + 'binary_message': binary_message, + } + if submit_sm_bill is not None: + q[props['message-id']]['rate'] = submit_sm_bill.getTotalAmounts() + q[props['message-id']]['charge'] = submit_sm_bill.getTotalAmounts() * pdu_count + q[props['message-id']]['uid'] = submit_sm_bill.user.uid + elif msg.routing_key[:15] == 'submit.sm.resp.': + # It's a submit_sm_resp + + pdu = pickle.loads(msg.content.body) + if props['message-id'] not in q: + print('*** Got resp of an unknown submit_sm: %s' % props['message-id'], flush=True) + chan.basic_ack(delivery_tag=msg.delivery_tag) + continue + + qmsg = q[props['message-id']] + + if qmsg['source_addr'] is None: + qmsg['source_addr'] = '' + + insert_log = ("""INSERT INTO {} (msgid, source_addr, rate, pdu_count, charge, + destination_addr, short_message, + status, uid, created_at, binary_message, + routed_cid, source_connector, status_at) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + ON DUPLICATE KEY UPDATE trials = trials + 1;""".format(db_table)) + + cursor.execute(insert_log, ( + props['message-id'], + qmsg['source_addr'], + qmsg['rate'], + qmsg['pdu_count'], + qmsg['charge'], + qmsg['destination_addr'], + qmsg['short_message'], + pdu.status, + qmsg['uid'], + props['headers']['created_at'], + qmsg['binary_message'], + qmsg['routed_cid'], + qmsg['source_connector'], + props['headers']['created_at'],)) + db_conn.commit() + elif msg.routing_key[:12] == 'dlr_thrower.': + if props['headers']['message_status'][:5] == 'ESME_': + # Ignore dlr from submit_sm_resp + chan.basic_ack(delivery_tag=msg.delivery_tag) + continue + + # It's a dlr + if props['message-id'] not in q: + print('*** Got dlr of an unknown submit_sm: %s' % props['message-id'], flush=True) + chan.basic_ack(delivery_tag=msg.delivery_tag) + continue + + # Update message status + qmsg = q[props['message-id']] + update_log = ("UPDATE submit_log SET status = %s, status_at = %s WHERE msgid = %s;".format(db_table)) + cursor.execute(update_log, ( + props['headers']['message_status'], + datetime.now(), + props['message-id'],)) + db_conn.commit() + else: + print('*** unknown route: %s' % msg.routing_key, flush=True) + + chan.basic_ack(delivery_tag=msg.delivery_tag) + + # A clean way to tear down and stop + yield chan.basic_cancel("sms_logger") + yield chan.channel_close() + chan0 = yield conn.channel(0) + yield chan0.connection_close() + + reactor.stop() + + +if __name__ == "__main__": + sleep(2) + print(' ', flush=True) + print(' ', flush=True) + print('***************** sms_logger *****************', flush=True) + if db_type_mysql == 1: + print('*** Staring sms_logger, DB drive: MySQL', flush=True) + else: + print('*** Staring sms_logger, DB drive: PostgreSQL', flush=True) + print('**********************************************', flush=True) + + host = amqp_broker_host + port = amqp_broker_port + vhost = '/' + username = 'guest' + password = 'guest' + spec_file = os.environ.get("AMQP_SPEC_FILE", '/etc/jasmin/resource/amqp0-9-1.xml') + + spec = txamqp.spec.load(spec_file) + + # Connect and authenticate + d = ClientCreator(reactor, + AMQClient, + delegate=TwistedDelegate(), + vhost=vhost, + spec=spec).connectTCP(host, port) + d.addCallback(gotConnection, username, password) + + + def whoops(err): + if reactor.running: + log.err(err) + reactor.stop() + + + d.addErrback(whoops) + + reactor.run() diff --git a/config/settings/com.py b/config/settings/com.py index 0781d34..a875509 100644 --- a/config/settings/com.py +++ b/config/settings/com.py @@ -14,7 +14,7 @@ SECRET_KEY = os.environ.get("SECRET_KEY", default='8na#(#x@0i*3ah%&$-q)b&wqu5ct_a3))d8-sqk-ux*5lol*wl') -DEBUG = bool(os.environ.get("DEBUG", '0')) +DEBUG = bool(os.environ.get("DEBUG", '')) SITE_ID = int(os.environ.get("SITE_ID", default='1')) diff --git a/docker-compose.yml b/docker-compose.yml index b3f96f5..250524f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,30 +9,32 @@ services: replicas: 1 update_config: order: start-first - entrypoint: /app/config/docker/docker-entrypoint.sh + # entrypoint: bash ./docker-entrypoint.sh env_file: - .env environment: - DEBUG: 1 + DEBUG: '1' DJANGO_SETTINGS_MODULE: config.settings.pro - ALLOWED_HOSTS: '*' - POSTGRE_URL: postgres://postgres:123@127.0.0.1:5432/jasmin_web_db + ALLOWED_HOSTS: '127.0.0.1,127.0.0.11' + PRODB_URL: postgres://jasmin:jasmin@172.17.0.1:5432/jasmin REDIS_URI: redis://jasmin_redis:6379/1 + TELNET_HOST: jasmin + SUBMIT_LOG: 1 volumes: - - ./public:/app/public - - ./logs:/app/logs + - web_public:/app/public + - web_logs:/app/logs depends_on: - jasmin_redis jasmin_celery: image: tarekaec/jasmin_web_panel:1.2 - entrypoint: /app/config/docker/docker-entrypoint-celery.sh + entrypoint: bash ./docker-entrypoint-celery.sh deploy: replicas: 1 env_file: - .env environment: DEBUG: 0 - POSTGRE_URL: postgres://postgres:123@127.0.0.1:5432/jasmin_web_db + PRODB_URL: postgres://jasmin:jasmin@172.17.0.1:5432/jasmin DJANGO_SETTINGS_MODULE: config.settings.pro CELERY_BROKER_URL: redis://jasmin_redis:6379/0 CELERY_RESULT_BACKEND: redis://jasmin_redis:6379/0 @@ -58,3 +60,7 @@ services: volumes: redis_data: driver: local + web_public: + driver: local + web_logs: + driver: local diff --git a/main/core/utils/common.py b/main/core/utils/common.py index bbc6d72..a2a2e8c 100644 --- a/main/core/utils/common.py +++ b/main/core/utils/common.py @@ -126,7 +126,7 @@ def is_online(host: str, port: int) -> Tuple[bool, str]: try: skt.settimeout(1.0) skt.connect((host, port)) - return True, "OK" + return True, "OK, Jasmin Connected" except Exception as e: msg = str(e) return False, msg diff --git a/main/web/static/web/global.js b/main/web/static/web/global.js index 5482275..6525241 100644 --- a/main/web/static/web/global.js +++ b/main/web/static/web/global.js @@ -1,6 +1,7 @@ (function($){ $(document).on("input", ".float-input", function(){this.value = this.value.replace(/[^0-9.]/g, ''); this.value = this.value.replace(/(\..*)\./g, '$1');}); $(document).on("input", ".integer-input", function(){$(this).val($(this).val().replace(/[^0-9]/g, ''));}); + toastr.options.positionClass = 'toast-bottom-right'; window.toTitleCase = function(str){ return str.replace(/\w\S*/g, function(txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();