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/.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 302cd43..0000000 --- a/Dockerfile +++ /dev/null @@ -1,46 +0,0 @@ -FROM python:3.8-alpine - -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 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 - - - -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 - -COPY ./docker-entrypoint.sh docker-entrypoint.sh - -#RUN chmod -R u+x ${JASMIN_HOME} && \ -# chgrp -R 0 ${JASMIN_HOME} && \ -# chmod -R g=u ${JASMIN_HOME} - -RUN chown -R jasmin:jasmin $JASMIN_HOME/ - -USER jasmin - -ENTRYPOINT ["docker-entrypoint.sh"] \ No newline at end of file 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 57886dd..0f08c6f 100644 --- a/Sample.env +++ b/Sample.env @@ -8,19 +8,19 @@ 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://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 # REDIS_DB=0 -ACTIVE_APP=web ALLOWED_HOSTS=* +CSRF_TRUSTED_ORIGINS= TELNET_HOST=127.0.0.1 TELNET_PORT=8990 diff --git a/celery_run.sh b/celery_run.sh deleted file mode 100755 index 498b73f..0000000 --- a/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/config/celery.py b/config/celery.py new file mode 100644 index 0000000..58d7946 --- /dev/null +++ b/config/celery.py @@ -0,0 +1,36 @@ +import os +from django.conf import settings +from celery import Celery +from celery.utils.log import get_task_logger + +from django.utils import timezone + +logger = get_task_logger(__name__) + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.dev") + +app = Celery('config') + +app.config_from_object("django.conf:settings", namespace="CELERY") +app.conf.broker_url = settings.CELERY_BROKER_URL +app.conf.result_backend = settings.CELERY_RESULT_BACKEND +app.conf.broker_connection_max_retries = 0 +app.conf.broker_connection_retry_on_startup = True +app.autodiscover_tasks(lambda: settings.INSTALLED_APPS, related_name='tasks') + +BROKER_CONNECTION_TIMEOUT = 120 + +CELERY_DEFAULT_UP_TIME = timezone.now() + + +@app.task(bind=True) +def debug_task(self): + print("Request: {0!r}".format(self.Request)) + + +def revoke_task(task_id): + app.control.revoke(task_id) + + +def clear_tasks(): + return app.control.purge() diff --git a/config/docker/alpine/Dockerfile b/config/docker/alpine/Dockerfile new file mode 100644 index 0000000..b61871c --- /dev/null +++ b/config/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/config/docker/alpine/docker-compose-alpine.yml b/config/docker/alpine/docker-compose-alpine.yml new file mode 100644 index 0000000..d30b958 --- /dev/null +++ b/config/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/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 new file mode 100644 index 0000000..e99cd13 --- /dev/null +++ b/config/docker/slim/Dockerfile @@ -0,0 +1,73 @@ +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 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 \ + 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 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} + +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/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 ["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-celery.sh b/config/docker/slim/docker-entrypoint-celery.sh new file mode 100755 index 0000000..2ae9259 --- /dev/null +++ b/config/docker/slim/docker-entrypoint-celery.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +APP_DIR=${APP_DIR:-'/app'} +CELERY_LOG_LEVEL=${CELERY_LOG_LEVEL:-'warning'} + +# shellcheck disable=SC2164 +cd "$APP_DIR" + +source "$APP_DIR"/env/bin/activate + +"$APP_DIR"/env/bin/celery --app config worker --max-tasks-per-child 1 -l "$CELERY_LOG_LEVEL" diff --git a/config/docker/slim/docker-entrypoint.sh b/config/docker/slim/docker-entrypoint.sh new file mode 100755 index 0000000..f382cb1 --- /dev/null +++ b/config/docker/slim/docker-entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +APP_PORT=${APP_PORT:-8000} +APP_DIR=${APP_DIR:-'/app'} +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:-'sync'} +APP_WORKERS=${APP_WORKERS:-4} + +# shellcheck disable=SC2164 +cd "$APP_DIR" + +source "$APP_DIR"/env/bin/activate + +python manage.py migrate +python manage.py load_new +python manage.py collectstatic --noinput --clear --no-post-process + +"$APP_DIR"/env/bin/gunicorn config."$APP_WSGI":application \ + --workers "$APP_WORKERS" \ + --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/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 49a7e34..a875509 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", '')) -SITE_ID = int(env("SITE_ID", default='1')) +SITE_ID = int(os.environ.get("SITE_ID", default='1')) INSTALLED_APPS = [ 'jet.dashboard', @@ -27,14 +28,14 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.humanize', + "django.contrib.sites", # 'channels', - 'crequest', + 'crequest', # noqa 'rest_framework', 'main.api', 'main.core', - 'main.taskapp', 'main.users', 'main.web', ] @@ -48,7 +49,9 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'crequest.middleware.CrequestMiddleware', + "django.contrib.sites.middleware.CurrentSiteMiddleware", + 'crequest.middleware.CrequestMiddleware', # noqa + 'main.core.middleware.AjaxMiddleware', 'main.core.middleware.TelnetConnectionMiddleware', 'main.core.middleware.UserAgentMiddleware', 'main.users.middleware.LastUserActivityMiddleware', @@ -57,7 +60,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': [ @@ -76,12 +79,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' @@ -94,30 +99,30 @@ 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_L10N = True - 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" -from django.contrib.messages import constants as message_constants +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', @@ -141,9 +146,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 +156,6 @@ LAST_ACTIVITY_INTERVAL_SECS = 3600 # REST API Settings - REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.BasicAuthentication', @@ -177,17 +181,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 +211,10 @@ - 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' + +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") diff --git a/config/settings/pro.py b/config/settings/pro.py index 22bb22c..1262c8f 100644 --- a/config/settings/pro.py +++ b/config/settings/pro.py @@ -6,8 +6,11 @@ } 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, @@ -19,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 b1d8416..0000000 --- a/deployment/docker/slim-buster/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -FROM python:3.8-slim-buster - -RUN apt update && apt install telnet - -RUN apt 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/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.yml b/docker-compose.yml index ae8faad..250524f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,35 +2,65 @@ 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.2 ports: - "8000:8000" deploy: replicas: 1 + update_config: + order: start-first + # entrypoint: bash ./docker-entrypoint.sh env_file: - .env environment: - JASMIN_PORT: 8000 - healthcheck: - disable: true + DEBUG: '1' + DJANGO_SETTINGS_MODULE: config.settings.pro + 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:/web/public - entrypoint: /jasmin/docker-entrypoint.sh + - web_public:/app/public + - web_logs:/app/logs + depends_on: + - jasmin_redis jasmin_celery: - image: tarekaec/jasmin_web_panel:1.0 - # image: tarekaec/jasmin_web_panel:1.0-alpine + image: tarekaec/jasmin_web_panel:1.2 + entrypoint: bash ./docker-entrypoint-celery.sh deploy: replicas: 1 env_file: - .env environment: DEBUG: 0 + 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 + CELERY_LOG_LEVEL: info healthcheck: disable: true depends_on: - jasmin_redis - entrypoint: /jasmin/celery_run.sh 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 + web_public: + driver: local + web_logs: + driver: local diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh deleted file mode 100755 index 7573a7a..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 - -/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/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/__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/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/__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/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/mailer/mail_modules.py b/main/core/mailer/mail_modules.py index 30fcf7c..c47d9da 100644 --- a/main/core/mailer/mail_modules.py +++ b/main/core/mailer/mail_modules.py @@ -1,100 +1,106 @@ -# -*- encoding: utf-8 -*- -from __future__ import unicode_literals, absolute_import +""" +Python 2 +from email.MIMEMultipart import MIMEMultipart +from email.MIMEText import MIMEText +""" +from typing import List + from django.template.loader import render_to_string -from django.conf import settings as django_settings -from django.utils.translation import gettext_lazy as _ from main.core.models import EmailServer -import smtplib, re, os, imghdr +import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText -from email.mime.image import MIMEImage -from email.mime.application import MIMEApplication -''' -Python 2 -from email.MIMEMultipart import MIMEMultipart -from email.MIMEText import MIMEText -''' + class PyMail(object): - def __init__(self, subject, from_mail, maillist, message): - self.subject = str(subject) - self.from_mail = from_mail - self.maillist = maillist - self.message = str(message) - self.emailserver = EmailServer.objects.filter(active=True).first() - def send(self): - mailserver = None - if self.emailserver: - if not self.from_mail: - self.from_mail = self.emailserver.username - if self.emailserver.ssl: - # identify ourselves to smtp gmail client - mailserver = smtplib.SMTP_SSL(self.emailserver.server, self.emailserver.port) - # re-identify ourselves as an encrypted connection - mailserver.ehlo() - else: - mailserver = smtplib.SMTP(self.emailserver.server, self.emailserver.port) - mailserver.starttls() - #mailserver.login(django_settings.EMAIL_HOST_USER, django_settings.EMAIL_HOST_PASSWORD) - mailserver.login(self.emailserver.username, self.emailserver.password) - for mail in self.maillist: - message = """\ + def __init__(self, subject, from_mail, mails, message): + self.subject = str(subject) + self.from_mail = from_mail + self.mails = mails + self.message = str(message) + self.email_server = EmailServer.objects.get(active=True) + + def send(self): + mailserver = None + if self.email_server: + if not self.from_mail: + self.from_mail = self.email_server.username + if self.email_server.ssl: + # identify ourselves to smtp gmail client + mailserver = smtplib.SMTP_SSL(self.email_server.server, self.email_server.port) + # re-identify ourselves as an encrypted connection + mailserver.ehlo() + else: + mailserver = smtplib.SMTP(self.email_server.server, self.email_server.port) + mailserver.starttls() + # mailserver.login(django_settings.EMAIL_HOST_USER, django_settings.EMAIL_HOST_PASSWORD) + mailserver.login(self.email_server.username, self.email_server.password) + for mail in self.maillist: + message = """\ From: %s To: %s Subject: %s %s """ % (self.from_mail, mail, self.subject, self.message) - # print(self.from_mail) - # print(message) - mailserver.sendmail(self.from_mail, mail, message.encode("utf8")) - mailserver.close() + # print(self.from_mail) + # print(message) + mailserver.sendmail(self.from_mail, mail, message.encode("utf8")) + mailserver.close() + class PyMailMultiPart(object): - def __init__(self, subject, html_template="core/email/email_sample.html"): - self.subject = str(subject) - self.emailserver = EmailServer.objects.filter(active=True).first() - self.from_mail = self.emailserver.username - self.html_template = html_template - def named(self, mail, name=""): - return "{0} <{1}>".format(name, mail) if name else mail - def send(self, maillist=[], kwargs={}): - mailobject = None - if self.emailserver: - msg = MIMEMultipart('related', type="text/html") - msg["Subject"] = self.subject - msg["From"] = self.from_mail - htmlpart = MIMEText(render_to_string(self.html_template, kwargs), 'html') - msg.attach(htmlpart) - if self.emailserver.ssl: - mailobject = smtplib.SMTP_SSL(self.emailserver.server, self.emailserver.port) - mailobject.ehlo() - else: - mailobject = smtplib.SMTP(self.emailserver.server, self.emailserver.port) - mailobject.starttls() - mailobject.login(self.emailserver.username, self.emailserver.password) - for to_mail in maillist: - msg["To"] = to_mail - mailobject.sendmail(self.from_mail, to_mail, msg.as_string()) - mailobject.quit() - def send_envelopes(self, envelopes=[]): - mailobject = None - if self.emailserver: - if self.emailserver.ssl: - mailobject = smtplib.SMTP_SSL(self.emailserver.server, self.emailserver.port) - mailobject.ehlo() - else: - mailobject = smtplib.SMTP(self.emailserver.server, self.emailserver.port) - mailobject.starttls() - mailobject.login(self.emailserver.username, self.emailserver.password) - for enve in envelopes: - msg = MIMEMultipart('related', type='text/html') - msg["Subject"] = self.subject - msg["From"] = self.from_mail - htmlpart = MIMEText(render_to_string(self.html_template, enve.get("kwargs")), 'html') - msg.attach(htmlpart) - msg["To"] = self.named(mail=enve.get("email"), name=enve.get("name")) - mailobject.sendmail(self.from_mail, enve.get("email"), msg.as_string()) - mailobject.quit() \ No newline at end of file + def __init__(self, subject, html_template="core/email/email_sample.html"): + self.subject = str(subject) + self.email_server = EmailServer.objects.get(active=True) + self.from_mail = self.email_server.username + self.html_template = html_template + + def named(self, mail, name=""): + return "{0} <{1}>".format(name, mail) if name else mail + + def send(self, mails: list, kwargs=None): + if kwargs is None: + kwargs = {} + if self.email_server: + msg = MIMEMultipart('related', type="text/html") + msg["Subject"] = self.subject + msg["From"] = self.from_mail + 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( + host=self.email_server.server, port=self.email_server.port, timeout=120.0, + ) + mail_obj.ehlo() + else: + 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: + msg["To"] = to_mail + mail_obj.sendmail(self.from_mail, to_mail, msg.as_string()) + mail_obj.quit() + + def send_envelopes(self, envelopes: dict): + if self.email_server: + if self.email_server.ssl: + mail_obj = smtplib.SMTP_SSL(self.email_server.server, self.email_server.port) + mail_obj.ehlo() + else: + mail_obj = smtplib.SMTP(self.email_server.server, self.email_server.port) + mail_obj.starttls() + mail_obj.login(self.email_server.username, self.email_server.password) + for envelope in envelopes: + msg = MIMEMultipart('related', type='text/html') + msg["Subject"] = self.subject + msg["From"] = self.from_mail + html_part = MIMEText(render_to_string(self.html_template, envelope.get("kwargs")), 'html') + msg.attach(html_part) + msg["To"] = self.named(mail=envelope.get("email"), name=envelope.get("name")) + mail_obj.sendmail(self.from_mail, envelope.get("email"), msg.as_string()) + mail_obj.quit() diff --git a/main/core/middleware.py b/main/core/middleware.py index 564372a..2694ab3 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.headers.get('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/core/models/timestamped.py b/main/core/models/timestamped.py index d4d3c28..58a12f4 100644 --- a/main/core/models/timestamped.py +++ b/main/core/models/timestamped.py @@ -15,13 +15,13 @@ class TimeStampedModel(models.Model): class Meta: abstract = True def get_dict(self): - from main.core.utils import readabledateformat + from main.core.utils import readable_date_format return { "created": timezone.localtime(self.created).isoformat() if self.created else None, - "createdf": readabledateformat(timezone.localtime(self.created)) if self.created else None, + "createdf": readable_date_format(timezone.localtime(self.created)) if self.created else None, "createds": timesince(self.created) if self.created else None, "modified": timezone.localtime( self.modified).isoformat() if self.modified else None, - "modifiedf": readabledateformat(timezone.localtime(self.modified)) if self.modified else None, + "modifiedf": readable_date_format(timezone.localtime(self.modified)) if self.modified else None, "modifieds": timesince(self.modified) if self.modified else None, "isedited": self.isedited, } 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..6d2100b --- /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.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" + 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)) # 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/tasks/__init__.py b/main/core/tasks/__init__.py index 02aeb66..42cdc87 100644 --- a/main/core/tasks/__init__.py +++ b/main/core/tasks/__init__.py @@ -1 +1 @@ -from .mail_html import mail_html_maillist, mail_html_envelopes \ No newline at end of file +from .mail_html import mail_html_mails, mail_html_envelopes diff --git a/main/core/tasks/mail_html.py b/main/core/tasks/mail_html.py index acfd836..308bdbf 100644 --- a/main/core/tasks/mail_html.py +++ b/main/core/tasks/mail_html.py @@ -1,35 +1,35 @@ -# -*- encoding: utf-8 -*- -from __future__ import absolute_import, unicode_literals -from django.utils.translation import gettext_lazy as _ +from typing import List from django.utils import translation -from django.template.loader import render_to_string +# from django.template.loader import render_to_string from django.conf import settings from main.core.mailer import PyMail, PyMailMultiPart -from main.taskapp.celery import app +from config.celery import app -""" -kwargs = { - user : {"username": "joe_user", "email": "joe@gmail.com", "name": "Joe Life"}, - site_name : "My Company", - logo_url : "http://www.g.com/logo.png", - site_url : "http://www.g.com", - logo : '{}'.format(site_url, site_url, logo_url) -} -""" - -@app.task -def mail_html_maillist(maillist, subject, html_template, kwargs={}, lang=settings.LANGUAGE_CODE,): +@app.task(bind=True) +def mail_html_mails(self, mails: List[str], subject: str, html_template: str, kwargs=None, + lang: str = settings.LANGUAGE_CODE): + if kwargs is None: + kwargs = {} translation.activate(lang) - mail = PyMailMultiPart(subject=subject, html_template=html_template,) - mail.send(maillist=maillist, kwargs=kwargs) + mail = PyMailMultiPart(subject=subject, html_template=html_template, ) + mail.send(mails=mails, kwargs=kwargs) + -@app.task -def mail_html_envelopes(envelopes, subject, html_template, lang=settings.LANGUAGE_CODE,): - ''' +@app.task(bind=True) +def mail_html_envelopes(self, envelopes, subject: str, html_template: str, lang: str = settings.LANGUAGE_CODE): + """ e.g envelopes = {"name": "Joe Life", "email": "info@domain.com", "kwargs": "Dictionary of extra"} - ''' + + kwargs = { + users : {"username": "joe_user", "email": "joe@gmail.com", "name": "Joe Life"}, + site_name : "My Company", + logo_url : "http://www.g.com/logo.png", + site_url : "http://www.g.com", + logo : '{}'.format(site_url, site_url, logo_url) + } + """ translation.activate(lang) - mail = PyMailMultiPart(subject=subject, html_template=html_template,) - mail.send_envelopes(envelopes=envelopes) \ No newline at end of file + mail = PyMailMultiPart(subject=subject, html_template=html_template, ) + mail.send_envelopes(envelopes=envelopes) 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 39725c8..438b7bd 100644 --- a/main/core/utils/__init__.py +++ b/main/core/utils/__init__.py @@ -1,17 +1,19 @@ from .boolean import is_date, is_decimal, is_float, is_int, is_json from .common import ( - get_query, - timestamp2datetime, - readabledateformat, - get_client_ip, - str2date, - display_form_validations, - shortenLargeNumber, - password_generator, - paginate, + 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 61575ce..a2a2e8c 100644 --- a/main/core/utils/common.py +++ b/main/core/utils/common.py @@ -1,23 +1,29 @@ -# -*- encoding: utf-8 -*- -from __future__ import unicode_literals -from django.utils.translation import gettext_lazy as _ -from django.conf import settings +import random +import re +import string +import socket +from typing import Tuple, Optional + +from dateutil.parser import parse from django.contrib import messages -from django.utils import timezone as djtz -from django.utils.dateformat import DateFormat -from django.db.models import Q from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage +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 dateutil.parser import parse from .boolean import is_date -import re, string, random + def timestamp2datetime(timestamp): - return djtz.datetime.fromtimestamp(float(timestamp)/1000.0) + return timezone.datetime.fromtimestamp(float(timestamp) / 1000.0) -def readabledateformat(datetime): + +def readable_date_format(datetime): return DateFormat(datetime).format("d F Y H:i") + def get_client_ip(request): x_forwarded_for = request.headers.get('X-Forwarded-For') if x_forwarded_for: @@ -26,13 +32,16 @@ def get_client_ip(request): ip = request.META.get('REMOTE_ADDR') return ip -def str2date(datestring, lang="en"): - langdayfirst = { + +def str2date(date_string, lang="en"): + lang_day_first = { "en": False, "tr": True, "ar": False, } - return djtz.make_aware(parse(datestring, dayfirst=langdayfirst[lang])) if is_date(datestring) else djtz.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): paginator = Paginator(objects, per_page) @@ -44,25 +53,29 @@ def paginate(objects, per_page=24, page=1): paginated_objects = paginator.page(paginator.num_pages) return paginated_objects -def normalize_query(query_string, findterms=re.compile(r'"([^"]+)"|(\S+)').findall, normspace=re.compile(r'\s{2,}').sub): - ''' Splits the query string in invidual keywords, getting rid of unecessary spaces + +def normalize_query(query_string, findterms=re.compile(r'"([^"]+)"|(\S+)').findall, + normspace=re.compile(r'\s{2,}').sub): + """ Splits the query string in invidual keywords, getting rid of unecessary spaces and grouping quoted words together. Example: - + >>> normalize_query(' some random words "with quotes " and spaces') ['some', 'random', 'words', 'with quotes', 'and', 'spaces'] - - ''' + + """ return [normspace(' ', (t[0] or t[1]).strip()) for t in findterms(query_string)] + + def get_query(query_string, search_fields): - ''' Returns a query, that is a combination of Q objects. That combination + """ Returns a query, that is a combination of Q objects. That combination aims to search keywords within a model by testing the given search fields. - - ''' - query = None # Query to search for every search term + + """ + query = None # Query to search for every search term terms = normalize_query(query_string) for term in terms: - or_query = None # Query to search for a given term in each field + or_query = None # Query to search for a given term in each field for field_name in search_fields: q = Q(**{"%s__icontains" % field_name: term}) if or_query is None: @@ -77,13 +90,15 @@ def get_query(query_string, search_fields): query = Q(**{"%s__icontains" % search_fields[0]: ""}) return query + def display_form_validations(form, request, message_type=messages.ERROR): for field_name, errors in form.errors.items(): field = form.fields.get(field_name) field_name = field.label if field else field_name messages.add_message(request, message_type, "{}: {}".format(field_name, ", ".join(errors))) -def shortenLargeNumber(num, digits=1): + +def shorten_large_number(num, digits=1): units = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'] for i in range(len(units) - 1, -1, -1): decimal = 1000 ** (i + 1) @@ -91,6 +106,7 @@ def shortenLargeNumber(num, digits=1): return "{}{}".format(round(num / decimal, digits), units[i]) return str(num) + def password_generator(size=8, chars=string.ascii_letters + string.digits): """ Returns a string of random characters, useful in generating temporary @@ -102,4 +118,24 @@ def password_generator(size=8, chars=string.ascii_letters + string.digits): Credit: Ignacio Vasquez-Abrams Source: http://stackoverflow.com/a/2257449 """ - return ''.join(random.choice(chars) for i in range(size)) \ No newline at end of file + return ''.join(random.choice(chars) for i in range(size)) + + +def is_online(host: str, port: int) -> Tuple[bool, str]: + skt = socket.socket() + try: + skt.settimeout(1.0) + skt.connect((host, port)) + return True, "OK, Jasmin Connected" + 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/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/static/assets/img/django-aio.png b/main/static/assets/img/django-aio.png deleted file mode 100644 index 0f586df..0000000 Binary files a/main/static/assets/img/django-aio.png and /dev/null differ diff --git a/main/static/assets/img/jasmin.jpg b/main/static/assets/img/jasmin.jpg new file mode 100644 index 0000000..37aa423 Binary files /dev/null and b/main/static/assets/img/jasmin.jpg differ diff --git a/main/static/assets/img/jasmin.png b/main/static/assets/img/jasmin.png new file mode 100644 index 0000000..4543c68 Binary files /dev/null and b/main/static/assets/img/jasmin.png differ diff --git a/main/taskapp/__init__.py b/main/taskapp/__init__.py deleted file mode 100644 index 45e0195..0000000 --- a/main/taskapp/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# -*- coding: utf-8 -*- -default_app_config = 'main.taskapp.apps.CeleryConfig' diff --git a/main/taskapp/apps.py b/main/taskapp/apps.py deleted file mode 100644 index 8aba14e..0000000 --- a/main/taskapp/apps.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- encoding: utf-8 -*- -from __future__ import absolute_import -from django.apps import AppConfig -from django.conf import settings -from .celery import app - -class CeleryConfig(AppConfig): - name = "main.taskapp" - verbose_name = "Celery Config" - label = "celeryapp" - - def ready(self): - # Using a string here means the worker will not have to - # pickle the object when using Windows. - app.config_from_object('django.conf:settings' , namespace='CELERY') - app.autodiscover_tasks(lambda: settings.INSTALLED_APPS, force=True) \ No newline at end of file diff --git a/main/taskapp/celery.py b/main/taskapp/celery.py deleted file mode 100644 index 6ab71fc..0000000 --- a/main/taskapp/celery.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- encoding: utf-8 -*- -from __future__ import absolute_import -from django.conf import settings -from django.apps import AppConfig -from celery import Celery -from celery.utils.log import get_task_logger -import os - -logger = get_task_logger(__name__) - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.dev") - -REDIS_URL = f"redis://{settings.REDIS_HOST}:{settings.REDIS_PORT}/{settings.REDIS_DB}" - -app = Celery('main', backend=REDIS_URL, broker=REDIS_URL) - -DEFAULT_RETRY_DELAY = 5 # 15 seconds -MAX_RETRIES = 5 -CELERY_TIMEZONE = settings.TIME_ZONE # "Etc/GMT-3" or "Europe/Istanbul" -CELERY_ENABLE_UTC = False - - -@app.task(bind=True) -def debug_task(self): - print('Request: {0!r}'.format(self.request)) # pragma: no cover - - -def revoke_task(task_id): - app.control.revoke(task_id) - - -def clear_tasks(): - return app.control.purge() 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/apps.py b/main/users/apps.py index 6d900fc..9193cc0 100644 --- a/main/users/apps.py +++ b/main/users/apps.py @@ -1,12 +1,10 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals, absolute_import - from django.apps import AppConfig from django.utils.translation import gettext_lazy as _ from django.contrib.auth.apps import AuthConfig AuthConfig.verbose_name = _("Groups") + class UsersConfig(AppConfig): name = "main.users" - verbose_name = _("Users") \ No newline at end of file + verbose_name = _("Users") 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/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/middleware.py b/main/users/middleware.py index 3d1d530..8627b54 100644 --- a/main/users/middleware.py +++ b/main/users/middleware.py @@ -8,18 +8,19 @@ from datetime import timedelta from dateutil.parser import parse + class LastUserActivityMiddleware(MiddlewareMixin): KEY = "last-activity" def process_request(self, request): if request.user.is_authenticated: last_activity = request.session.get(self.KEY) - + # If key is old enough, update database too_old_time = timezone.now() - timedelta(seconds=settings.LAST_ACTIVITY_INTERVAL_SECS) if not last_activity or parse(last_activity) < too_old_time: User.objects.filter(pk=request.user.pk).update( - last_login=timezone.now(), - login_count=F('login_count')+1) + last_login=timezone.now(), + login_count=F('login_count') + 1) request.session[self.KEY] = timezone.now().isoformat() - return None \ No newline at end of file + return None diff --git a/main/users/urls.py b/main/users/urls.py index 30a7b0d..b8cd760 100644 --- a/main/users/urls.py +++ b/main/users/urls.py @@ -1,8 +1,4 @@ -# -*- 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 django.urls import path from .views import * app_name = 'users' @@ -11,6 +7,9 @@ path(route='profile/', view=profile_view, name='profile_view'), path(route='settings/', view=settings_view, name='settings_view'), 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=signout_view, name="signout_view",), -] \ No newline at end of file + 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 85fc80a..103de96 100644 --- a/main/users/views/__init__.py +++ b/main/users/views/__init__.py @@ -1,2 +1,3 @@ from .profile import profile_view, settings_view, activity_log_view -from .signin import signin_view, signout_view \ No newline at end of file +from .reset import reset_view, reset_password_view, email_verification_view +from .signin import signin_view, logout_view diff --git a/main/users/views/profile.py b/main/users/views/profile.py index 69b415b..301b52e 100644 --- a/main/users/views/profile.py +++ b/main/users/views/profile.py @@ -1,23 +1,23 @@ -# -*- encoding: utf-8 -*- -from __future__ import unicode_literals -from django.utils.translation import gettext as _ -from django.utils import timezone as djtz -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.db.models import Q +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 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 -from PIL import Image -import json, os @login_required def profile_view(request): @@ -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: @@ -84,7 +84,7 @@ def profile_view(request): return JsonResponse(dict( message=str(_("Amazing!, Your profile picture has been updated successfully")), state=200, - result="%(filename)s?s=%(timenow)s" % dict(filename=filename, timenow=djtz.now()) + result="%(filename)s?s=%(timenow)s" % dict(filename=filename, timenow=timezone.now()) )) elif s == "avatar_reset": try: @@ -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): @@ -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 5d535d2..e784389 100644 --- a/main/users/views/reset.py +++ b/main/users/views/reset.py @@ -1,13 +1,80 @@ -# -*- encoding: utf-8 -*- -from __future__ import unicode_literals from django.utils.translation import gettext as _ -from django.shortcuts import render -from django.contrib.auth import login, logout, authenticate -from django.contrib import messages -from django.shortcuts import HttpResponseRedirect, render, redirect, HttpResponse 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 django.conf import settings + +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): - return render(request, "auth/reset.html") \ No newline at end of file + if request.POST: + form = ResetPasswordForm(request.POST) + username = request.POST.get("username") + if form.is_valid(): + 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:signin_view")) + else: + messages.warning(request, _("Warning, invalid username or email address")) + else: + display_form_validations(form=form, request=request) + 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:signin_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_email = True + user.is_verified = True + user.save() + messages.success(request, _("Your email hase been verified successfully")) + else: + messages.error(request, _("Unknown error occurred!")) + return redirect(reverse("users:signin_view")) + diff --git a/main/users/views/signin.py b/main/users/views/signin.py index 741efc7..d05f862 100644 --- a/main/users/views/signin.py +++ b/main/users/views/signin.py @@ -1,18 +1,14 @@ -# -*- encoding: utf-8 -*- -from __future__ import unicode_literals from django.utils.translation import gettext as _ -from django.shortcuts import render from django.contrib.auth import login, logout, authenticate from django.contrib import messages -from django.shortcuts import HttpResponseRedirect, render, redirect, HttpResponse +from django.shortcuts import HttpResponseRedirect, render, redirect from django.db.models import Q -from django.urls import reverse -from django.conf import settings from main.users.models import User from main.users.forms import SignInForm from main.core.utils import display_form_validations + def signin_view(request): form = SignInForm() if request.POST: @@ -20,17 +16,18 @@ def signin_view(request): if form.is_valid(): username = request.POST.get("username") password = request.POST.get("password") - nextpage = request.POST.get("next") or "/" + next_page = request.POST.get("next") or "/" try: - user = User.objects.get(Q(username=username)|Q(email=username)) + user = User.objects.get(Q(username=username) | Q(email=username)) auth = authenticate(username=user.username, password=password) if auth and user: if user.is_active: if not user.is_email: - messages.warning(request, _("Your Email Address is not verified yet! please verify your email address.")) + msg = _("Your Email Address is not verified yet! please verify your email address.") + messages.warning(request, message=msg) else: login(request, user) - return redirect(nextpage) + return redirect(next_page) else: messages.warning(request, _('User banned, please contact support')) else: @@ -42,6 +39,7 @@ def signin_view(request): display_form_validations(form=form, request=request) return render(request, "auth/signin.html", dict(form=form)) -def signout_view(request): + +def logout_view(request): logout(request) return HttpResponseRedirect('/') 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/dashboard.js b/main/web/static/web/dashboard.js new file mode 100644 index 0000000..6b3e44b --- /dev/null +++ b/main/web/static/web/dashboard.js @@ -0,0 +1,24 @@ +(function($){ + //var csrfmiddlewaretoken = document.getElementsByName('csrfmiddlewaretoken')[0].value; + var gw_state = function() { + $.ajax({ + type: "GET", + url: window.location.pathname + 'manage/', + data: {s: 'gw_state'}, + beforeSend: function(){}, + success: function(data){ + if (data.status) { + toastr.success(data["message"], {closeButton: true, progressBar: true,}); + } else { + toastr.warning(data["message"], {closeButton: true, progressBar: true,}); + } + $("#binding_status").removeClass("bg-secondary").addClass(data.status? 'bg-success' : 'bg-warning'); + $("#binding_status_text").text(data.message); + }, + error: function(jqXHR, textStatus, errorThrown){ + toastr.error(JSON.parse(jqXHR.responseText)["message"], {closeButton: true, progressBar: true,}); + } + }); + } + gw_state(); +})(jQuery); \ No newline at end of file diff --git a/main/web/static/web/global.js b/main/web/static/web/global.js index fb771f6..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(); @@ -42,7 +43,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 +51,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 +121,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 +142,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/auth/base.html b/main/web/templates/auth/base.html index 9bd94d1..579c2d9 100644 --- a/main/web/templates/auth/base.html +++ b/main/web/templates/auth/base.html @@ -1,6 +1,6 @@ {% load static i18n %} - + {% 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/profile.html b/main/web/templates/auth/profile.html index cdc0097..0780a5e 100644 --- a/main/web/templates/auth/profile.html +++ b/main/web/templates/auth/profile.html @@ -61,7 +61,10 @@

{% trans "Personal Information" %}

- {% trans "You need to activate your email address when changed" %} + {% trans "You need to activate your email address when changed" %} +
+
+ {% if user.is_email %}{% else %}{% endif %}
diff --git a/main/web/templates/auth/reset.html b/main/web/templates/auth/reset.html index edd9f0d..07b54a7 100644 --- a/main/web/templates/auth/reset.html +++ b/main/web/templates/auth/reset.html @@ -1,7 +1,30 @@ {% 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" %} + {% if show_confirm_form %} +
{% csrf_token %} +
+ +
+
+ +
+ +
+ {% else %} +
{% csrf_token %} +
+ +
+ +
+ {% endif %} +
+
{% 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 %} -
- + diff --git a/main/web/templates/web/dashboard.html b/main/web/templates/web/dashboard.html index f305f71..89f4841 100644 --- a/main/web/templates/web/dashboard.html +++ b/main/web/templates/web/dashboard.html @@ -20,6 +20,18 @@
+
+
+
+ {% 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/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 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 @@ - + diff --git a/main/web/templates/web/includes/topbar.html b/main/web/templates/web/includes/topbar.html index 677dd52..a653cc5 100644 --- a/main/web/templates/web/includes/topbar.html +++ b/main/web/templates/web/includes/topbar.html @@ -26,7 +26,7 @@ {% trans "Activity Log" %} - + {% trans 'Logout' %} 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) diff --git a/requirements.txt b/requirements.txt index d216135..0345e78 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,40 +1,2 @@ -# asgiref -celery -coreapi -coreschema +-r scripts/development.txt -Django==3.2.* -# django-3-jet==1.0.8 -django-jet-reboot -django-autoslug -django-crequest -django-environ -django-filter -djangorestframework -dnspython -email-validator -gunicorn -gevent -importlib-metadata==4.8.2 -itypes -Markdown -MarkupSafe -pexpect -Pillow -psycopg2-binary -ptyprocess -pysha3 -python-dateutil -pytz -PyYAML -redis -requests - -ua-parser -uritemplate -user-agents - -tablib -xlrd -xlwt -zipp diff --git a/scripts/base.txt b/scripts/base.txt new file mode 100644 index 0000000..9c59242 --- /dev/null +++ b/scripts/base.txt @@ -0,0 +1,28 @@ +# Celery +celery[redis]>=5.3,<5.4 + +# Django Dependencies +Django>=4.2,<4.3 +# django-3-jet==1.0.8 +django-jet-reboot +django-autoslug +django-crequest +django-environ +django-filter +djangorestframework +coreapi + +email-validator +pexpect +Pillow>=9.5,<9.6 +python-dateutil +PyYAML +requests + +ua-parser +user-agents + +tablib +xlrd +xlwt +zipp \ No newline at end of file diff --git a/scripts/development.txt b/scripts/development.txt new file mode 100644 index 0000000..a047682 --- /dev/null +++ b/scripts/development.txt @@ -0,0 +1,3 @@ +-r base.txt +psycopg[binary]>=3.1,<3.2 +django-upgrade diff --git a/scripts/production.txt b/scripts/production.txt new file mode 100644 index 0000000..b5f18e4 --- /dev/null +++ b/scripts/production.txt @@ -0,0 +1,4 @@ +-r base.txt +gunicorn +uvicorn +psycopg>=3.1,<3.2