├── .artifact_exclude ├── .circleci └── config.yml ├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile ├── MANIFEST.in ├── Makefile ├── README.md ├── automats ├── contact_synchronizer.png ├── contact_synchronizer.vsd ├── domain_auth_changer.png ├── domain_auth_changer.vsd ├── domain_contacts_synchronizer.png ├── domain_contacts_synchronizer.vsd ├── domain_hostnames_synchronizer.png ├── domain_hostnames_synchronizer.vsd ├── domain_reader.png ├── domain_reader.vsd ├── domain_refresher.png ├── domain_refresher.vsd ├── domain_resurrector.png ├── domain_resurrector.vsd ├── domain_synchronizer.png ├── domain_synchronizer.vsd ├── domain_transfer_requestor.png ├── domain_transfer_requestor.vsd ├── domains_checker.png ├── domains_checker.vsd ├── visio2switch.vss └── zenaida.vss ├── bin └── epp_gate.pl ├── django-microservice.json ├── docker-compose.yml ├── docs ├── Makefile ├── _plantuml │ └── plantuml.jar ├── _static │ └── .gitkeep ├── conf.py ├── index.rst ├── installation.rst └── usage.rst ├── etc ├── deploy.example ├── epp_credentials.txt.example ├── init │ ├── gate-zenaida.conf │ └── uwsgi-zenaida.conf ├── logrotate.d │ └── zenaida ├── nginx │ └── zenaida.example ├── rabbitmq_client_credentials.txt.example ├── rabbitmq_gate_credentials.txt.example ├── startup_user_systemctl.example ├── systemd │ └── system │ │ ├── uwsgi-emperor.service.example │ │ ├── zenaida-background-worker.service.example │ │ ├── zenaida-btcpay.service.example │ │ ├── zenaida-gate-health.path.example │ │ ├── zenaida-gate-watcher.service.example │ │ ├── zenaida-gate.service.example │ │ ├── zenaida-notifications.service.example │ │ └── zenaida-poll.service.example └── uwsgi │ ├── emperor.ini │ ├── local.uwsgi.ini │ └── vassals │ └── zenaida.ini ├── graph_models.png ├── opt └── cocca-8 │ └── make_ssl_cert ├── pytest.ini ├── requirements.txt ├── requirements ├── requirements-base.txt └── requirements-testing.txt ├── setup.cfg ├── setup_gen.py ├── src ├── __init__.py ├── accounts │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── decorators.py │ ├── forms.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── process_notifications.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20190203_0040.py │ │ ├── 0003_notification.py │ │ ├── 0004_auto_20200504_0830.py │ │ ├── 0005_auto_20201219_1451.py │ │ ├── 0006_account_notes.py │ │ ├── 0007_alter_notification_status.py │ │ ├── 0008_account_is_approved.py │ │ ├── 0009_alter_notification_subject.py │ │ ├── 0010_alter_notification_subject.py │ │ ├── 0011_alter_notification_subject.py │ │ └── __init__.py │ ├── models │ │ ├── __init__.py │ │ ├── account.py │ │ ├── activation.py │ │ └── notification.py │ ├── notifications.py │ ├── tasks.py │ ├── templates │ │ └── accounts │ │ │ ├── login.html │ │ │ ├── logout.html │ │ │ ├── password_change_done.html │ │ │ ├── password_change_form.html │ │ │ ├── password_reset.html │ │ │ ├── password_reset_complete.html │ │ │ ├── password_reset_confirm.html │ │ │ ├── password_reset_done.html │ │ │ └── register.html │ ├── users.py │ └── views.py ├── automats │ ├── __init__.py │ ├── automat.py │ ├── contact_synchronizer.py │ ├── domain_auth_changer.py │ ├── domain_contacts_synchronizer.py │ ├── domain_hostnames_synchronizer.py │ ├── domain_reader.py │ ├── domain_refresher.py │ ├── domain_resurrector.py │ ├── domain_synchronizer.py │ ├── domain_transfer_requestor.py │ └── domains_checker.py ├── back │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── constants.py │ ├── csv_import.py │ ├── csv_import_legacy.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ ├── background_worker.py │ │ │ ├── background_worker_daily.py │ │ │ ├── cleanup_unused_contacts.py │ │ │ ├── csv_import.py │ │ │ ├── email_announcement.py │ │ │ ├── epp_poll.py │ │ │ ├── list_domains.py │ │ │ ├── send_email.py │ │ │ └── sync_domains.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20181119_1117.py │ │ ├── 0003_domain_epp_status.py │ │ ├── 0004_auto_20190120_1936.py │ │ ├── 0005_auto_20190203_0040.py │ │ ├── 0006_auto_20190208_2051.py │ │ ├── 0007_auto_20190209_1420.py │ │ ├── 0008_auto_20190209_2011.py │ │ ├── 0009_auto_20190224_1822.py │ │ ├── 0010_auto_20190224_1934.py │ │ ├── 0011_auto_20190302_1832.py │ │ ├── 0012_domain_epp_statuses.py │ │ ├── 0013_auto_20190425_1053.py │ │ ├── 0014_auto_20190509_1034.py │ │ ├── 0015_auto_20190523_0940.py │ │ ├── 0016_auto_20190602_1419.py │ │ ├── 0017_auto_20190901_1958.py │ │ ├── 0018_profile_email_notifications_enabled.py │ │ ├── 0019_auto_20191106_0838.py │ │ ├── 0020_auto_20200226_0850.py │ │ ├── 0021_auto_20200229_1043.py │ │ ├── 0022_profile_automatic_renewal_enabled.py │ │ ├── 0023_auto_20200304_0925.py │ │ ├── 0024_auto_20200304_1005.py │ │ ├── 0025_auto_20200314_1144.py │ │ ├── 0026_auto_20200504_0830.py │ │ ├── 0027_auto_20200701_1835.py │ │ ├── 0028_auto_20201219_1451.py │ │ ├── 0029_domain_modified_date.py │ │ ├── 0030_domain_latest_sync_date.py │ │ ├── 0031_auto_20250116_1500.py │ │ ├── 0032_auto_20250118_1929.py │ │ ├── 0033_backendrenew.py │ │ ├── 0034_backendrenew_previous_expiry_date.py │ │ ├── 0035_backendrenew_next_expiry_date.py │ │ ├── 0036_auto_20250302_1053.py │ │ ├── 0037_alter_domain_auto_renew_enabled.py │ │ ├── 0038_backendrenew_restore_order.py │ │ ├── 0039_alter_domain_auto_renew_enabled.py │ │ ├── 0040_auto_20250529_1707.py │ │ ├── 0041_domain_extension_info.py │ │ └── __init__.py │ ├── models │ │ ├── __init__.py │ │ ├── back_end_renew.py │ │ ├── contact.py │ │ ├── domain.py │ │ ├── profile.py │ │ ├── registrar.py │ │ └── zone.py │ ├── tasks.py │ └── validators.py ├── base │ ├── __init__.py │ ├── bruteforceprotection.py │ ├── email.py │ ├── exceptions.py │ ├── mixins.py │ ├── push_notifications.py │ ├── sms.py │ └── utils.py ├── billing │ ├── __init__.py │ ├── admin.py │ ├── decorators.py │ ├── exceptions.py │ ├── forms.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ ├── btcpay_verify.py │ │ │ └── verify_4cs.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20190120_1549.py │ │ ├── 0002_auto_20190120_2142.py │ │ ├── 0003_auto_20190120_2150.py │ │ ├── 0004_merge_20190120_2203.py │ │ ├── 0005_auto_20190120_2224.py │ │ ├── 0006_auto_20190120_2303.py │ │ ├── 0007_order_orderitem.py │ │ ├── 0008_auto_20190127_1513.py │ │ ├── 0009_order_description.py │ │ ├── 0010_auto_20190202_1307.py │ │ ├── 0011_auto_20190505_1645.py │ │ ├── 0012_btcpayinvoice.py │ │ ├── 0013_auto_20190705_1806.py │ │ ├── 0014_auto_20190811_2030.py │ │ ├── 0015_auto_20190901_1958.py │ │ ├── 0016_auto_20201219_1451.py │ │ ├── 0017_auto_20210123_1757.py │ │ ├── 0018_auto_20210124_0739.py │ │ ├── 0019_alter_orderitem_status.py │ │ ├── 0020_order_retries.py │ │ ├── 0021_alter_orderitem_status.py │ │ └── __init__.py │ ├── models │ │ ├── __init__.py │ │ ├── order.py │ │ ├── order_item.py │ │ └── payment.py │ ├── orders.py │ ├── pay_4csonline │ │ ├── __init__.py │ │ └── views.py │ ├── pay_btcpay │ │ ├── __init__.py │ │ ├── models.py │ │ └── views.py │ ├── payments.py │ ├── tasks.py │ ├── templates │ │ └── billing │ │ │ ├── 4csonline │ │ │ ├── failed_payment.html │ │ │ ├── merchant_form.html │ │ │ ├── pending_payment.html │ │ │ ├── start_payment.html │ │ │ └── success_payment.html │ │ │ ├── account_invoices.html │ │ │ ├── account_orders.html │ │ │ ├── account_payments.html │ │ │ ├── btcpay │ │ │ ├── __init__.py │ │ │ └── start_payment.html │ │ │ ├── new_payment.html │ │ │ └── order_details.html │ └── views.py ├── board │ ├── __init__.py │ ├── admin.py │ ├── dashboard.py │ ├── forms.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models │ │ ├── __init__.py │ │ └── csv_file_sync.py │ ├── templates │ │ ├── __init__.py │ │ └── board │ │ │ ├── __init__.py │ │ │ ├── admin_head.html │ │ │ ├── admin_index.html │ │ │ ├── admin_page.html │ │ │ ├── balance_adjustment.html │ │ │ ├── bulk_transfer.html │ │ │ ├── csv_file_sync.html │ │ │ ├── csv_file_sync_record.html │ │ │ ├── financial_report.html │ │ │ ├── not_existing_domain_sync.html │ │ │ ├── sending_single_email.html │ │ │ └── two_factor_reset.html │ └── views.py ├── front │ ├── __init__.py │ ├── decorators.py │ ├── forms.py │ ├── templates │ │ ├── escrow │ │ │ └── escrow.html │ │ ├── faq │ │ │ └── faq.html │ │ └── front │ │ │ ├── 404_error.html │ │ │ ├── 500_error.html │ │ │ ├── account_contact_create_update.html │ │ │ ├── account_contact_delete.html │ │ │ ├── account_contacts.html │ │ │ ├── account_domain_create.html │ │ │ ├── account_domain_details.html │ │ │ ├── account_domain_transfer_code.html │ │ │ ├── account_domain_transfer_takeover.html │ │ │ ├── account_domains.html │ │ │ ├── account_profile.html │ │ │ ├── contact_us.html │ │ │ ├── domain_lookup.html │ │ │ ├── escrow.html │ │ │ └── faq.html │ ├── templatetags │ │ ├── __init__.py │ │ └── front_filters.py │ └── views.py ├── lib │ ├── __init__.py │ ├── captcha.py │ ├── iso_countries.py │ └── xml2json.py ├── logs │ ├── __init__.py │ ├── admin.py │ ├── middleware.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_requestlog_path_full.py │ │ └── __init__.py │ └── models.py ├── main │ ├── __init__.py │ ├── apps.py │ ├── confdocs.py │ ├── params_docker.py │ ├── params_example.py │ ├── settings.py │ ├── static │ │ ├── css │ │ │ └── styles.css │ │ ├── font-awesome │ │ │ ├── css │ │ │ │ ├── font-awesome.css │ │ │ │ ├── font-awesome.css.map │ │ │ │ └── font-awesome.min.css │ │ │ ├── fonts │ │ │ │ ├── FontAwesome.otf │ │ │ │ ├── fontawesome-webfont.eot │ │ │ │ ├── fontawesome-webfont.svg │ │ │ │ ├── fontawesome-webfont.ttf │ │ │ │ ├── fontawesome-webfont.woff │ │ │ │ └── fontawesome-webfont.woff2 │ │ │ ├── less │ │ │ │ ├── animated.less │ │ │ │ ├── bordered-pulled.less │ │ │ │ ├── core.less │ │ │ │ ├── extras.less │ │ │ │ ├── fixed-width.less │ │ │ │ ├── font-awesome.less │ │ │ │ ├── icons.less │ │ │ │ ├── larger.less │ │ │ │ ├── list.less │ │ │ │ ├── mixins.less │ │ │ │ ├── path.less │ │ │ │ ├── rotated-flipped.less │ │ │ │ ├── screen-reader.less │ │ │ │ ├── spinning.less │ │ │ │ ├── stacked.less │ │ │ │ └── variables.less │ │ │ └── scss │ │ │ │ ├── _animated.scss │ │ │ │ ├── _bordered-pulled.scss │ │ │ │ ├── _core.scss │ │ │ │ ├── _extras.scss │ │ │ │ ├── _fixed-width.scss │ │ │ │ ├── _icons.scss │ │ │ │ ├── _larger.scss │ │ │ │ ├── _list.scss │ │ │ │ ├── _mixins.scss │ │ │ │ ├── _path.scss │ │ │ │ ├── _rotated-flipped.scss │ │ │ │ ├── _screen-reader.scss │ │ │ │ ├── _spinning.scss │ │ │ │ ├── _stacked.scss │ │ │ │ ├── _variables.scss │ │ │ │ └── font-awesome.scss │ │ ├── icons │ │ │ ├── android-icon-144x144.png │ │ │ ├── android-icon-192x192.png │ │ │ ├── android-icon-36x36.png │ │ │ ├── android-icon-48x48.png │ │ │ ├── android-icon-72x72.png │ │ │ ├── android-icon-96x96.png │ │ │ ├── apple-icon-114x114.png │ │ │ ├── apple-icon-120x120.png │ │ │ ├── apple-icon-144x144.png │ │ │ ├── apple-icon-152x152.png │ │ │ ├── apple-icon-180x180.png │ │ │ ├── apple-icon-57x57.png │ │ │ ├── apple-icon-60x60.png │ │ │ ├── apple-icon-72x72.png │ │ │ ├── apple-icon-76x76.png │ │ │ ├── apple-icon-precomposed.png │ │ │ ├── apple-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon-96x96.png │ │ │ ├── ms-icon-144x144.png │ │ │ ├── ms-icon-150x150.png │ │ │ ├── ms-icon-310x310.png │ │ │ ├── ms-icon-70x70.png │ │ │ └── www-icon.png │ │ ├── images │ │ │ ├── bitcoin.png │ │ │ ├── favicon.ico │ │ │ ├── mastercard.png │ │ │ └── visa.png │ │ ├── js │ │ │ ├── api.js │ │ │ ├── ie10-viewport-bug-workaround.js │ │ │ └── recaptcha__en.js │ │ └── vendor │ │ │ ├── bootstrap │ │ │ ├── css │ │ │ │ ├── bootstrap-grid.css │ │ │ │ ├── bootstrap-grid.css.map │ │ │ │ ├── bootstrap-grid.min.css │ │ │ │ ├── bootstrap-grid.min.css.map │ │ │ │ ├── bootstrap-reboot.css │ │ │ │ ├── bootstrap-reboot.css.map │ │ │ │ ├── bootstrap-reboot.min.css │ │ │ │ ├── bootstrap-reboot.min.css.map │ │ │ │ ├── bootstrap.css │ │ │ │ ├── bootstrap.css.map │ │ │ │ ├── bootstrap.min.css │ │ │ │ └── bootstrap.min.css.map │ │ │ └── js │ │ │ │ ├── bootstrap.bundle.js │ │ │ │ ├── bootstrap.bundle.js.map │ │ │ │ ├── bootstrap.bundle.min.js │ │ │ │ ├── bootstrap.bundle.min.js.map │ │ │ │ ├── bootstrap.js │ │ │ │ ├── bootstrap.js.map │ │ │ │ ├── bootstrap.min.js │ │ │ │ └── bootstrap.min.js.map │ │ │ ├── jquery │ │ │ ├── jquery-3.2.1.js │ │ │ ├── jquery-3.2.1.min.js │ │ │ └── jquery-3.2.1.min.map │ │ │ └── popper │ │ │ ├── popper-utils.js │ │ │ ├── popper-utils.js.map │ │ │ ├── popper-utils.min.js │ │ │ ├── popper-utils.min.js.map │ │ │ ├── popper.js │ │ │ ├── popper.js.map │ │ │ ├── popper.min.js │ │ │ └── popper.min.js.map │ ├── templatetags │ │ ├── __init__.py │ │ └── settings_filter.py │ └── urls.py ├── manage.py ├── templates │ ├── .gitkeep │ ├── base │ │ ├── container_fluid.html │ │ ├── container_sm_10.html │ │ ├── container_sm_4.html │ │ ├── container_sm_8.html │ │ ├── epp_status.html │ │ ├── foot.html │ │ ├── head.html │ │ ├── index.html │ │ ├── pagination.html │ │ └── pagination_class_based_views.html │ └── email │ │ ├── account_approved.html │ │ ├── activation_profile.html │ │ ├── domain_deleted.html │ │ ├── domain_expire_in_1_day.html │ │ ├── domain_expire_in_3_days.html │ │ ├── domain_expire_in_5_days.html │ │ ├── domain_expire_soon.html │ │ ├── domain_expiring.html │ │ ├── domain_renewed.html │ │ ├── low_balance.html │ │ ├── low_balance_back_end_renew.html │ │ ├── single_email.html │ │ ├── test_announcement.html │ │ └── test_email.html ├── tests │ ├── __init__.py │ ├── accounts │ │ ├── __init__.py │ │ ├── test_notifications.py │ │ └── test_tasks.py │ ├── automats │ │ ├── __init__.py │ │ ├── test_contact_synchronizer.py │ │ ├── test_domain_auth_changer.py │ │ ├── test_domain_contacts_synchronizer.py │ │ ├── test_domain_hostnames_synchronizer.py │ │ ├── test_domain_refresher.py │ │ ├── test_domain_resurrector.py │ │ ├── test_domain_synchronizer.py │ │ ├── test_domain_transfer_requestor.py │ │ └── test_domains_checker.py │ ├── back │ │ ├── __init__.py │ │ ├── domains_sample.csv │ │ ├── domains_sample_legacy.csv │ │ ├── management │ │ │ ├── __init__.py │ │ │ └── commands │ │ │ │ ├── __init__.py │ │ │ │ └── test_background_worker_daily.py │ │ ├── test_csv_import.py │ │ └── test_tasks.py │ ├── base │ │ ├── __init__.py │ │ ├── test_bruteforceprotection.py │ │ ├── test_email.py │ │ ├── test_mixins.py │ │ ├── test_push_notifications.py │ │ └── test_sms.py │ ├── billing │ │ ├── __init__.py │ │ ├── pay_4csonline │ │ │ ├── __init__.py │ │ │ └── test_views.py │ │ ├── pay_btcpay │ │ │ ├── __init__.py │ │ │ └── test_views.py │ │ ├── test_orders.py │ │ ├── test_payments.py │ │ ├── test_tasks.py │ │ └── test_views.py │ ├── board │ │ ├── __init__.py │ │ ├── domains_sample.csv │ │ └── test_views.py │ ├── conftest.py │ ├── front │ │ ├── __init__.py │ │ └── test_views.py │ ├── main │ │ ├── __init__.py │ │ ├── test_apps.py │ │ └── test_settings.py │ ├── testsupport.py │ ├── two_factor │ │ ├── __init__.py │ │ ├── management │ │ │ ├── __init__.py │ │ │ └── commands │ │ │ │ ├── __init__.py │ │ │ │ ├── test_two_factor_disable.py │ │ │ │ └── test_two_factor_status.py │ │ ├── mixins.py │ │ └── test_utils.py │ └── zen │ │ ├── __init__.py │ │ ├── test_zcontacts.py │ │ └── test_zdomains.py ├── two_factor │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ ├── two_factor_disable.py │ │ │ └── two_factor_status.py │ ├── models.py │ ├── signals.py │ ├── templates │ │ └── two_factor │ │ │ ├── _base.html │ │ │ ├── _base_focus.html │ │ │ ├── _wizard_actions.html │ │ │ ├── _wizard_forms.html │ │ │ ├── core │ │ │ ├── backup_tokens.html │ │ │ ├── login.html │ │ │ ├── otp_required.html │ │ │ ├── setup.html │ │ │ └── setup_complete.html │ │ │ └── profile │ │ │ ├── disable.html │ │ │ └── profile.html │ ├── templatetags │ │ ├── __init__.py │ │ └── two_factor.py │ ├── urls.py │ ├── utils.py │ └── views │ │ ├── __init__.py │ │ ├── core.py │ │ ├── mixins.py │ │ ├── profile.py │ │ └── utils.py ├── wsgi.py └── zen │ ├── __init__.py │ ├── zcontacts.py │ ├── zdomains.py │ ├── zerrors.py │ ├── zmaster.py │ ├── zpoll.py │ ├── zusers.py │ └── zzones.py └── tox.ini /.artifact_exclude: -------------------------------------------------------------------------------- 1 | .git* 2 | docs/_build* 3 | venv 4 | src/test 5 | zenaida*.tgz 6 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | executors: 3 | my-machine-executor: 4 | machine: 5 | image: default 6 | 7 | jobs: 8 | zenaida-job: 9 | 10 | executor: my-machine-executor 11 | steps: 12 | - checkout 13 | - run: 14 | name: "Use Docker Compose" 15 | command: | 16 | docker-compose -v 17 | - run: 18 | name: "Build" 19 | command: | 20 | make docker/test 21 | 22 | workflows: 23 | build: 24 | jobs: 25 | - zenaida-job: 26 | filters: 27 | branches: 28 | ignore: master 29 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .tox 2 | venv 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | charset = utf-8 12 | 13 | # Use 2 spaces for the HTML files 14 | [*.html] 15 | indent_size = 2 16 | 17 | # The JSON files contain newlines inconsistently 18 | [*.json] 19 | indent_size = 2 20 | insert_final_newline = ignore 21 | 22 | # Use 2 spaces for the YAML files 23 | [*.yml] 24 | indent_style = space 25 | indent_size = 2 26 | 27 | [*.{rst,ini}] 28 | indent_style = space 29 | indent_size = 4 30 | 31 | # Ignore vendor libraries 32 | [**/static/**/vendor/**] 33 | indent_style = ignore 34 | indent_size = ignore 35 | 36 | # Minified JavaScript files shouldn't be changed 37 | [**.min.js] 38 | indent_style = ignore 39 | insert_final_newline = ignore 40 | 41 | # Makefiles always use tabs for indentation 42 | [Makefile] 43 | indent_style = tab 44 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | src/main/static/* linguist-vendored 2 | src/static/* linguist-vendored 3 | *.js linguist-vendored 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | global: 3 | - DOCKER_COMPOSE_VERSION=1.20.0 4 | 5 | 6 | services: 7 | - docker 8 | 9 | 10 | language: python 11 | 12 | cache: pip 13 | 14 | matrix: 15 | include: 16 | 17 | - name: "3.8" 18 | python: "3.8" 19 | 20 | 21 | before_install: 22 | - set -e 23 | - sudo rm /usr/local/bin/docker-compose 24 | - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose 25 | - chmod +x docker-compose 26 | - sudo mv docker-compose /usr/local/bin 27 | 28 | - docker -v 29 | - docker-compose -v 30 | - make -v 31 | 32 | 33 | script: 34 | - make docker/test 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # zenaida Change Log 2 | 3 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 4 | and should contain all notable changes to the project made for humans. 5 | 6 | 7 | ## [0.0.1] - 2018-03-25 8 | 9 | 10 | ### Added 11 | 12 | Added first files to the project 13 | 14 | 15 | 16 | 17 | 18 | 19 | ## [Unreleased] 20 | 21 | All unreleased changes should be added to appropriate section below 22 | 23 | ### Added 24 | New features 25 | - 26 | 27 | ### Changed 28 | Changes in existing functionality 29 | - 30 | 31 | ### Deprecated 32 | Once-stable features removed in upcoming releases 33 | - 34 | 35 | ### Removed 36 | Deprecated featured removed in this release 37 | - 38 | 39 | ### Fixed 40 | Any bug fixes 41 | - 42 | 43 | ### Security 44 | Invite users to upgrade in case of vulnerabilities 45 | - 46 | 47 | ## [0.0.1] - YYYY-MM-DD 48 | 49 | Adhere to [semantic versioning](http://semver.org/) and [ISO date format](http://www.iso.org/iso/home/standards/iso8601.htm) 50 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9.7 2 | WORKDIR /app 3 | COPY . /app 4 | COPY ./src/main/params_docker.py /app/src/main/params.py 5 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include CHANGELOG.md 3 | 4 | recursive-exclude tests * 5 | recursive-exclude * __pycache__ 6 | recursive-exclude * *.py[co] 7 | 8 | recursive-include docs *.rst conf.py Makefile 9 | -------------------------------------------------------------------------------- /automats/contact_synchronizer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/automats/contact_synchronizer.png -------------------------------------------------------------------------------- /automats/contact_synchronizer.vsd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/automats/contact_synchronizer.vsd -------------------------------------------------------------------------------- /automats/domain_auth_changer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/automats/domain_auth_changer.png -------------------------------------------------------------------------------- /automats/domain_auth_changer.vsd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/automats/domain_auth_changer.vsd -------------------------------------------------------------------------------- /automats/domain_contacts_synchronizer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/automats/domain_contacts_synchronizer.png -------------------------------------------------------------------------------- /automats/domain_contacts_synchronizer.vsd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/automats/domain_contacts_synchronizer.vsd -------------------------------------------------------------------------------- /automats/domain_hostnames_synchronizer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/automats/domain_hostnames_synchronizer.png -------------------------------------------------------------------------------- /automats/domain_hostnames_synchronizer.vsd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/automats/domain_hostnames_synchronizer.vsd -------------------------------------------------------------------------------- /automats/domain_reader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/automats/domain_reader.png -------------------------------------------------------------------------------- /automats/domain_reader.vsd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/automats/domain_reader.vsd -------------------------------------------------------------------------------- /automats/domain_refresher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/automats/domain_refresher.png -------------------------------------------------------------------------------- /automats/domain_refresher.vsd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/automats/domain_refresher.vsd -------------------------------------------------------------------------------- /automats/domain_resurrector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/automats/domain_resurrector.png -------------------------------------------------------------------------------- /automats/domain_resurrector.vsd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/automats/domain_resurrector.vsd -------------------------------------------------------------------------------- /automats/domain_synchronizer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/automats/domain_synchronizer.png -------------------------------------------------------------------------------- /automats/domain_synchronizer.vsd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/automats/domain_synchronizer.vsd -------------------------------------------------------------------------------- /automats/domain_transfer_requestor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/automats/domain_transfer_requestor.png -------------------------------------------------------------------------------- /automats/domain_transfer_requestor.vsd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/automats/domain_transfer_requestor.vsd -------------------------------------------------------------------------------- /automats/domains_checker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/automats/domains_checker.png -------------------------------------------------------------------------------- /automats/domains_checker.vsd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/automats/domains_checker.vsd -------------------------------------------------------------------------------- /automats/visio2switch.vss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/automats/visio2switch.vss -------------------------------------------------------------------------------- /automats/zenaida.vss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/automats/zenaida.vss -------------------------------------------------------------------------------- /django-microservice.json: -------------------------------------------------------------------------------- 1 | {"cookiecutter": {"cache": "redis", "python_version": "3.8", "database": "postgresql_psycopg2", "staticfiles": "y", "basepath": "zenaida", "project_slug": "zenaida", "namespace": "", "package": "zenaida", "project_short_description": " Open source domain registry system built on top of EPP protocol", "full_name": "Veselin Penev", "copyright_year": "2018", "_template": "template-py-package/django-microservice", "email": "penev.veselin@gmail.com"}} -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | 5 | postgres: 6 | restart: always 7 | image: postgres:15.1 8 | ports: 9 | - "5435:5432" 10 | environment: 11 | - POSTGRES_USER=zenaida_db_user 12 | - POSTGRES_PASSWORD=zenaida_db_user 13 | - POSTGRES_DB=zenaida_db_01 14 | 15 | test: 16 | environment: 17 | - DOCKER_ENV=1 18 | - PYTHON_VER=python3 19 | build: . 20 | command: make test 21 | links: 22 | - postgres 23 | depends_on: 24 | - postgres 25 | -------------------------------------------------------------------------------- /docs/_plantuml/plantuml.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/docs/_plantuml/plantuml.jar -------------------------------------------------------------------------------- /docs/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/docs/_static/.gitkeep -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. zenaida documentation master file, created by 2 | sphinx-quickstart on Tue Jul 9 22:26:36 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Open source domain registry system built on top of EPP protocol 7 | ============================================ 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | installation 15 | usage 16 | api/index 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | 25 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Installation 3 | ============ 4 | 5 | Via the deploy role in `devops`_. 6 | Set the following variables:: 7 | 8 | build_cmd: make artifact 9 | 10 | 11 | .. _devops: http://git.kpn.org/prjects/DE/repos/devops 12 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Usage 3 | ======== 4 | 5 | Via NGINX and Uwsgi as usual. 6 | -------------------------------------------------------------------------------- /etc/epp_credentials.txt.example: -------------------------------------------------------------------------------- 1 | epp.yourdomain.com 700 zenaida_registrar secret_password -------------------------------------------------------------------------------- /etc/init/gate-zenaida.conf: -------------------------------------------------------------------------------- 1 | # EPP Gate service configuration for Zenaida 2 | # 3 | # Copy to global init configuration: 4 | # 5 | # sudo cp etc/init/gate-zenaida.conf /etc/init/gate-zenaida.conf 6 | # 7 | # 8 | # Then stop/start EPP Gate service: 9 | # 10 | # sudo stop gate-zenaida 11 | # sudo start gate-zenaida 12 | # 13 | # 14 | # You can always check current situation with: 15 | # 16 | # sudo initctl status gate-zenaida 17 | # 18 | 19 | 20 | description "EPP Gate process to serve real-time requests between Zenaida and EPP registry system" 21 | 22 | start on runlevel [2345] 23 | stop on runlevel [!2345] 24 | 25 | setuid zenaida 26 | setgid zenaida 27 | 28 | pre-start script 29 | bash /home/zenaida/zenaida/bin/epp_gate_start.sh 30 | end script 31 | 32 | post-stop script 33 | bash /home/zenaida/zenaida/bin/epp_gate_stop.sh 34 | end script 35 | -------------------------------------------------------------------------------- /etc/init/uwsgi-zenaida.conf: -------------------------------------------------------------------------------- 1 | # Zenaida UWSGI Upstart service configuration. 2 | # 3 | # Copy to global init configuration: 4 | # 5 | # sudo cp etc/init/uwsgi-zenaida.conf /etc/init/uwsgi-zenaida.conf 6 | # 7 | # 8 | # Restart uwsgi service: 9 | # 10 | # sudo stop uwsgi-zenaida 11 | # sudo start uwsgi-zenaida 12 | # 13 | # 14 | # You can always check current situation with: 15 | # 16 | # sudo initctl status uwsgi-zenaida 17 | # 18 | 19 | 20 | description "uWSGI application server in Emperor mode to serve Zenaida traffic" 21 | 22 | start on runlevel [2345] 23 | stop on runlevel [!2345] 24 | 25 | setuid zenaida 26 | setgid www-data 27 | 28 | exec /home/zenaida/zenaida/venv/bin/uwsgi --ini /home/zenaida/zenaida/etc/uwsgi/emperor.ini 29 | -------------------------------------------------------------------------------- /etc/rabbitmq_client_credentials.txt.example: -------------------------------------------------------------------------------- 1 | epp_client epp_password -------------------------------------------------------------------------------- /etc/rabbitmq_gate_credentials.txt.example: -------------------------------------------------------------------------------- 1 | localhost 5672 rabbitmq_user rabbitmq_password -------------------------------------------------------------------------------- /etc/startup_user_systemctl.example: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | install -d -o zenaida /run/user/`id -u zenaida` 4 | 5 | systemctl start user@`id -u zenaida` 6 | 7 | systemctl start uwsgi-emperor.service 8 | 9 | DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/`id -u zenaida`/bus su zenaida -c 'systemctl --user restart zenaida-gate.service' 10 | 11 | DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/`id -u zenaida`/bus su zenaida -c 'systemctl --user restart zenaida-poll.service' 12 | 13 | DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/`id -u zenaida`/bus su zenaida -c 'systemctl --user restart zenaida-gate-watcher.service' 14 | 15 | DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/`id -u zenaida`/bus su zenaida -c 'systemctl --user restart zenaida-gate-health.path' 16 | 17 | DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/`id -u zenaida`/bus su zenaida -c 'systemctl --user restart zenaida-btcpay.service' 18 | 19 | DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/`id -u zenaida`/bus su zenaida -c 'systemctl --user restart zenaida-background-worker.service' 20 | 21 | DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/`id -u zenaida`/bus su zenaida -c 'systemctl --user restart zenaida-notifications.service' 22 | -------------------------------------------------------------------------------- /etc/systemd/system/uwsgi-emperor.service.example: -------------------------------------------------------------------------------- 1 | # Zenaida UWSGI systemd service configuration. 2 | # 3 | # Make link in your global systemd folder to activate the uwsgi emperor service: 4 | # 5 | # cd /home/zenaida/zenaida/ 6 | # cp etc/systemd/system/uwsgi-emperor.service.example etc/systemd/system/uwsgi-emperor.service 7 | # sudo ln -s etc/systemd/system/uwsgi-emperor.service /etc/systemd/system/ 8 | # 9 | # 10 | # Start uwsgi emperor service: 11 | # 12 | # sudo systemctl start uwsgi-emperor.service 13 | # 14 | # 15 | # You can always check current situation with: 16 | # 17 | # systemctl status uwsgi-emperor.service 18 | # 19 | 20 | 21 | [Unit] 22 | Description=uWSGI Emperor 23 | After=syslog.target 24 | 25 | [Service] 26 | ExecStart=/home/zenaida/zenaida/venv/bin/uwsgi --ini /home/zenaida/zenaida/etc/uwsgi/emperor.ini 27 | # Requires systemd version 211 or newer 28 | RuntimeDirectory=uwsgi 29 | Restart=always 30 | KillSignal=SIGQUIT 31 | Type=notify 32 | StandardError=syslog 33 | NotifyAccess=all 34 | 35 | [Install] 36 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /etc/systemd/system/zenaida-background-worker.service.example: -------------------------------------------------------------------------------- 1 | # Systemd service configuration for Zenaida background process which executes different regular tasks 2 | # 3 | # Copy and modify `zenaida-background-worker.service` file to your local systemd folder to enable the service: 4 | # 5 | # mkdir -p /home/zenaida/.config/systemd/user/ 6 | # cd /home/zenaida/zenaida/ 7 | # cp etc/systemd/system/zenaida-background-worker.service.example /home/zenaida/.config/systemd/user/zenaida-background-worker.service 8 | # systemctl --user enable zenaida-background-worker.service 9 | # 10 | # 11 | # To start Zenaida Poll service run this command: 12 | # 13 | # systemctl --user start zenaida-background-worker.service 14 | # 15 | # 16 | # You can always check current situation with: 17 | # 18 | # systemctl --user status zenaida-background-worker.service 19 | # 20 | 21 | [Unit] 22 | Description=ZenaidaBackgroundWorker 23 | After=network.target 24 | 25 | [Service] 26 | Type=simple 27 | WorkingDirectory=/home/zenaida/zenaida/ 28 | ExecStart=/bin/sh -c "/home/zenaida/zenaida/venv/bin/python /home/zenaida/zenaida/src/manage.py background_worker 1>>/home/zenaida/logs/background_worker 2>/home/zenaida/logs/background_worker.err" 29 | 30 | [Install] 31 | WantedBy=multi-user.target 32 | -------------------------------------------------------------------------------- /etc/systemd/system/zenaida-btcpay.service.example: -------------------------------------------------------------------------------- 1 | # Zenaida BTCPay verification systemd service configuration. 2 | # 3 | # Copy and modify `zenaida-btcpay.service` file to your local systemd folder to enable the service: 4 | # 5 | # mkdir -p /home/zenaida/.config/systemd/user/ 6 | # cd /home/zenaida/zenaida/ 7 | # cp etc/systemd/system/zenaida-btcpay.service.example /home/zenaida/.config/systemd/user/zenaida-btcpay.service 8 | # systemctl --user enable zenaida-btcpay.service 9 | # 10 | # 11 | # To start Zenaida Poll service run this command: 12 | # 13 | # systemctl --user start zenaida-btcpay.service 14 | # 15 | # 16 | # You can always check current situation with: 17 | # 18 | # systemctl --user status zenaida-btcpay.service 19 | # 20 | 21 | [Unit] 22 | Description=ZenaidaBTCPayVerify 23 | After=network.target 24 | 25 | [Service] 26 | Type=simple 27 | WorkingDirectory=/home/zenaida/zenaida/ 28 | ExecStart=/bin/sh -c "/home/zenaida/zenaida/venv/bin/python /home/zenaida/zenaida/src/manage.py btcpay_verify 1>>/home/zenaida/logs/btcpay 2>/home/zenaida/logs/btcpay.err" 29 | 30 | [Install] 31 | WantedBy=multi-user.target 32 | -------------------------------------------------------------------------------- /etc/systemd/system/zenaida-gate-health.path.example: -------------------------------------------------------------------------------- 1 | # Zenaida Gate health file configuration. 2 | # 3 | # This file to be used as a single point of true about current EPP Gate connection status. 4 | # When Python code detects error/failure in response from CoCCA server and recognize "dropped" connection 5 | # it will append a new line to the /home/zenaida/health file and this way report about the issue. 6 | # Another service "zenaida-gate-watcher" suppose to be triggered and will restart the main "zenaida-gate" service. 7 | # 8 | # First make link in your global systemd folder to activate the service: 9 | # 10 | # mkdir -p /home/zenaida/.config/systemd/user/ 11 | # cd /home/zenaida/zenaida/ 12 | # cp etc/systemd/system/zenaida-gate-health.path.example /home/zenaida/.config/systemd/user/zenaida-gate-health.path 13 | # systemctl --user enable zenaida-gate-health.path 14 | # 15 | # 16 | # Start Zenaida Gate health file monitoring by executing this command: 17 | # 18 | # systemctl --user start zenaida-gate-health.path 19 | # 20 | # 21 | # You can always check current situation with: 22 | # 23 | # systemctl --user status zenaida-gate-health.path 24 | # 25 | 26 | 27 | [Path] 28 | PathModified=/home/zenaida/health 29 | Unit=zenaida-gate-watcher.service 30 | 31 | [Install] 32 | WantedBy=multi-user.target 33 | -------------------------------------------------------------------------------- /etc/systemd/system/zenaida-gate-watcher.service.example: -------------------------------------------------------------------------------- 1 | # Zenaida Gate wather service configuration. 2 | # 3 | # Watcher service suppose to be looking at `/home/zenaida/health` file and be triggered every time that file modified. 4 | # Then it must restart the main "zenaida-gate" service right away. 5 | # 6 | # Copy and modify `zenaida-gate-watcher.service` file to your local systemd folder to enable the service: 7 | # 8 | # mkdir -p /home/zenaida/.config/systemd/user/ 9 | # cd /home/zenaida/zenaida/ 10 | # cp etc/systemd/system/zenaida-gate-watcher.service.example /home/zenaida/.config/systemd/user/zenaida-gate-watcher.service 11 | # systemctl --user enable zenaida-gate-watcher.service 12 | # 13 | # 14 | # To start Zenaida Gate watcher service run such command: 15 | # 16 | # systemctl --user start zenaida-gate-watcher.service 17 | # 18 | # 19 | # You can always check current situation with: 20 | # 21 | # systemctl --user status zenaida-gate-watcher.service 22 | # 23 | 24 | 25 | [Unit] 26 | Description=ZenaidaGateWatcher 27 | After=network.target 28 | 29 | [Service] 30 | Type=oneshot 31 | ExecStart=/bin/systemctl --user restart zenaida-gate.service 32 | 33 | [Install] 34 | WantedBy=multi-user.target 35 | -------------------------------------------------------------------------------- /etc/systemd/system/zenaida-gate.service.example: -------------------------------------------------------------------------------- 1 | # Zenaida Gate systemd service configuration. 2 | # 3 | # Copy and modify `zenaida-gate.service` file to your local systemd folder to enable the service: 4 | # 5 | # mkdir -p /home/zenaida/.config/systemd/user/ 6 | # cd /home/zenaida/zenaida/ 7 | # cp etc/systemd/system/zenaida-gate.service.example /home/zenaida/.config/systemd/user/zenaida-gate.service 8 | # systemctl --user enable zenaida-gate.service 9 | # 10 | # 11 | # To start Zenaida Gate service run this command: 12 | # 13 | # systemctl --user start zenaida-gate.service 14 | # 15 | # 16 | # You can always check current situation with: 17 | # 18 | # systemctl --user status zenaida-gate.service 19 | # 20 | 21 | [Unit] 22 | Description=ZenaidaGate 23 | After=network.target 24 | 25 | [Service] 26 | Type=simple 27 | WorkingDirectory=/home/zenaida/zenaida/ 28 | ExecStart=/home/zenaida/zenaida/venv/bin/epp-gate --verbose --reconnect --epp=/home/zenaida/keys/epp_credentials.txt --rabbitmq=/home/zenaida/keys/rabbitmq_gate_credentials.txt --queue=epp_rpc_messages 1>>/home/zenaida/logs/gate 2>>/home/zenaida/logs/gate 29 | 30 | [Install] 31 | WantedBy=multi-user.target 32 | -------------------------------------------------------------------------------- /etc/systemd/system/zenaida-notifications.service.example: -------------------------------------------------------------------------------- 1 | # Zenaida service to send email/sms notifications to customers. 2 | # 3 | # Copy and modify `zenaida-notifications.service` file to your local systemd folder to enable the service: 4 | # 5 | # mkdir -p /home/zenaida/.config/systemd/user/ 6 | # cd /home/zenaida/zenaida/ 7 | # cp etc/systemd/system/zenaida-notifications.service.example /home/zenaida/.config/systemd/user/zenaida-notifications.service 8 | # systemctl --user enable zenaida-notifications.service 9 | # 10 | # 11 | # To start Zenaida notifications service run this command: 12 | # 13 | # systemctl --user start zenaida-notifications.service 14 | # 15 | # 16 | # You can always check current situation with: 17 | # 18 | # systemctl --user status zenaida-notifications.service 19 | # 20 | 21 | [Unit] 22 | Description=ZenaidaNotifications 23 | After=network.target 24 | 25 | [Service] 26 | Type=simple 27 | WorkingDirectory=/home/zenaida/zenaida/ 28 | ExecStart=/bin/sh -c "/home/zenaida/zenaida/venv/bin/python /home/zenaida/zenaida/src/manage.py process_notifications 1>>/home/zenaida/logs/notifications 2>>/home/zenaida/logs/notifications" 29 | 30 | [Install] 31 | WantedBy=multi-user.target 32 | -------------------------------------------------------------------------------- /etc/systemd/system/zenaida-poll.service.example: -------------------------------------------------------------------------------- 1 | # Zenaida Poll systemd service configuration. 2 | # 3 | # Copy and modify `zenaida-poll.service` file to your local systemd folder to enable the service: 4 | # 5 | # mkdir -p /home/zenaida/.config/systemd/user/ 6 | # cd /home/zenaida/zenaida/ 7 | # cp etc/systemd/system/zenaida-poll.service.example /home/zenaida/.config/systemd/user/zenaida-poll.service 8 | # systemctl --user enable zenaida-poll.service 9 | # 10 | # 11 | # To start Zenaida Poll service run this command: 12 | # 13 | # systemctl --user start zenaida-poll.service 14 | # 15 | # 16 | # You can always check current situation with: 17 | # 18 | # systemctl --user status zenaida-poll.service 19 | # 20 | 21 | [Unit] 22 | Description=ZenaidaPoll 23 | After=network.target 24 | 25 | [Service] 26 | Type=simple 27 | WorkingDirectory=/home/zenaida/zenaida/ 28 | ExecStart=/bin/sh -c "/home/zenaida/zenaida/venv/bin/python /home/zenaida/zenaida/src/manage.py epp_poll 1>>/home/zenaida/logs/poll 2>>/home/zenaida/logs/poll" 29 | 30 | [Install] 31 | WantedBy=multi-user.target 32 | -------------------------------------------------------------------------------- /etc/uwsgi/emperor.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | emperor = /home/zenaida/zenaida/etc/uwsgi/vassals 3 | uid = www-data 4 | gid = www-data 5 | plugins = python38 6 | logger = file:/home/zenaida/logs/uwsgi-emperor 7 | -------------------------------------------------------------------------------- /etc/uwsgi/local.uwsgi.ini: -------------------------------------------------------------------------------- 1 | # Zenaida local testing config 2 | 3 | [uwsgi] 4 | logto = /tmp/uwsgi 5 | http = 0.0.0.0:8080 6 | http-to = /tmp/uwsgi.sock 7 | master = true 8 | module = wsgi:application 9 | chdir = src/ 10 | base_dir = %D/.. 11 | name = zenaida 12 | processes = 1 13 | threads = 2 14 | chmod-socket = 664 15 | vacuum = true 16 | die-on-term = true 17 | # stats = 127.0.0.1:9191 18 | -------------------------------------------------------------------------------- /etc/uwsgi/vassals/zenaida.ini: -------------------------------------------------------------------------------- 1 | # Zenaida uwsgi vassal live config 2 | 3 | [uwsgi] 4 | logger = file:/home/zenaida/logs/uwsgi-zenaida 5 | chdir = /home/zenaida/zenaida/src/ 6 | module = wsgi:application 7 | socket = 127.0.0.1:12321 8 | master = true 9 | base_dir = %D/.. 10 | name = zenaida 11 | processes = 1 12 | threads = 2 13 | chmod-socket = 664 14 | vacuum = true 15 | die-on-term = true 16 | # stats = 127.0.0.1:9191 17 | -------------------------------------------------------------------------------- /graph_models.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/graph_models.png -------------------------------------------------------------------------------- /opt/cocca-8/make_ssl_cert: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # How to use: 4 | # sudo ./make_ssl_cert 5 | 6 | echo " -- Stop COCCA -- " 7 | sudo /opt/cocca-8/ctlscript.sh stop 8 | 9 | echo " -- Cleaning -- " 10 | sudo rm -rf request.csr 11 | sudo rm -rf *.pem 12 | 13 | echo " -- Delete Keystore -- " 14 | sudo rm -rf epp.zenaida.ai.keystore 15 | 16 | echo " -- Recreate Keystore -- " 17 | sudo /opt/cocca-8/java/bin/keytool -genkey -noprompt -alias cocca -dname "CN=epp.zenaida.ai, OU=Zenaida, O=DataHaven.Net, L=TheValley, S=Anguilla, C=AI" -keystore epp.zenaida.ai.keystore -storepass "$1" -KeySize 2048 -keypass "$1" -keyalg RSA 18 | sudo /opt/cocca-8/java/bin/keytool -list -keystore epp.zenaida.ai.keystore -v -storepass "$1" > key.check 19 | 20 | echo " -- Build CSR -- " 21 | sudo /opt/cocca-8/java/bin/keytool -certreq -alias cocca -file request.csr -keystore epp.zenaida.ai.keystore -storepass "$1" 22 | 23 | echo " -- Request Certificate -- " 24 | sudo certbot certonly --csr ./request.csr --standalone 25 | 26 | echo " -- Import Certificate to the Keystore -- " 27 | sudo /opt/cocca-8/java/bin/keytool -import -trustcacerts -alias cocca -file 0001_chain.pem -keystore epp.zenaida.ai.keystore -storepass "$1" 28 | 29 | echo " -- Apply new Keystore -- " 30 | sudo mv -v epp.zenaida.ai.keystore /opt/cocca-8/keys/ 31 | 32 | echo " -- Start COCCA again -- " 33 | sudo /opt/cocca-8/ctlscript.sh start 34 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE=main.settings -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.8.1 2 | backports.zoneinfo==0.2.1 3 | beautifulsoup4==4.12.3 4 | btcpay-python==1.3.0 5 | certifi==2024.6.2 6 | charset-normalizer==3.3.2 7 | Django==3.2.25 8 | django-admin-ip-restrictor==2.2.0 9 | django-bootstrap4==24.1 10 | django-dbbackup==4.1.0 11 | django-extensions==3.2.3 12 | django-formtools==2.5.1 13 | django-grappelli==4.0.1 14 | django-ipware==3.0.7 15 | django-nested-admin==4.0.2 16 | django-otp==1.5.0 17 | django-ranged-response==0.2.0 18 | django-simple-captcha==0.5.20 19 | djangorestframework==3.15.1 20 | ecdsa==0.19.0 21 | fqn-decorators==2.0.2 22 | idna==3.7 23 | lxml==5.2.2 24 | pdfkit==1.0.0 25 | pika==1.3.2 26 | pillow==10.3.0 27 | pkgversion==3.0.2 28 | psycopg2-binary==2.9.9 29 | py==1.11.0 30 | pypng==0.20220715.0 31 | python-dateutil==2.9.0.post0 32 | python-memcached==1.62 33 | python-monkey-business==1.0.0 34 | pytz==2024.1 35 | Pygments==2.19.1 36 | qrcode==7.4.2 37 | requests==2.32.3 38 | sentry-sdk==2.20.0 39 | six==1.16.0 40 | soupsieve==2.5 41 | sqlparse==0.5.0 42 | transliterate==1.10.2 43 | typing_extensions==4.12.2 44 | urllib3==2.2.1 45 | uWSGI==2.0.26 46 | https://github.com/datahaven-net/epp-python-client/archive/master.zip 47 | -------------------------------------------------------------------------------- /requirements/requirements-base.txt: -------------------------------------------------------------------------------- 1 | # Base requirements 2 | pkgversion 3 | pika 4 | transliterate 5 | fqn-decorators 6 | requests 7 | psycopg2-binary 8 | uwsgi 9 | Django 10 | django-admin-ip-restrictor 11 | django-bootstrap4 12 | django-nested-admin 13 | django-grappelli 14 | django-extensions 15 | django_otp 16 | django-formtools 17 | django-dbbackup 18 | django-simple-captcha 19 | djangorestframework 20 | qrcode 21 | pdfkit 22 | btcpay-python 23 | python-memcached 24 | python-dateutil 25 | Pygments 26 | lxml 27 | beautifulsoup4 28 | https://github.com/datahaven-net/epp-python-client/archive/master.zip 29 | -------------------------------------------------------------------------------- /requirements/requirements-testing.txt: -------------------------------------------------------------------------------- 1 | # tox 2 | isort 3 | flake8 4 | mock 5 | django-debug-toolbar 6 | 7 | # PyTest for running the tests. 8 | importlib.metadata==2.0.0 9 | pytest 10 | pytest-mock 11 | pytest-cov 12 | pytest-django 13 | 14 | # docs 15 | sphinx 16 | # sphinxcontrib-plantuml 17 | sphinx_rtd_theme 18 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = build,.git,**migrations/,src/main/settings.py 3 | ignore = E902 4 | max-line-length = 119 5 | 6 | [isort] 7 | combine_as_imports = true 8 | default_section = THIRDPARTY 9 | from_first = false 10 | include_trailing_comma = true 11 | length_sort = false 12 | multi_line_output = 5 13 | not_skip = __init__.py 14 | order_by_type = true 15 | known_django = django 16 | sections = FUTURE,STDLIB,DJANGO,THIRDPARTY,OTHER,FIRSTPARTY,LOCALFOLDER 17 | use_parenthesis = true 18 | line_length = 119 19 | 20 | [wheel] 21 | universal = 1 22 | 23 | [coverage:run] 24 | omit = 25 | src/tests/* 26 | src/billing/migrations/* 27 | src/back/migrations/* 28 | src/accounts/migrations/* 29 | src/wsgi.py 30 | src/manage.py 31 | src/main/confdocs.py 32 | -------------------------------------------------------------------------------- /setup_gen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from pkgversion import list_requirements, pep440_version, write_setup_py 4 | from setuptools import find_packages 5 | 6 | write_setup_py( 7 | name='zenaida', 8 | version=pep440_version(), 9 | description="Open source domain registry system built on top of EPP protocol", 10 | long_description=open('README.md').read(), 11 | author="Veselin Penev", 12 | author_email='penev.veselin@gmail.com', 13 | url='https://github.com/datahaven-net/zenaida', 14 | install_requires=list_requirements('requirements/requirements-base.txt'), 15 | packages=find_packages(), 16 | tests_require=['tox'], 17 | include_package_data=True, 18 | zip_safe=False, 19 | classifiers=[ 20 | 'Environment :: Web Environment', 21 | 'Operating System :: OS Independent', 22 | 'Programming Language :: Python', 23 | 'Programming Language :: Python :: 3', 24 | 'Programming Language :: Python :: 3.8', 25 | 'Topic :: Internet :: WWW/HTTP', 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/__init__.py -------------------------------------------------------------------------------- /src/accounts/__init__.py: -------------------------------------------------------------------------------- 1 | # from accounts.models.account import Account 2 | 3 | # default_app_config = 'accounts.apps.AccountsConfig' 4 | -------------------------------------------------------------------------------- /src/accounts/apps.py: -------------------------------------------------------------------------------- 1 | #from __future__ import unicode_literals 2 | # 3 | #from django.apps import AppConfig 4 | # 5 | # 6 | #class AccountsConfig(AppConfig): 7 | # name = 'accounts' 8 | # 9 | # def ready(self): 10 | # """Location for package configurations""" 11 | # return True 12 | -------------------------------------------------------------------------------- /src/accounts/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/accounts/management/__init__.py -------------------------------------------------------------------------------- /src/accounts/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/accounts/management/commands/__init__.py -------------------------------------------------------------------------------- /src/accounts/management/commands/process_notifications.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from accounts import notifications 4 | 5 | 6 | class Command(BaseCommand): 7 | 8 | help = 'Sending Email/SMS notifications from the queue' 9 | 10 | def handle(self, *args, **options): 11 | notifications.process_notifications_queue() 12 | -------------------------------------------------------------------------------- /src/accounts/migrations/0002_auto_20190203_0040.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-02-03 00:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('accounts', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='account', 15 | name='balance', 16 | field=models.FloatField(default=0.0), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/accounts/migrations/0004_auto_20200504_0830.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.10 on 2020-05-04 08:30 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('accounts', '0003_notification'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='notification', 15 | name='subject', 16 | field=models.CharField(choices=[('domain_expiring', 'DOMAIN EXPIRING'), ('domain_expire_soon', 'DOMAIN EXPIRE SOON'), ('domain_restored', 'DOMAIN RESTORED'), ('domain_transferred', 'DOMAIN TRANSFERRED'), ('domain_activated', 'DOMAIN ACTIVATED'), ('domain_deactivated', 'DOMAIN DEACTIVATED'), ('domain_renewed', 'DOMAIN RENEWED'), ('low_balance', 'LOW BALANCE')], max_length=32), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/accounts/migrations/0005_auto_20201219_1451.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.4 on 2020-12-19 14:51 2 | 3 | import django.core.serializers.json 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('accounts', '0004_auto_20200504_0830'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='notification', 16 | name='details', 17 | field=models.JSONField(encoder=django.core.serializers.json.DjangoJSONEncoder, null=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /src/accounts/migrations/0006_account_notes.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.4 on 2021-01-10 11:54 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('accounts', '0005_auto_20201219_1451'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='account', 15 | name='notes', 16 | field=models.TextField(blank=True, default=None, help_text='any note regarding this account such as manual balance changes.', null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/accounts/migrations/0007_alter_notification_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2024-08-23 13:09 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('accounts', '0006_account_notes'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='notification', 15 | name='status', 16 | field=models.CharField(choices=[('started', 'STARTED'), ('sent', 'SENT'), ('failed', 'FAILED'), ('skipped', 'SKIPPED')], default='started', max_length=10), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/accounts/migrations/0008_account_is_approved.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2024-11-09 14:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('accounts', '0007_alter_notification_status'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='account', 15 | name='is_approved', 16 | field=models.BooleanField(default=True, help_text='indicate if the user was approved by the site administrator', verbose_name='approved'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/accounts/migrations/0009_alter_notification_subject.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2025-01-01 16:14 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('accounts', '0008_account_is_approved'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='notification', 15 | name='subject', 16 | field=models.CharField(choices=[('domain_expiring', 'DOMAIN EXPIRING'), ('domain_expire_soon', 'DOMAIN EXPIRE SOON'), ('domain_restored', 'DOMAIN RESTORED'), ('domain_transferred', 'DOMAIN TRANSFERRED'), ('domain_activated', 'DOMAIN ACTIVATED'), ('domain_deactivated', 'DOMAIN DEACTIVATED'), ('domain_renewed', 'DOMAIN RENEWED'), ('low_balance', 'LOW BALANCE'), ('account_approved', 'ACCOUNT_APPROVED')], max_length=32), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/accounts/migrations/0010_alter_notification_subject.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2025-03-02 10:53 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('accounts', '0009_alter_notification_subject'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='notification', 15 | name='subject', 16 | field=models.CharField(choices=[('domain_expiring', 'DOMAIN EXPIRING'), ('domain_expire_soon', 'DOMAIN EXPIRE SOON'), ('domain_restored', 'DOMAIN RESTORED'), ('domain_transferred', 'DOMAIN TRANSFERRED'), ('domain_activated', 'DOMAIN ACTIVATED'), ('domain_deactivated', 'DOMAIN DEACTIVATED'), ('domain_renewed', 'DOMAIN RENEWED'), ('domain_deleted', 'DOMAIN DELETED'), ('low_balance', 'LOW BALANCE'), ('low_balance_back_end_renew', 'LOW BALANCE BACK END RENEW'), ('account_approved', 'ACCOUNT_APPROVED')], max_length=32), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/accounts/migrations/0011_alter_notification_subject.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2025-05-29 17:07 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('accounts', '0010_alter_notification_subject'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='notification', 15 | name='subject', 16 | field=models.CharField(choices=[('domain_expiring', 'DOMAIN EXPIRING'), ('domain_expire_soon', 'DOMAIN EXPIRE SOON'), ('domain_expire_in_5_days', 'DOMAIN EXPIRE IN 5 DAYS'), ('domain_expire_in_3_days', 'DOMAIN EXPIRE IN 3 DAYS'), ('domain_expire_in_1_day', 'DOMAIN EXPIRE IN 1 DAY'), ('domain_expired', 'DOMAIN EXPIRED'), ('domain_restored', 'DOMAIN RESTORED'), ('domain_transferred', 'DOMAIN TRANSFERRED'), ('domain_activated', 'DOMAIN ACTIVATED'), ('domain_deactivated', 'DOMAIN DEACTIVATED'), ('domain_renewed', 'DOMAIN RENEWED'), ('domain_deleted', 'DOMAIN DELETED'), ('low_balance', 'LOW BALANCE'), ('low_balance_back_end_renew', 'LOW BALANCE BACK END RENEW'), ('account_approved', 'ACCOUNT_APPROVED')], max_length=32), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/accounts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/accounts/migrations/__init__.py -------------------------------------------------------------------------------- /src/accounts/models/__init__.py: -------------------------------------------------------------------------------- 1 | from accounts.models.account import Account 2 | from accounts.models.notification import Notification 3 | -------------------------------------------------------------------------------- /src/accounts/models/activation.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from accounts.models.account import Account 4 | 5 | 6 | class Activation(models.Model): 7 | 8 | created_at = models.DateTimeField(auto_now_add=True) 9 | code = models.CharField(max_length=20) 10 | account = models.ForeignKey(Account, on_delete=models.CASCADE, related_name='activations') 11 | 12 | def __str__(self): 13 | return 'Activation({})'.format(self.account.email) 14 | -------------------------------------------------------------------------------- /src/accounts/templates/accounts/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/container_sm_4.html' %} 2 | 3 | {% block content %} 4 | 5 | 6 |

Log in

7 | 8 |
9 | {% csrf_token %} 10 | 11 | {% bootstrap_form form %} 12 | 13 |

14 | 15 | 16 |
17 | 18 | 19 |
20 | 21 | 26 | 27 | 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /src/accounts/templates/accounts/logout.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/container_sm_4.html' %} 2 | 3 | {% block content %} 4 | 5 | 6 |

Logged out

7 | 8 |

9 | We wish you a good time and see you again. 10 |

11 | 12 |

13 | Log in again 14 |

15 | 16 | 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /src/accounts/templates/accounts/password_change_done.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/index.html' %} 2 | 3 | {% block main_content %} 4 | 5 | 6 |

Password change

7 | 8 |
9 | Your password was changed. 10 |
11 | 12 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /src/accounts/templates/accounts/password_change_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/container_sm_4.html' %} 2 | 3 | {% block content %} 4 | 5 | 6 |

Password change

7 | 8 |
9 | {% csrf_token %} 10 | 11 | {% bootstrap_form form %} 12 | 13 | 14 |
15 | 16 | 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /src/accounts/templates/accounts/password_reset.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/container_sm_4.html' %} 2 | 3 | {% block content %} 4 | 5 | 6 |

Password reset

7 | 8 |
9 | {% csrf_token %} 10 | 11 | {% bootstrap_form form %} 12 | 13 | 14 |
15 | 16 | 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /src/accounts/templates/accounts/password_reset_complete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/index.html' %} 2 | 3 | {% block main_content %} 4 | 5 | 6 |

Password reset

7 | 8 |
9 | Your password has been set. You may go ahead and log in now. 10 |
11 | 12 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /src/accounts/templates/accounts/password_reset_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/container_sm_4.html' %} 2 | 3 | {% block content %} 4 | 5 | 6 |

Password reset

7 | 8 | {% if validlink %} 9 | 10 |

11 | Please enter your new password twice so we can verify you typed it in correctly. 12 |

13 | 14 |
15 | {% csrf_token %} 16 | 17 | {% bootstrap_form form %} 18 | 19 | 20 |
21 | 22 | {% else %} 23 | 24 |
25 | The password reset link was invalid, possibly because it has already been used. Please request a new 26 | password reset. 27 |
28 | 29 | {% endif %} 30 | 31 | 32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /src/accounts/templates/accounts/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/index.html' %} 2 | 3 | {% block main_content %} 4 | 5 | 6 |

Password reset

7 | 8 |
9 | We've emailed you instructions for setting your password, if an account exists with the email you entered. 10 | You should receive them shortly. 11 |
12 | 13 |
14 | If you don't receive an email, please make sure you've entered the address you registered with, 15 | and check your spam folder. 16 |
17 | 18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /src/accounts/templates/accounts/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/container_sm_4.html' %} 2 | 3 | {% block content %} 4 | 5 | 6 |

Create an account

7 | 8 |
9 | {% csrf_token %} 10 | 11 | {% bootstrap_form form %} 12 | 13 |

14 | 15 | 16 |
17 | 18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /src/accounts/users.py: -------------------------------------------------------------------------------- 1 | from accounts.models import Account 2 | 3 | 4 | def list_all_users_by_date(year, month=None): 5 | if year and month: 6 | return Account.users.filter(date_joined__year=year, date_joined__month=month) 7 | else: 8 | return Account.users.filter(date_joined__year=year) 9 | -------------------------------------------------------------------------------- /src/automats/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/automats/__init__.py -------------------------------------------------------------------------------- /src/back/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/back/__init__.py -------------------------------------------------------------------------------- /src/back/apps.py: -------------------------------------------------------------------------------- 1 | #from __future__ import unicode_literals 2 | # 3 | #from django.apps import AppConfig 4 | # 5 | # 6 | #class BackConfig(AppConfig): 7 | # name = 'back' 8 | # 9 | # def ready(self): 10 | # """Location for package configurations""" 11 | # return True 12 | -------------------------------------------------------------------------------- /src/back/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/back/management/__init__.py -------------------------------------------------------------------------------- /src/back/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/back/management/commands/__init__.py -------------------------------------------------------------------------------- /src/back/management/commands/cleanup_unused_contacts.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Count 2 | from django.core.management.base import BaseCommand 3 | 4 | from back.models.contact import Contact, Registrant 5 | 6 | 7 | class Command(BaseCommand): 8 | """ 9 | Usage: 10 | 11 | ./venv/bin/python src/manage.py cleanup_unused_contacts 12 | 13 | """ 14 | 15 | help = 'Removes all Contact and Registrant objects from the DB which are not attached to any domains' 16 | 17 | def handle(self, *args, **options): 18 | for cont in Contact.contacts.annotate( 19 | all_domains_count=Count("admin_domains", distinct=True) + Count("billing_domains", distinct=True) + Count("tech_domains", distinct=True) 20 | ).filter(all_domains_count=0): 21 | self.stdout.write('erasing %r\n' % cont) 22 | cont.delete() 23 | for reg in Registrant.registrants.annotate(all_domains_count=Count("registrant_domains", distinct=True)).filter(all_domains_count=0): 24 | self.stdout.write('erasing %r\n' % reg) 25 | reg.delete() 26 | self.stdout.write(self.style.SUCCESS('Done')) 27 | -------------------------------------------------------------------------------- /src/back/management/commands/epp_poll.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from zen import zpoll 4 | 5 | 6 | class Command(BaseCommand): 7 | 8 | help = 'Starts background process to "listen" EPP notifications from the back-end' 9 | 10 | def handle(self, *args, **options): 11 | zpoll.main() 12 | -------------------------------------------------------------------------------- /src/back/management/commands/list_domains.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | from back.models.domain import Domain 4 | 5 | 6 | class Command(BaseCommand): 7 | 8 | help = 'Print all known domain names' 9 | 10 | def handle(self, *args, **options): 11 | for domain_obj in Domain.domains.all(): 12 | print(domain_obj.name) 13 | -------------------------------------------------------------------------------- /src/back/migrations/0002_auto_20181119_1117.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2018-11-19 11:17 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='contact', 15 | name='epp_id', 16 | field=models.CharField(blank=True, default=None, max_length=32, null=True, unique=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='domain', 20 | name='epp_id', 21 | field=models.CharField(blank=True, default=None, max_length=32, null=True, unique=True), 22 | ), 23 | migrations.AlterField( 24 | model_name='registrar', 25 | name='epp_id', 26 | field=models.CharField(blank=True, default=None, max_length=32, unique=True), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /src/back/migrations/0003_domain_epp_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-01-20 15:49 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0002_auto_20181119_1117'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='domain', 15 | name='epp_status', 16 | field=models.CharField(choices=[('EPP_STATUS_ACTIVE', 'ACTIVE'), ('EPP_STATUS_INACTIVE', 'INACTIVE'), ('EPP_STATUS_DEACTIVATED', 'DEACTIVATED'), ('EPP_STATUS_CLIENT_HOLD', 'CLIENT HOLD'), ('EPP_STATUS_SERVER_HOLD', 'SERVER HOLD'), ('EPP_STATUS_TO_BE_DELETED', 'TO BE DELETED'), ('EPP_STATUS_TO_BE_RESTORED', 'TO BE RESTORED'), ('EPP_STATUS_UNKNOWN', 'UNKNOWN(ERROR)')], default='INACTIVE', max_length=32), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/back/migrations/0004_auto_20190120_1936.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-01-20 19:36 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0003_domain_epp_status'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name='domain', 15 | options={'base_manager_name': 'domains', 'default_manager_name': 'domains', 'ordering': ['expiry_date']}, 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /src/back/migrations/0008_auto_20190209_2011.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-02-09 20:11 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0007_auto_20190209_1420'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='domain', 15 | name='epp_status', 16 | field=models.CharField(choices=[('inactive', 'INACTIVE'), ('deactivated', 'DEACTIVATED'), ('client_hold', 'CLIENT HOLD'), ('server_hold', 'SERVER HOLD'), ('to_be_deleted', 'TO BE DELETED'), ('to_be_restored', 'TO BE RESTORED'), ('unknown', 'UNKNOWN'), ('active', 'ACTIVE')], default='inactive', max_length=32), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/back/migrations/0009_auto_20190224_1822.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-02-24 18:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0008_auto_20190209_2011'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='contact', 15 | name='epp_id', 16 | field=models.CharField(default=None, max_length=32, null=True, unique=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='domain', 20 | name='epp_id', 21 | field=models.CharField(default=None, max_length=32, null=True, unique=True), 22 | ), 23 | migrations.AlterField( 24 | model_name='registrant', 25 | name='epp_id', 26 | field=models.CharField(default=None, max_length=32, null=True, unique=True), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /src/back/migrations/0010_auto_20190224_1934.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-02-24 19:34 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0009_auto_20190224_1822'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='contact', 15 | name='epp_id', 16 | field=models.CharField(blank=True, default=None, max_length=32, null=True, unique=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='domain', 20 | name='epp_id', 21 | field=models.CharField(blank=True, default=None, max_length=32, null=True, unique=True), 22 | ), 23 | migrations.AlterField( 24 | model_name='registrant', 25 | name='epp_id', 26 | field=models.CharField(blank=True, default=None, max_length=32, null=True, unique=True), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /src/back/migrations/0011_auto_20190302_1832.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-03-02 18:32 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('back', '0010_auto_20190224_1934'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='domain', 16 | name='contact_admin', 17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='admin_domains', to='back.Contact', verbose_name='Administrative'), 18 | ), 19 | migrations.AlterField( 20 | model_name='domain', 21 | name='contact_billing', 22 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='billing_domains', to='back.Contact', verbose_name='Billing'), 23 | ), 24 | migrations.AlterField( 25 | model_name='domain', 26 | name='contact_tech', 27 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='tech_domains', to='back.Contact', verbose_name='Technical'), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /src/back/migrations/0012_domain_epp_statuses.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2019-04-25 10:49 2 | 3 | import django.contrib.postgres.fields.jsonb 4 | import django.core.serializers.json 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('back', '0011_auto_20190302_1832'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='domain', 17 | name='epp_statuses', 18 | field=django.contrib.postgres.fields.jsonb.JSONField(encoder=django.core.serializers.json.DjangoJSONEncoder, null=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /src/back/migrations/0013_auto_20190425_1053.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2019-04-25 10:53 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0012_domain_epp_statuses'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='domain', 15 | name='epp_status', 16 | ), 17 | migrations.AddField( 18 | model_name='domain', 19 | name='status', 20 | field=models.CharField(choices=[('inactive', 'INACTIVE'), ('to_be_deleted', 'TO BE DELETED'), ('to_be_restored', 'TO BE RESTORED'), ('unknown', 'UNKNOWN'), ('active', 'ACTIVE')], default='inactive', max_length=32), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /src/back/migrations/0014_auto_20190509_1034.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2019-05-09 10:34 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0013_auto_20190425_1053'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='domain', 15 | name='status', 16 | field=models.CharField(choices=[('inactive', 'INACTIVE'), ('to_be_deleted', 'TO BE DELETED'), ('to_be_restored', 'TO BE RESTORED'), ('blocked', 'BLOCKED'), ('unknown', 'UNKNOWN'), ('active', 'ACTIVE')], default='inactive', max_length=32), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/back/migrations/0015_auto_20190523_0940.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2019-05-23 09:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0014_auto_20190509_1034'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='domain', 15 | name='create_date', 16 | field=models.DateTimeField(blank=True, default=None, null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='domain', 20 | name='expiry_date', 21 | field=models.DateTimeField(blank=True, default=None, null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /src/back/migrations/0016_auto_20190602_1419.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2019-06-02 14:19 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('back', '0015_auto_20190523_0940'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='domain', 16 | name='contact_admin', 17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='admin_domains', to='back.Contact', verbose_name='Administrative contact'), 18 | ), 19 | migrations.AlterField( 20 | model_name='domain', 21 | name='contact_billing', 22 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='billing_domains', to='back.Contact', verbose_name='Billing contact'), 23 | ), 24 | migrations.AlterField( 25 | model_name='domain', 26 | name='contact_tech', 27 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='tech_domains', to='back.Contact', verbose_name='Technical contact'), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /src/back/migrations/0017_auto_20190901_1958.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-09-01 19:58 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0016_auto_20190602_1419'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='contact', 15 | name='address_postal_code', 16 | field=models.CharField(blank=True, max_length=255, verbose_name='ZIP code'), 17 | ), 18 | migrations.AlterField( 19 | model_name='contact', 20 | name='address_province', 21 | field=models.CharField(blank=True, max_length=255, verbose_name='Province'), 22 | ), 23 | migrations.AlterField( 24 | model_name='contact', 25 | name='organization_name', 26 | field=models.CharField(blank=True, max_length=255, verbose_name='Organization'), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /src/back/migrations/0018_profile_email_notifications_enabled.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-10-30 10:15 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0017_auto_20190901_1958'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='profile', 15 | name='email_notifications_enabled', 16 | field=models.BooleanField(default=True, verbose_name='Email notifications'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/back/migrations/0019_auto_20191106_0838.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-11-06 08:38 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0018_profile_email_notifications_enabled'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='profile', 15 | name='email_notifications_enabled', 16 | field=models.BooleanField(default=True, verbose_name='Email notifications enabled'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/back/migrations/0020_auto_20200226_0850.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.9 on 2020-02-26 08:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0019_auto_20191106_0838'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='domain', 15 | name='status', 16 | field=models.CharField(choices=[('inactive', 'INACTIVE'), ('to_be_deleted', 'TO BE DELETED'), ('to_be_restored', 'TO BE RESTORED'), ('suspended', 'SUSPENDED'), ('blocked', 'BLOCKED'), ('unknown', 'UNKNOWN'), ('active', 'ACTIVE')], default='inactive', max_length=32), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/back/migrations/0021_auto_20200229_1043.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.10 on 2020-02-29 10:43 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('back', '0020_auto_20200226_0850'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='contact', 16 | name='contact_fax', 17 | field=models.CharField(blank=True, max_length=17, null=True, validators=[django.core.validators.RegexValidator(message="Phone number must be entered in the format: '+999999999'. Up to 15 digits allowed.", regex='^\\+?1?\\d{9,15}$')], verbose_name='Fax'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /src/back/migrations/0022_profile_automatic_renewal_enabled.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.10 on 2020-02-29 20:28 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0021_auto_20200229_1043'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='profile', 15 | name='automatic_renewal_enabled', 16 | field=models.BooleanField(default=False, verbose_name='Automatically renew expiring domains'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/back/migrations/0023_auto_20200304_0925.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.10 on 2020-03-04 09:25 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0022_profile_automatic_renewal_enabled'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='domain', 15 | name='auto_renew_enabled', 16 | field=models.BooleanField(default=True, help_text='Domain will be automatically renewed 3 months before the expiration date, if you have enough funds.', verbose_name='Automatically renew'), 17 | ), 18 | migrations.AlterField( 19 | model_name='profile', 20 | name='automatic_renewal_enabled', 21 | field=models.BooleanField(default=True, help_text='Your domains will be automatically renewed 3 months before the expiration date, if you have enough funds.', verbose_name='Automatically renew expiring domains'), 22 | ), 23 | migrations.AlterField( 24 | model_name='profile', 25 | name='email_notifications_enabled', 26 | field=models.BooleanField(default=True, help_text='Enable email notifications about expiring domains and low balance.', verbose_name='Email notifications'), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /src/back/migrations/0025_auto_20200314_1144.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.10 on 2020-03-14 11:44 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('back', '0024_auto_20200304_1005'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='domain', 16 | name='contact_admin', 17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='admin_domains', to='back.Contact', verbose_name='Administrative contact'), 18 | ), 19 | migrations.AlterField( 20 | model_name='domain', 21 | name='contact_billing', 22 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='billing_domains', to='back.Contact', verbose_name='Billing contact'), 23 | ), 24 | migrations.AlterField( 25 | model_name='domain', 26 | name='contact_tech', 27 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tech_domains', to='back.Contact', verbose_name='Technical contact'), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /src/back/migrations/0026_auto_20200504_0830.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.10 on 2020-05-04 08:30 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0025_auto_20200314_1144'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='domain', 15 | name='auto_renew_enabled', 16 | field=models.BooleanField(default=True, help_text='Domain will be automatically renewed 3 months before the expiration date, if you have enough funds. Account balance will be automatically deducted.', verbose_name='Automatically renew'), 17 | ), 18 | migrations.AlterField( 19 | model_name='profile', 20 | name='automatic_renewal_enabled', 21 | field=models.BooleanField(default=False, help_text='Your domains will be automatically renewed 3 months before the expiration date, if you have enough funds. Account balance will be automatically deducted.', verbose_name='Automatically renew expiring domains'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /src/back/migrations/0027_auto_20200701_1835.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-07-01 18:35 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0026_auto_20200504_0830'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='profile', 15 | name='automatic_renewal_enabled', 16 | field=models.BooleanField(default=True, help_text='Your domains will be automatically renewed 3 months before the expiration date, if you have enough funds. Account balance will be automatically deducted.', verbose_name='Automatically renew expiring domains'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/back/migrations/0028_auto_20201219_1451.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.4 on 2020-12-19 14:51 2 | 3 | import django.core.serializers.json 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('back', '0027_auto_20200701_1835'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='domain', 16 | name='epp_statuses', 17 | field=models.JSONField(encoder=django.core.serializers.json.DjangoJSONEncoder, null=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /src/back/migrations/0029_domain_modified_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-07-08 08:33 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0028_auto_20201219_1451'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='domain', 15 | name='modified_date', 16 | field=models.DateTimeField(auto_now=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/back/migrations/0030_domain_latest_sync_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-07-08 09:12 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0029_domain_modified_date'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='domain', 15 | name='latest_sync_date', 16 | field=models.DateTimeField(blank=True, default=None, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/back/migrations/0031_auto_20250116_1500.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2025-01-16 15:00 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0030_domain_latest_sync_date'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='contact', 15 | name='epp_id', 16 | field=models.CharField(blank=True, default=None, max_length=64, null=True, unique=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='domain', 20 | name='epp_id', 21 | field=models.CharField(blank=True, default=None, max_length=64, null=True, unique=True), 22 | ), 23 | migrations.AlterField( 24 | model_name='registrant', 25 | name='epp_id', 26 | field=models.CharField(blank=True, default=None, max_length=64, null=True, unique=True), 27 | ), 28 | migrations.AlterField( 29 | model_name='registrar', 30 | name='epp_id', 31 | field=models.CharField(blank=True, default=None, max_length=64, unique=True), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /src/back/migrations/0034_backendrenew_previous_expiry_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2025-02-16 19:48 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0033_backendrenew'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='backendrenew', 15 | name='previous_expiry_date', 16 | field=models.DateTimeField(blank=True, default=None, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/back/migrations/0035_backendrenew_next_expiry_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2025-02-17 09:42 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0034_backendrenew_previous_expiry_date'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='backendrenew', 15 | name='next_expiry_date', 16 | field=models.DateTimeField(blank=True, default=None, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/back/migrations/0036_auto_20250302_1053.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2025-03-02 10:53 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0035_backendrenew_next_expiry_date'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='backendrenew', 15 | name='insufficient_balance_email_sent', 16 | field=models.BooleanField(default=False), 17 | ), 18 | migrations.AlterField( 19 | model_name='backendrenew', 20 | name='status', 21 | field=models.CharField(choices=[('started', 'Started'), ('processed', 'Processed'), ('rejected', 'Rejected')], default='started', max_length=16), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /src/back/migrations/0037_alter_domain_auto_renew_enabled.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2025-03-22 11:01 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0036_auto_20250302_1053'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='domain', 15 | name='auto_renew_enabled', 16 | field=models.BooleanField(default=True, help_text='Domain will be automatically renewed 3 months before the expiration date, if you have enough funds. Account balance will be automatically deducted.Please make sure you also enabled automatic domains renewal in your profile settings.', verbose_name='Automatically renew'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/back/migrations/0038_backendrenew_restore_order.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2025-05-22 18:26 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('billing', '0021_alter_orderitem_status'), 11 | ('back', '0037_alter_domain_auto_renew_enabled'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='backendrenew', 17 | name='restore_order', 18 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='restore_renewals', to='billing.order'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /src/back/migrations/0039_alter_domain_auto_renew_enabled.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2025-05-29 09:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('back', '0038_backendrenew_restore_order'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='domain', 15 | name='auto_renew_enabled', 16 | field=models.BooleanField(default=True, help_text='Domain will be automatically renewed 3 months before the expiration date, if you have enough funds. Account balance will be automatically deducted.', verbose_name='Automatically renew'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/back/migrations/0040_auto_20250529_1707.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2025-05-29 17:07 2 | 3 | import django.core.serializers.json 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('back', '0039_alter_domain_auto_renew_enabled'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='backendrenew', 16 | name='details', 17 | field=models.JSONField(encoder=django.core.serializers.json.DjangoJSONEncoder, null=True), 18 | ), 19 | migrations.AlterField( 20 | model_name='backendrenew', 21 | name='status', 22 | field=models.CharField(choices=[('started', 'Started'), ('processed', 'Processed'), ('rejected', 'Rejected'), ('failed', 'Failed')], default='started', max_length=16), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /src/back/migrations/0041_domain_extension_info.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2025-06-13 08:54 2 | 3 | import django.core.serializers.json 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('back', '0040_auto_20250529_1707'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='domain', 16 | name='extension_info', 17 | field=models.JSONField(encoder=django.core.serializers.json.DjangoJSONEncoder, null=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /src/back/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/back/migrations/__init__.py -------------------------------------------------------------------------------- /src/back/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/back/models/__init__.py -------------------------------------------------------------------------------- /src/back/models/registrar.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Registrar(models.Model): 5 | 6 | registrars = models.Manager() 7 | 8 | class Meta: 9 | app_label = 'back' 10 | base_manager_name = 'registrars' 11 | default_manager_name = 'registrars' 12 | 13 | # related fields: 14 | # domains -> back.models.domain.Domain 15 | 16 | epp_id = models.CharField(max_length=64, unique=True, blank=True, default=None, ) 17 | 18 | def __str__(self): 19 | return 'Registrar({})'.format(self.epp_id) 20 | 21 | def __repr__(self): 22 | return 'Registrar({})'.format(self.epp_id) 23 | -------------------------------------------------------------------------------- /src/back/models/zone.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Zone(models.Model): 5 | 6 | zones = models.Manager() 7 | 8 | class Meta: 9 | app_label = 'back' 10 | base_manager_name = 'zones' 11 | default_manager_name = 'zones' 12 | 13 | # related fields: 14 | # domains -> back.models.domain.Domain 15 | 16 | name = models.CharField(max_length=255, unique=True, ) 17 | 18 | def __str__(self): 19 | return 'Zone({})'.format(self.name.upper()) 20 | 21 | def __repr__(self): 22 | return 'Zone({})'.format(self.name.upper()) 23 | -------------------------------------------------------------------------------- /src/back/validators.py: -------------------------------------------------------------------------------- 1 | from django.core.validators import RegexValidator 2 | from django.db import models 3 | 4 | from back.constants import COUNTRIES 5 | 6 | phone_regex = RegexValidator( 7 | regex=r'^\+[0-9]{1,3}\.?[0-9]{1,14}$', 8 | message="Phone number must be entered in the format: '+123.4567890'. The number must include the country code, which is separated from the phone number (up to 14 digits allowed) by a period." 9 | ) 10 | 11 | 12 | class CountryField(models.CharField): 13 | def __init__(self, *args, **kwargs): 14 | kwargs.setdefault('max_length', 2) 15 | kwargs.setdefault('choices', COUNTRIES) 16 | 17 | super(CountryField, self).__init__(*args, **kwargs) 18 | 19 | def get_internal_type(self): 20 | return "CharField" 21 | -------------------------------------------------------------------------------- /src/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/base/__init__.py -------------------------------------------------------------------------------- /src/base/bruteforceprotection.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.core.cache import cache 4 | 5 | from base.exceptions import ExceededMaxAttemptsException 6 | 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class BruteForceProtection(object): 12 | 13 | def __init__(self, cache_key_prefix, key, max_attempts, timeout): 14 | self.cache_key = f"{cache_key_prefix}_{key}" 15 | self.max_attempts = max_attempts 16 | self.timeout = timeout 17 | self._local_value = None 18 | 19 | def read_total_attempts(self): 20 | self._local_value = cache.get(self.cache_key) 21 | logger.debug('bruteforceprotection.read_total_attempts key=%r %r', self.cache_key, self._local_value) 22 | return self._local_value if self._local_value else 0 23 | 24 | def increase_total_attempts(self): 25 | self.read_total_attempts() 26 | if not self._local_value: 27 | self._local_value = 0 28 | self._local_value += 1 29 | cache.set(self.cache_key, self._local_value, timeout=self.timeout) 30 | logger.debug('bruteforceprotection.increase_total_attempts key=%r %r', self.cache_key, self._local_value) 31 | return self._local_value 32 | 33 | def register_attempt(self): 34 | total_attempts = self.read_total_attempts() 35 | if total_attempts >= self.max_attempts: 36 | raise ExceededMaxAttemptsException 37 | self.increase_total_attempts() 38 | -------------------------------------------------------------------------------- /src/base/email.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.core.mail import EmailMultiAlternatives 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | def send_email(subject, text_content, from_email, to_email, html_content=None, ): 9 | msg = EmailMultiAlternatives(subject, text_content, from_email, to=[to_email, ], bcc=[to_email, ], cc=[to_email, ]) 10 | try: 11 | msg.send() 12 | except: 13 | logger.exception('Failed to send email') 14 | -------------------------------------------------------------------------------- /src/base/exceptions.py: -------------------------------------------------------------------------------- 1 | class ExceededMaxAttemptsException(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /src/base/mixins.py: -------------------------------------------------------------------------------- 1 | from django import shortcuts 2 | from django.contrib import messages 3 | from django.contrib.auth.decorators import login_required 4 | from django.utils.decorators import method_decorator 5 | 6 | 7 | class StaffRequiredMixin(object): 8 | @method_decorator(login_required) 9 | def dispatch(self, request, *args, **kwargs): 10 | if not request.user.is_staff: 11 | messages.error(request, 'You do not have the permission required to perform the requested operation.') 12 | return shortcuts.redirect('index') 13 | return super().dispatch(request, *args, **kwargs) 14 | -------------------------------------------------------------------------------- /src/base/push_notifications.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from django.conf import settings 3 | 4 | 5 | class PushNotificationService(object): 6 | def __init__(self, notification_message): 7 | self.notification_message = notification_message 8 | 9 | def push(self): 10 | for token_info in settings.PUSH_NOTIFICATION_SERVICE_SUBSCRIBERS_TOKENS: 11 | requests.post( 12 | url=settings.PUSH_NOTIFICATION_SERVICE_POST_URL, 13 | json=dict( 14 | token=token_info[0], 15 | user=token_info[1], 16 | message=self.notification_message 17 | ) 18 | ) 19 | return True 20 | -------------------------------------------------------------------------------- /src/base/utils.py: -------------------------------------------------------------------------------- 1 | def get_client_ip(request_meta): 2 | x_forwarded_for = request_meta.get('HTTP_X_FORWARDED_FOR') 3 | if x_forwarded_for: 4 | ip = x_forwarded_for.split(',')[0] 5 | else: 6 | ip = request_meta.get('REMOTE_ADDR') 7 | return ip 8 | 9 | 10 | def to_e164(phone_number): 11 | if not phone_number: 12 | return phone_number 13 | if not phone_number.startswith('+'): 14 | phone_number = '+' + phone_number 15 | if not phone_number.count('.'): 16 | phone_number = phone_number[0:2] + '.' + phone_number[2:] 17 | if phone_number.endswith('.'): 18 | phone_number = phone_number + '0' 19 | return phone_number 20 | -------------------------------------------------------------------------------- /src/billing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/billing/__init__.py -------------------------------------------------------------------------------- /src/billing/decorators.py: -------------------------------------------------------------------------------- 1 | from django.contrib import messages 2 | 3 | from billing import orders as billing_orders 4 | 5 | 6 | def create_or_update_single_order(item_type, item_price): 7 | def decorator(func): 8 | def wrapper(self, **kwargs): 9 | domain_name = kwargs.get('domain_name') 10 | order = None 11 | started_orders = billing_orders.list_orders( 12 | owner=self.request.user, 13 | exclude_cancelled=True, 14 | include_statuses=['started'] 15 | ) 16 | if started_orders: 17 | order = started_orders[0] 18 | kwargs['has_existing_order'] = True 19 | messages.warning(self.request, 'There is an order you did not complete yet. ' 20 | 'Please confirm or cancel this order to create a new one') 21 | if not order: 22 | order = billing_orders.order_single_item( 23 | owner=self.request.user, 24 | item_type=item_type, 25 | item_price=item_price, 26 | item_name=domain_name, 27 | ) 28 | kwargs['order'] = order 29 | return func(self, **kwargs) 30 | return wrapper 31 | return decorator 32 | -------------------------------------------------------------------------------- /src/billing/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | class BillingError(Exception): 3 | message = 'Unknown error' 4 | 5 | def __init__(self, message='', *args, **kwargs): 6 | if message: 7 | self.message = message 8 | super(BillingError).__init__(*args, **kwargs) 9 | 10 | def __str__(self): 11 | return '%s' % (self.message, ) 12 | 13 | 14 | class DomainBlockedError(BillingError): 15 | 16 | message = 'Domain is blocked and can not be registered/renewed at the moment.' 17 | -------------------------------------------------------------------------------- /src/billing/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/billing/management/__init__.py -------------------------------------------------------------------------------- /src/billing/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/billing/management/commands/__init__.py -------------------------------------------------------------------------------- /src/billing/migrations/0002_auto_20190120_1549.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-01-20 15:49 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('billing', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='payment', 15 | name='status', 16 | field=models.CharField(choices=[('started', 'Started'), ('failed', 'Failed'), ('paid', 'Paid'), ('processed', 'Processed')], default='started', max_length=16), 17 | ), 18 | migrations.AlterField( 19 | model_name='payment', 20 | name='transaction_id', 21 | field=models.CharField(max_length=16, unique=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /src/billing/migrations/0002_auto_20190120_2142.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-01-20 21:42 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('billing', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='payment', 15 | name='merchant_reference', 16 | field=models.CharField(blank=True, default=None, max_length=16, null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='payment', 20 | name='started_at', 21 | field=models.DateTimeField(), 22 | ), 23 | migrations.AlterField( 24 | model_name='payment', 25 | name='status', 26 | field=models.CharField(choices=[('started', 'Started'), ('failed', 'Failed'), ('paid', 'Paid'), ('processed', 'Processed')], default='started', max_length=16), 27 | ), 28 | migrations.AlterField( 29 | model_name='payment', 30 | name='transaction_id', 31 | field=models.CharField(max_length=16, unique=True), 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /src/billing/migrations/0003_auto_20190120_2150.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-01-20 21:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('billing', '0002_auto_20190120_2142'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='payment', 15 | name='amount', 16 | field=models.FloatField(), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/billing/migrations/0004_merge_20190120_2203.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-01-20 22:03 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('billing', '0002_auto_20190120_1549'), 10 | ('billing', '0003_auto_20190120_2150'), 11 | ] 12 | 13 | operations = [ 14 | ] 15 | -------------------------------------------------------------------------------- /src/billing/migrations/0005_auto_20190120_2224.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-01-20 22:24 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('billing', '0004_merge_20190120_2203'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='payment', 15 | name='status', 16 | field=models.CharField(choices=[('started', 'Started'), ('cancelled', 'Cancelled'), ('declined', 'Declined'), ('paid', 'Paid'), ('unconfirmed', 'Unconfirmed'), ('processed', 'Processed')], default='started', max_length=16), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/billing/migrations/0006_auto_20190120_2303.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-01-20 23:03 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('billing', '0005_auto_20190120_2224'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='payment', 15 | name='finished_at', 16 | field=models.DateTimeField(blank=True, default=None, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/billing/migrations/0008_auto_20190127_1513.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-01-27 15:13 2 | 3 | from django.db import migrations, models 4 | 5 | import zen.zdomains 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('billing', '0007_order_orderitem'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='orderitem', 17 | name='name', 18 | field=models.CharField(max_length=255, validators=[zen.zdomains.validate_domain_name]), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /src/billing/migrations/0009_order_description.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-01-27 15:30 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('billing', '0008_auto_20190127_1513'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='order', 15 | name='description', 16 | field=models.CharField(blank=True, default='', max_length=255), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/billing/migrations/0010_auto_20190202_1307.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.5 on 2019-02-02 13:07 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('billing', '0009_order_description'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='orderitem', 15 | name='status', 16 | field=models.CharField(choices=[('pending', 'Pending'), ('failed', 'Failed'), ('processed', 'Processed')], default='started', max_length=16), 17 | ), 18 | migrations.AlterField( 19 | model_name='order', 20 | name='status', 21 | field=models.CharField(choices=[('started', 'Started'), ('cancelled', 'Cancelled'), ('failed', 'Failed'), ('incomplete', 'Incomplete'), ('processed', 'Processed')], default='started', max_length=16), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /src/billing/migrations/0011_auto_20190505_1645.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2019-05-05 16:45 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('billing', '0010_auto_20190202_1307'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='payment', 15 | name='method', 16 | field=models.CharField(choices=[('pay_4csonline', 'Credit Card'), ('pay_bank_transfer_anguilla', 'Bank Transfer'), ('pay_btcpay', 'BitCoin')], max_length=32), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/billing/migrations/0013_auto_20190705_1806.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.2 on 2019-07-05 18:06 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('billing', '0012_btcpayinvoice'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='payment', 15 | name='method', 16 | field=models.CharField(choices=[('pay_4csonline', 'Credit Card'), ('pay_btcpay', 'BitCoin')], max_length=32), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/billing/migrations/0014_auto_20190811_2030.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.3 on 2019-08-11 20:30 2 | 3 | import django.contrib.postgres.fields.jsonb 4 | import django.core.serializers.json 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('billing', '0013_auto_20190705_1806'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='orderitem', 17 | name='details', 18 | field=django.contrib.postgres.fields.jsonb.JSONField(encoder=django.core.serializers.json.DjangoJSONEncoder, null=True), 19 | ), 20 | migrations.AlterField( 21 | model_name='orderitem', 22 | name='type', 23 | field=models.CharField(choices=[('domain_register', 'Domain Register'), ('domain_renew', 'Domain Renew'), ('domain_restore', 'Domain Restore'), ('domain_transfer', 'Domain Transfer')], max_length=32), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /src/billing/migrations/0015_auto_20190901_1958.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-09-01 19:58 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('billing', '0014_auto_20190811_2030'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='order', 15 | name='status', 16 | field=models.CharField(choices=[('started', 'Started'), ('processing', 'Processing'), ('cancelled', 'Cancelled'), ('failed', 'Failed'), ('incomplete', 'Incomplete'), ('processed', 'Processed')], default='started', max_length=16), 17 | ), 18 | migrations.AlterField( 19 | model_name='orderitem', 20 | name='status', 21 | field=models.CharField(choices=[('started', 'Started'), ('pending', 'Pending'), ('failed', 'Failed'), ('processed', 'Processed')], default='started', max_length=16), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /src/billing/migrations/0016_auto_20201219_1451.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.4 on 2020-12-19 14:51 2 | 3 | import django.core.serializers.json 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('billing', '0015_auto_20190901_1958'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='orderitem', 16 | name='details', 17 | field=models.JSONField(encoder=django.core.serializers.json.DjangoJSONEncoder, null=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /src/billing/migrations/0017_auto_20210123_1757.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.4 on 2021-01-23 17:57 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('billing', '0016_auto_20201219_1451'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='payment', 15 | name='notes', 16 | field=models.TextField(blank=True, default=None, help_text='any note regarding the payment such as the reason of the balance change by admin.', null=True), 17 | ), 18 | migrations.AlterField( 19 | model_name='payment', 20 | name='method', 21 | field=models.CharField(choices=[('pay_4csonline', 'Credit Card'), ('pay_btcpay', 'BitCoin'), ('pay_by_admin', 'Pay by Admin')], max_length=32), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /src/billing/migrations/0018_auto_20210124_0739.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.4 on 2021-01-24 07:39 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('billing', '0017_auto_20210123_1757'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='payment', 15 | name='method', 16 | field=models.CharField(choices=[('pay_4csonline', 'Credit Card'), ('pay_btcpay', 'BitCoin'), ('pay_by_admin', 'Balance adjustment by admin')], max_length=32), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/billing/migrations/0019_alter_orderitem_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.5 on 2022-01-11 22:27 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('billing', '0018_auto_20210124_0739'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='orderitem', 15 | name='status', 16 | field=models.CharField(choices=[('started', 'Started'), ('pending', 'Pending'), ('executing', 'Executing'), ('failed', 'Failed'), ('processed', 'Processed')], default='started', max_length=16), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/billing/migrations/0020_order_retries.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.5 on 2022-01-22 13:57 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('billing', '0019_alter_orderitem_status'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='order', 15 | name='retries', 16 | field=models.IntegerField(default=0), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/billing/migrations/0021_alter_orderitem_status.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.5 on 2022-01-22 16:37 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('billing', '0020_order_retries'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='orderitem', 15 | name='status', 16 | field=models.CharField(choices=[('started', 'Started'), ('pending', 'Pending'), ('executing', 'Executing'), ('failed', 'Failed'), ('blocked', 'Blocked'), ('processed', 'Processed')], default='started', max_length=16), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/billing/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/billing/migrations/__init__.py -------------------------------------------------------------------------------- /src/billing/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/billing/models/__init__.py -------------------------------------------------------------------------------- /src/billing/pay_4csonline/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/billing/pay_4csonline/__init__.py -------------------------------------------------------------------------------- /src/billing/pay_btcpay/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/billing/pay_btcpay/__init__.py -------------------------------------------------------------------------------- /src/billing/pay_btcpay/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class BTCPayInvoice(models.Model): 5 | 6 | invoices = models.Manager() 7 | 8 | class Meta: 9 | app_label = 'billing' 10 | base_manager_name = 'invoices' 11 | default_manager_name = 'invoices' 12 | 13 | transaction_id = models.CharField(max_length=16, unique=True, null=False, blank=False) 14 | 15 | invoice_id = models.CharField(max_length=32, unique=True, null=False, blank=False) 16 | 17 | amount = models.FloatField(null=False, blank=False) 18 | 19 | started_at = models.DateTimeField(auto_now_add=True) 20 | 21 | finished_at = models.DateTimeField(null=True, blank=True, default=None) 22 | 23 | status = models.CharField( 24 | choices=( 25 | ('new', 'New',), 26 | ('paid', 'Paid',), 27 | ('confirmed', 'Confirmed',), 28 | ('complete', 'Complete',), 29 | ('expired', 'Expired',), 30 | ('invalid', 'Invalid',), 31 | ), 32 | default='started', 33 | max_length=16, 34 | null=False, 35 | blank=False, 36 | ) 37 | 38 | def __str__(self): 39 | return 'BTCPayInvoice({} {} {} {})'.format(self.transaction_id, self.invoice_id, self.amount, self.status) 40 | -------------------------------------------------------------------------------- /src/billing/templates/billing/4csonline/failed_payment.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Payment failed 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 |
28 |
29 | Unfortunately your payment has failed: 30 | {{message}} 31 |
32 |
33 |
34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /src/billing/templates/billing/4csonline/pending_payment.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Payment verification is pending 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 |
28 | {{message}} 29 |
30 |
31 |
32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /src/billing/templates/billing/4csonline/start_payment.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/container_fluid.html' %} 2 | 3 | {% block content %} 4 | 5 | 6 |
7 | 17 |
18 | 19 | 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /src/billing/templates/billing/4csonline/success_payment.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Payment accepted 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 27 |
28 |
29 |
30 |
31 | 32 | Your payment of {{ amount }} was processed successfully. 33 | 34 |
35 |
36 |
37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /src/billing/templates/billing/account_invoices.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/index.html' %} 2 | 3 | {% block main_content %} 4 | 5 | 6 | 29 | 30 | 31 | 32 | {% include 'base/pagination_class_based_views.html' %} 33 | 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /src/billing/templates/billing/btcpay/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/billing/templates/billing/btcpay/__init__.py -------------------------------------------------------------------------------- /src/billing/templates/billing/btcpay/start_payment.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/container_sm_8.html' %} 2 | 3 | {% block content %} 4 | 5 | 6 |
7 | You will be paying ${{amount}} US as bitcoins.
8 | Payment gateway automatically calculates bitcoin price according to market exchange rate.
9 | Be aware that, payment process may take a few minutes. 10 |
11 | {% csrf_token %} 12 | 13 | 14 |
15 |
16 | 17 | 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /src/billing/templates/billing/new_payment.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/container_sm_4.html' %} 2 | 3 | {% block content %} 4 | 5 | {% load static %} 6 | 7 | 8 |
9 | mastercard logo 10 | bitcoin logo 11 | visa card logo 12 |
13 | 14 |
15 | Your current balance is ${{ user.balance }} US 16 |
17 |
18 | 19 |
20 | {% csrf_token %} 21 | {% bootstrap_form form %} 22 | 23 |
24 | 25 |
26 | * Note that your card will be charged ${{ credit_card_payment_price }} for a credit of 27 | ${{ credit_card_payment_base_price }} in your account when you pay with a credit card. 28 | 29 | 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /src/board/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/board/__init__.py -------------------------------------------------------------------------------- /src/board/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from nested_admin import NestedModelAdmin # @UnresolvedImport 4 | 5 | from board.models.csv_file_sync import CSVFileSync 6 | 7 | 8 | class CSVFileSyncAdmin(NestedModelAdmin): 9 | pass 10 | 11 | 12 | admin.site.register(CSVFileSync, CSVFileSyncAdmin) 13 | -------------------------------------------------------------------------------- /src/board/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.13 on 2020-12-22 09:49 2 | 3 | from django.db import migrations, models 4 | import django.db.models.manager 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='CSVFileSync', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('created_at', models.DateTimeField(auto_now_add=True)), 20 | ('input_filename', models.CharField(db_index=True, max_length=255)), 21 | ('dry_run', models.BooleanField(default=True)), 22 | ('status', models.CharField(choices=[('started', 'STARTED'), ('finished', 'FINISHED'), ('failed', 'FAILED')], default='started', max_length=10)), 23 | ('output_log', models.TextField(blank=True)), 24 | ('processed_count', models.IntegerField(default=0)), 25 | ], 26 | options={ 27 | 'base_manager_name': 'executions', 28 | 'default_manager_name': 'executions', 29 | }, 30 | managers=[ 31 | ('executions', django.db.models.manager.Manager()), 32 | ], 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /src/board/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/board/migrations/__init__.py -------------------------------------------------------------------------------- /src/board/models/__init__.py: -------------------------------------------------------------------------------- 1 | from board.models.csv_file_sync import CSVFileSync 2 | -------------------------------------------------------------------------------- /src/board/models/csv_file_sync.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.db import models 4 | 5 | 6 | class CSVFileSync(models.Model): 7 | 8 | executions = models.Manager() 9 | 10 | class Meta: 11 | app_label = 'board' 12 | base_manager_name = 'executions' 13 | default_manager_name = 'executions' 14 | 15 | created_at = models.DateTimeField(auto_now_add=True) 16 | 17 | input_filename = models.CharField(max_length=255, db_index=True) 18 | 19 | dry_run = models.BooleanField(default=True) 20 | 21 | status = models.CharField( 22 | max_length=10, 23 | choices=( 24 | ('started', 'STARTED', ), 25 | ('finished', 'FINISHED', ), 26 | ('failed', 'FAILED', ), 27 | ), 28 | default='started', 29 | ) 30 | 31 | output_log = models.TextField(blank=True) 32 | 33 | processed_count = models.IntegerField(default=0) 34 | 35 | @property 36 | def filename(self): 37 | return os.path.basename(self.input_filename) 38 | 39 | def __str__(self): 40 | return 'CSVFileSync({}:{})'.format(self.filename, self.status) 41 | 42 | def __repr__(self): 43 | return 'CSVFileSync({}:{})'.format(self.filename, self.status) 44 | -------------------------------------------------------------------------------- /src/board/templates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/board/templates/__init__.py -------------------------------------------------------------------------------- /src/board/templates/board/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/board/templates/board/__init__.py -------------------------------------------------------------------------------- /src/board/templates/board/admin_index.html: -------------------------------------------------------------------------------- 1 | {% include 'board/admin_head.html' %} 2 | 3 | 4 |
5 |
6 | 7 | {% block main_content %} 8 | 9 | {% endblock %} 10 | 11 |
12 |
13 | 14 | 15 | {% include 'base/foot.html' %} 16 | -------------------------------------------------------------------------------- /src/board/templates/board/admin_page.html: -------------------------------------------------------------------------------- 1 | {% extends 'board/admin_index.html' %} 2 | 3 | {% block main_content %} 4 | 5 | {% block content_wrapper %} 6 | 7 |
8 |
9 |
10 |
11 | {% block content %}{% endblock %} 12 |
13 |
14 |
15 |
16 | 17 | {% endblock %} 18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /src/board/templates/board/bulk_transfer.html: -------------------------------------------------------------------------------- 1 | {% extends 'board/admin_page.html' %} 2 | 3 | {% block main_content %} 4 | 5 |

Bulk domain transfer

6 | 7 | 26 | 27 | 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /src/board/templates/board/csv_file_sync_record.html: -------------------------------------------------------------------------------- 1 | {% extends 'board/admin_page.html' %} 2 | 3 | {% block content %} 4 | 5 | 6 | 21 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /src/board/templates/board/not_existing_domain_sync.html: -------------------------------------------------------------------------------- 1 | {% extends 'board/admin_page.html' %} 2 | 3 | {% block main_content %} 4 | 5 |

Domain synchronization

6 | 7 | 24 | 25 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /src/board/templates/board/sending_single_email.html: -------------------------------------------------------------------------------- 1 | {% extends 'board/admin_page.html' %} 2 | 3 | {% block main_content %} 4 | 5 |

Send a testing e-mail

6 | 7 | 26 | 27 | 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /src/board/templates/board/two_factor_reset.html: -------------------------------------------------------------------------------- 1 | {% extends 'board/admin_page.html' %} 2 | 3 | {% block main_content %} 4 | 5 |

Account 2FA reset

6 | 7 | 24 | 25 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /src/front/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/front/templates/escrow/escrow.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/front/templates/faq/faq.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/front/templates/front/404_error.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/index.html' %} 2 | 3 | {% block main_content %} 4 | 5 | 6 |

THIS PAGE IS NOT FOUND.

7 | 8 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /src/front/templates/front/500_error.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/index.html' %} 2 | 3 | {% block main_content %} 4 | 5 | 6 |

There is a server error. Please try again later.

7 | 8 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /src/front/templates/front/account_contact_delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/index.html' %} 2 | 3 | {% block main_content %} 4 | 5 | 6 | 13 | 14 | 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /src/front/templates/front/account_domain_transfer_code.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/index.html' %} 2 | 3 | {% block main_content %} 4 | 5 | 6 | 10 | 11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /src/front/templates/front/contact_us.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/index.html' %} 2 | 3 | {% block main_content %} 4 | 5 | 6 | 7 | 8 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /src/front/templates/front/domain_lookup.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/index.html' %} 2 | 3 | {% block main_content %} 4 | 5 | 6 | {% if result == 'not exist' %} 7 | 12 | {% else %} 13 |
14 |
15 | {% csrf_token %} 16 |
17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 |
25 |
26 | {% endif %} 27 | 28 | 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /src/front/templates/front/escrow.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/index.html' %} 2 | 3 | {% block main_content %} 4 | 5 | 6 | {% include 'escrow/escrow.html' %} 7 | 8 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /src/front/templates/front/faq.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/index.html' %} 2 | 3 | {% block main_content %} 4 | 5 | 6 | {% include 'faq/faq.html' %} 7 | 8 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /src/front/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/front/templatetags/__init__.py -------------------------------------------------------------------------------- /src/front/templatetags/front_filters.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from django import template 3 | 4 | register = template.Library() 5 | 6 | 7 | @register.filter() 8 | def add_days(days): 9 | return datetime.date.today() + datetime.timedelta(days=days) 10 | -------------------------------------------------------------------------------- /src/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/lib/__init__.py -------------------------------------------------------------------------------- /src/logs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/logs/__init__.py -------------------------------------------------------------------------------- /src/logs/migrations/0002_requestlog_path_full.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2025-01-09 12:16 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('logs', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='requestlog', 15 | name='path_full', 16 | field=models.CharField(blank=True, db_index=True, default='', help_text='Request path', max_length=255), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /src/logs/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/logs/migrations/__init__.py -------------------------------------------------------------------------------- /src/logs/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class RequestLog(models.Model): 5 | timestamp = models.DateTimeField(help_text="Time of the request", auto_now_add=True, db_index=True) 6 | ip_address = models.GenericIPAddressField(help_text="IP address of the requestor", blank=True, null=True, db_index=True) 7 | user = models.CharField(help_text="If captured, the name of the user who sent the request", 8 | max_length=255, blank=True, default="", db_index=True) 9 | method = models.CharField(help_text="HTTP method", max_length=16, blank=True, default="", db_index=True) 10 | path = models.CharField(help_text="Request path", max_length=255, blank=True, default="", db_index=True) 11 | path_full = models.CharField(help_text="Request path", max_length=255, blank=True, default="", db_index=True) 12 | request = models.TextField(help_text="Request body", null=True, default="") 13 | status_code = models.IntegerField(help_text="Response HTTP status code", null=True, blank=True, default=None, db_index=True) 14 | exception = models.TextField(help_text="Exception info", blank=True, null=True, default=None) 15 | duration = models.FloatField(help_text="Duration of the response", default=0.0) 16 | 17 | @staticmethod 18 | def erase_old_records(num_records): 19 | return RequestLog.objects.filter( 20 | pk__in=RequestLog.objects.order_by('-timestamp').all().values_list('pk')[num_records:], 21 | ).delete() 22 | -------------------------------------------------------------------------------- /src/main/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'main.apps.MainConfig' 2 | -------------------------------------------------------------------------------- /src/main/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import random 4 | 5 | from django.apps import AppConfig 6 | 7 | 8 | class MainConfig(AppConfig): 9 | name = 'main' 10 | 11 | def ready(self): 12 | """Location for package configurations""" 13 | random.seed() 14 | return True 15 | -------------------------------------------------------------------------------- /src/main/confdocs.py: -------------------------------------------------------------------------------- 1 | """ 2 | """ 3 | from __future__ import unicode_literals 4 | 5 | import os 6 | 7 | import django 8 | 9 | 10 | def configure_django_settings(): 11 | """Configure Django""" 12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "main.settings") 13 | django.setup() 14 | 15 | 16 | def configure(): 17 | configure_django_settings() 18 | -------------------------------------------------------------------------------- /src/main/params_docker.py: -------------------------------------------------------------------------------- 1 | ENV = 'docker' 2 | 3 | SECRET_KEY = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' 4 | 5 | DEBUG = True 6 | 7 | DATABASES_ENGINE = 'django.db.backends.postgresql' 8 | DATABASES_NAME = 'zenaida_db_01' 9 | DATABASES_USER = 'zenaida_db_user' 10 | DATABASES_PASSWORD = 'zenaida_db_user' 11 | DATABASES_HOST = 'postgres' 12 | DATABASES_PORT = 5432 13 | 14 | ZENAIDA_REGISTRAR_ID = 'whois_ai' 15 | ZENAIDA_SUPPORTED_ZONES = ['ai', ] 16 | -------------------------------------------------------------------------------- /src/main/static/css/styles.css: -------------------------------------------------------------------------------- 1 | /* Hi there */ 2 | 3 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/font-awesome/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /src/main/static/font-awesome/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/font-awesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /src/main/static/font-awesome/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/font-awesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /src/main/static/font-awesome/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/font-awesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /src/main/static/font-awesome/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/font-awesome/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /src/main/static/font-awesome/less/animated.less: -------------------------------------------------------------------------------- 1 | // Animated Icons 2 | // -------------------------- 3 | 4 | .@{fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .@{fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/less/bordered-pulled.less: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em @fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .@{fa-css-prefix}-pull-left { float: left; } 11 | .@{fa-css-prefix}-pull-right { float: right; } 12 | 13 | .@{fa-css-prefix} { 14 | &.@{fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.@{fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .@{fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/less/core.less: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/less/extras.less: -------------------------------------------------------------------------------- 1 | // Extras 2 | // -------------------------- 3 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/less/fixed-width.less: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .@{fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/less/font-awesome.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.6.3 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables.less"; 7 | @import "mixins.less"; 8 | @import "path.less"; 9 | @import "core.less"; 10 | @import "larger.less"; 11 | @import "fixed-width.less"; 12 | @import "list.less"; 13 | @import "bordered-pulled.less"; 14 | @import "animated.less"; 15 | @import "rotated-flipped.less"; 16 | @import "stacked.less"; 17 | @import "icons.less"; 18 | @import "screen-reader.less"; 19 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/less/larger.less: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .@{fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .@{fa-css-prefix}-2x { font-size: 2em; } 11 | .@{fa-css-prefix}-3x { font-size: 3em; } 12 | .@{fa-css-prefix}-4x { font-size: 4em; } 13 | .@{fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/less/list.less: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: @fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .@{fa-css-prefix}-li { 11 | position: absolute; 12 | left: -@fa-li-width; 13 | width: @fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.@{fa-css-prefix}-lg { 17 | left: (-@fa-li-width + (4em / 14)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/less/path.less: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}'); 7 | src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'), 8 | url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'), 9 | url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), 10 | url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'), 11 | url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/less/rotated-flipped.less: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } 5 | .@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } 6 | .@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } 7 | 8 | .@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } 9 | .@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .@{fa-css-prefix}-rotate-90, 15 | :root .@{fa-css-prefix}-rotate-180, 16 | :root .@{fa-css-prefix}-rotate-270, 17 | :root .@{fa-css-prefix}-flip-horizontal, 18 | :root .@{fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/less/screen-reader.less: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { .sr-only(); } 5 | .sr-only-focusable { .sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/less/spinning.less: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .@{fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | @-webkit-keyframes fa-spin { 10 | 0% { 11 | -webkit-transform: rotate(0deg); 12 | transform: rotate(0deg); 13 | } 14 | 100% { 15 | -webkit-transform: rotate(359deg); 16 | transform: rotate(359deg); 17 | } 18 | } 19 | 20 | @keyframes fa-spin { 21 | 0% { 22 | -webkit-transform: rotate(0deg); 23 | transform: rotate(0deg); 24 | } 25 | 100% { 26 | -webkit-transform: rotate(359deg); 27 | transform: rotate(359deg); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/less/stacked.less: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .@{fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .@{fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .@{fa-css-prefix}-inverse { color: @fa-inverse; } 21 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/scss/_animated.scss: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .#{$fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .#{$fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/scss/_bordered-pulled.scss: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em $fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .#{$fa-css-prefix}-pull-left { float: left; } 11 | .#{$fa-css-prefix}-pull-right { float: right; } 12 | 13 | .#{$fa-css-prefix} { 14 | &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .#{$fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/scss/_core.scss: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/scss/_extras.scss: -------------------------------------------------------------------------------- 1 | /* EXTRAS 2 | * -------------------------- */ 3 | 4 | /* Stacked and layered icon */ 5 | 6 | /* Animated rotating icon */ 7 | .#{$fa-css-prefix}-spin { 8 | -webkit-animation: spin 2s infinite linear; 9 | -moz-animation: spin 2s infinite linear; 10 | -o-animation: spin 2s infinite linear; 11 | animation: spin 2s infinite linear; 12 | } 13 | 14 | @-moz-keyframes spin { 15 | 0% { -moz-transform: rotate(0deg); } 16 | 100% { -moz-transform: rotate(359deg); } 17 | } 18 | @-webkit-keyframes spin { 19 | 0% { -webkit-transform: rotate(0deg); } 20 | 100% { -webkit-transform: rotate(359deg); } 21 | } 22 | @-o-keyframes spin { 23 | 0% { -o-transform: rotate(0deg); } 24 | 100% { -o-transform: rotate(359deg); } 25 | } 26 | @-ms-keyframes spin { 27 | 0% { -ms-transform: rotate(0deg); } 28 | 100% { -ms-transform: rotate(359deg); } 29 | } 30 | @keyframes spin { 31 | 0% { transform: rotate(0deg); } 32 | 100% { transform: rotate(359deg); } 33 | } 34 | 35 | 36 | // Icon rotations & flipping 37 | // ------------------------- 38 | 39 | .#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } 40 | .#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } 41 | .#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } 42 | 43 | .#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } 44 | .#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } 45 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/scss/_fixed-width.scss: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .#{$fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/scss/_larger.scss: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .#{$fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .#{$fa-css-prefix}-2x { font-size: 2em; } 11 | .#{$fa-css-prefix}-3x { font-size: 3em; } 12 | .#{$fa-css-prefix}-4x { font-size: 4em; } 13 | .#{$fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/scss/_list.scss: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: $fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .#{$fa-css-prefix}-li { 11 | position: absolute; 12 | left: -$fa-li-width; 13 | width: $fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.#{$fa-css-prefix}-lg { 17 | left: -$fa-li-width + (4em / 14); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/scss/_path.scss: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); 7 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), 8 | url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), 9 | url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), 10 | url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), 11 | url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/scss/_rotated-flipped.scss: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } 5 | .#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } 6 | .#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } 7 | 8 | .#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } 9 | .#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .#{$fa-css-prefix}-rotate-90, 15 | :root .#{$fa-css-prefix}-rotate-180, 16 | :root .#{$fa-css-prefix}-rotate-270, 17 | :root .#{$fa-css-prefix}-flip-horizontal, 18 | :root .#{$fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/scss/_screen-reader.scss: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { @include sr-only(); } 5 | .sr-only-focusable { @include sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/scss/_spinning.scss: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .#{$fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | @-webkit-keyframes fa-spin { 10 | 0% { 11 | -webkit-transform: rotate(0deg); 12 | transform: rotate(0deg); 13 | } 14 | 100% { 15 | -webkit-transform: rotate(359deg); 16 | transform: rotate(359deg); 17 | } 18 | } 19 | 20 | @keyframes fa-spin { 21 | 0% { 22 | -webkit-transform: rotate(0deg); 23 | transform: rotate(0deg); 24 | } 25 | 100% { 26 | -webkit-transform: rotate(359deg); 27 | transform: rotate(359deg); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/scss/_stacked.scss: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .#{$fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .#{$fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .#{$fa-css-prefix}-inverse { color: $fa-inverse; } 21 | -------------------------------------------------------------------------------- /src/main/static/font-awesome/scss/font-awesome.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.6.3 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables"; 7 | @import "mixins"; 8 | @import "path"; 9 | @import "core"; 10 | @import "larger"; 11 | @import "fixed-width"; 12 | @import "list"; 13 | @import "bordered-pulled"; 14 | @import "animated"; 15 | @import "rotated-flipped"; 16 | @import "stacked"; 17 | @import "icons"; 18 | @import "screen-reader"; 19 | -------------------------------------------------------------------------------- /src/main/static/icons/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/android-icon-144x144.png -------------------------------------------------------------------------------- /src/main/static/icons/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/android-icon-192x192.png -------------------------------------------------------------------------------- /src/main/static/icons/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/android-icon-36x36.png -------------------------------------------------------------------------------- /src/main/static/icons/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/android-icon-48x48.png -------------------------------------------------------------------------------- /src/main/static/icons/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/android-icon-72x72.png -------------------------------------------------------------------------------- /src/main/static/icons/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/android-icon-96x96.png -------------------------------------------------------------------------------- /src/main/static/icons/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/apple-icon-114x114.png -------------------------------------------------------------------------------- /src/main/static/icons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/apple-icon-120x120.png -------------------------------------------------------------------------------- /src/main/static/icons/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/apple-icon-144x144.png -------------------------------------------------------------------------------- /src/main/static/icons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/apple-icon-152x152.png -------------------------------------------------------------------------------- /src/main/static/icons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/apple-icon-180x180.png -------------------------------------------------------------------------------- /src/main/static/icons/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/apple-icon-57x57.png -------------------------------------------------------------------------------- /src/main/static/icons/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/apple-icon-60x60.png -------------------------------------------------------------------------------- /src/main/static/icons/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/apple-icon-72x72.png -------------------------------------------------------------------------------- /src/main/static/icons/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/apple-icon-76x76.png -------------------------------------------------------------------------------- /src/main/static/icons/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/apple-icon-precomposed.png -------------------------------------------------------------------------------- /src/main/static/icons/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/apple-icon.png -------------------------------------------------------------------------------- /src/main/static/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/favicon-16x16.png -------------------------------------------------------------------------------- /src/main/static/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/favicon-32x32.png -------------------------------------------------------------------------------- /src/main/static/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/favicon-96x96.png -------------------------------------------------------------------------------- /src/main/static/icons/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/ms-icon-144x144.png -------------------------------------------------------------------------------- /src/main/static/icons/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/ms-icon-150x150.png -------------------------------------------------------------------------------- /src/main/static/icons/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/ms-icon-310x310.png -------------------------------------------------------------------------------- /src/main/static/icons/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/ms-icon-70x70.png -------------------------------------------------------------------------------- /src/main/static/icons/www-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/icons/www-icon.png -------------------------------------------------------------------------------- /src/main/static/images/bitcoin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/images/bitcoin.png -------------------------------------------------------------------------------- /src/main/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/images/favicon.ico -------------------------------------------------------------------------------- /src/main/static/images/mastercard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/images/mastercard.png -------------------------------------------------------------------------------- /src/main/static/images/visa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/static/images/visa.png -------------------------------------------------------------------------------- /src/main/static/js/api.js: -------------------------------------------------------------------------------- 1 | /* PLEASE DO NOT COPY AND PASTE THIS CODE. */(function(){var w=window,C='___grecaptcha_cfg',cfg=w[C]=w[C]||{},N='grecaptcha';var gr=w[N]=w[N]||{};gr.ready=gr.ready||function(f){(cfg['fns']=cfg['fns']||[]).push(f);};w['__recaptcha_api']='https://www.recaptcha.net/recaptcha/api2/';(cfg['render']=cfg['render']||[]).push('onload');w['__google_recaptcha_client']=true;var d=document,po=d.createElement('script');po.type='text/javascript';po.async=true;po.src='https://zenaida.cate.ai/static/js/recaptcha__en.js';po.crossOrigin='anonymous';var e=d.querySelector('script[nonce]'),n=e&&(e['nonce']||e.getAttribute('nonce'));if(n){po.setAttribute('nonce',n);}var s=d.getElementsByTagName('script')[0];s.parentNode.insertBefore(po, s);})(); -------------------------------------------------------------------------------- /src/main/static/js/ie10-viewport-bug-workaround.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * IE10 viewport hack for Surface/desktop Windows 8 bug 3 | * Copyright 2014-2017 The Bootstrap Authors 4 | * Copyright 2014-2017 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | */ 7 | 8 | // See the Getting Started docs for more information: 9 | // https://getbootstrap.com/getting-started/#support-ie10-width 10 | 11 | (function () { 12 | 'use strict' 13 | 14 | if (navigator.userAgent.match(/IEMobile\/10\.0/)) { 15 | var msViewportStyle = document.createElement('style') 16 | msViewportStyle.appendChild( 17 | document.createTextNode( 18 | '@-ms-viewport{width:auto!important}' 19 | ) 20 | ) 21 | document.head.appendChild(msViewportStyle) 22 | } 23 | 24 | }()) 25 | -------------------------------------------------------------------------------- /src/main/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/main/templatetags/__init__.py -------------------------------------------------------------------------------- /src/main/templatetags/settings_filter.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.conf import settings 3 | 4 | 5 | register = template.Library() 6 | 7 | 8 | @register.tag 9 | def settings_value(parser, token): 10 | try: 11 | tag_name, var = token.split_contents() 12 | except ValueError: 13 | raise template.TemplateSyntaxError("%r tag requires a single argument" % token.contents.split()[0]) 14 | return ValueFromSettings(var) 15 | 16 | 17 | class ValueFromSettings(template.Node): 18 | def __init__(self, var): 19 | self.arg = template.Variable(var) 20 | def render(self, context): 21 | return settings.__getattr__(str(self.arg)) 22 | -------------------------------------------------------------------------------- /src/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "main.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | execute_from_command_line(sys.argv) 10 | -------------------------------------------------------------------------------- /src/templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/templates/.gitkeep -------------------------------------------------------------------------------- /src/templates/base/container_fluid.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/index.html' %} 2 | 3 | {% block main_content %} 4 | 5 | {% block content_wrapper %} 6 | 7 |
8 |
9 |
10 |
11 | {% block content %}{% endblock %} 12 |
13 |
14 |
15 |
16 | 17 | {% endblock %} 18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /src/templates/base/container_sm_10.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/index.html' %} 2 | 3 | {% block main_content %} 4 | 5 | {% block content_wrapper %} 6 | 7 |
8 |
9 |
10 |
11 | {% block content %}{% endblock %} 12 |
13 |
14 |
15 |
16 | 17 | {% endblock %} 18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /src/templates/base/container_sm_4.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/index.html' %} 2 | 3 | {% block main_content %} 4 | 5 | {% block content_wrapper %} 6 | 7 |
8 |
9 |
10 |
11 | {% block content %}{% endblock %} 12 |
13 |
14 |
15 |
16 | 17 | {% endblock %} 18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /src/templates/base/container_sm_8.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/index.html' %} 2 | 3 | {% block main_content %} 4 | 5 | {% block content_wrapper %} 6 | 7 |
8 |
9 |
10 |
11 | {% block content %}{% endblock %} 12 |
13 |
14 |
15 |
16 | 17 | {% endblock %} 18 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /src/templates/base/epp_status.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Zenaida.Cate.ai EPP status 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 | 27 | {% block main_content %} 28 | 29 |
30 | 31 | EPP: {{ epp }} 32 | 33 |
34 | 35 | {% endblock %} 36 | 37 |
38 |
39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/templates/base/foot.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/templates/email/account_approved.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | {{ subject }} 7 | 8 | 9 | 10 | 11 | 12 |

Hello, dear Customer.

13 | 14 |

Your account at {{ site_name }} was approved by the administrator.

15 | 16 |

17 | Please, sign in at {{ site_url }} to start registering AI domains. 18 |

19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/templates/email/activation_profile.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | Profile Activation 7 | 8 | 9 | 10 | 11 | 12 | To activate your AI domains registry profile, please follow this link:
13 | 14 | {% if request.is_secure %} 15 | https://{{ domain }}/accounts/activate/{{ code }}/ 16 | {% else %} 17 | https://{{ domain }}/accounts/activate/{{ code }}/ 18 | {% endif %} 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/templates/email/domain_expire_in_1_day.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | {{ subject }} 7 | 8 | 9 | 10 | 11 | 12 |

Hello, {{ person_name }}.

13 | 14 |

Domain {{ domain_name }} will expire in 24 hours.

15 | 16 |

Please sign in to {{ site_url }} and make a payment for the domains you wish to keep. 17 | See payments F.A.Q. for more information.

18 | 19 |

To stop receiving email notifications from AI domains registry please sign in to {{ site_url }}, 20 | go to your profile settings and disable "Email notifications enabled" checkbox.

21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/templates/email/domain_expire_in_3_days.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | {{ subject }} 7 | 8 | 9 | 10 | 11 | 12 |

Hello, {{ person_name }}.

13 | 14 |

Domain {{ domain_name }} will expire very soon on {{ domain_expiry_date }}.

15 | 16 |

Please sign in to {{ site_url }} and make a payment for the domains you wish to keep. 17 | See payments F.A.Q. for more information.

18 | 19 |

To stop receiving email notifications from AI domains registry please sign in to {{ site_url }}, 20 | go to your profile settings and disable "Email notifications enabled" checkbox.

21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/templates/email/domain_expire_in_5_days.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | {{ subject }} 7 | 8 | 9 | 10 | 11 | 12 |

Hello, {{ person_name }}.

13 | 14 |

Domain {{ domain_name }} will expire soon on {{ domain_expiry_date }}.

15 | 16 |

Please sign in to {{ site_url }} and make a payment for the domains you wish to keep. 17 | See payments F.A.Q. for more information.

18 | 19 |

To stop receiving email notifications from AI domains registry please sign in to {{ site_url }}, 20 | go to your profile settings and disable "Email notifications enabled" checkbox.

21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/templates/email/domain_expire_soon.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | {{ subject }} 7 | 8 | 9 | 10 | 11 | 12 |

Hello, {{ person_name }}.

13 | 14 |

Domain {{ domain_name }} will be expired {{ domain_expiry_date }}.

15 | 16 |

Please sign in to {{ site_url }} and make a payment for the domains you wish to keep. 17 | See payments F.A.Q. for more information.

18 | 19 |

To stop receiving email notifications from AI domains registry please sign in to {{ site_url }}, 20 | go to your profile settings and disable "Email notifications enabled" checkbox.

21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/templates/email/domain_expiring.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | {{ subject }} 7 | 8 | 9 | 10 | 11 | 12 |

Hello, {{ person_name }}.

13 | 14 |

Domain {{ domain_name }} will be expired {{ domain_expiry_date }}.

15 | 16 |

Please sign in to {{ site_url }} and make a payment for the domains you wish to keep. 17 | See payments F.A.Q. for more information.

18 | 19 |

To stop receiving email notifications from AI domains registry please sign in to {{ site_url }}, 20 | go to your profile settings and disable "Email notifications enabled" checkbox.

21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/templates/email/single_email.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | {{ subject }} 7 | 8 | 9 | 10 | 11 | 12 | {{ body }} 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/templates/email/test_announcement.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | {{ subject }} 7 | 8 | 9 | 10 | 11 | 12 |

Hello, {{ person_name }}.

13 | 14 |

This is a test email announcement from AI domains registry system.

15 | 16 |

To stop receiving email notifications from AI domains registry please sign in to {{ site_url }}, 17 | go to your profile settings and disable "Email notifications enabled" checkbox.

18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/templates/email/test_email.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | {{ subject }} 7 | 8 | 9 | 10 | 11 | 12 |

Hello!

13 | 14 |

This is a testing email from AI domains registry system.

15 | 16 |

To stop receiving email notifications from AI domains registry please sign in to {{ site_url }}, 17 | go to your profile settings and disable "Email notifications enabled" checkbox.

18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/tests/__init__.py -------------------------------------------------------------------------------- /src/tests/accounts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/tests/accounts/__init__.py -------------------------------------------------------------------------------- /src/tests/automats/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/tests/automats/__init__.py -------------------------------------------------------------------------------- /src/tests/back/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/tests/back/__init__.py -------------------------------------------------------------------------------- /src/tests/back/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/tests/back/management/__init__.py -------------------------------------------------------------------------------- /src/tests/back/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/tests/back/management/commands/__init__.py -------------------------------------------------------------------------------- /src/tests/back/management/commands/test_background_worker_daily.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.test import TestCase 3 | from mock import mock 4 | 5 | from back.management.commands.background_worker_daily import Command as DailyWorkerDjangoCommand, \ 6 | sync_to_be_deleted_domains_from_backend 7 | from tests.testsupport import prepare_tester_domain 8 | 9 | 10 | class TestCommand(TestCase): 11 | @mock.patch('back.management.commands.background_worker_daily.sync_to_be_deleted_domains_from_backend') 12 | def test_django_command(self, mock_backend_sync): 13 | DailyWorkerDjangoCommand().handle() 14 | mock_backend_sync.assert_called_once() 15 | 16 | @pytest.mark.django_db 17 | @mock.patch('zen.zmaster.domain_synchronize_from_backend') 18 | def test_sync_to_be_deleted_domains_from_backend(self, mock_backend_sync): 19 | prepare_tester_domain(domain_name='test.ai', domain_status='to_be_deleted') 20 | sync_to_be_deleted_domains_from_backend() 21 | mock_backend_sync.assert_called_once_with( 22 | domain_name='test.ai', 23 | refresh_contacts=True, 24 | rewrite_contacts=False, 25 | change_owner_allowed=False, 26 | create_new_owner_allowed=False, 27 | soft_delete=True, 28 | raise_errors=True, 29 | log_events=True, 30 | log_transitions=True, 31 | ) 32 | -------------------------------------------------------------------------------- /src/tests/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/tests/base/__init__.py -------------------------------------------------------------------------------- /src/tests/base/test_bruteforceprotection.py: -------------------------------------------------------------------------------- 1 | import mock 2 | import pytest 3 | from django.test import TestCase 4 | 5 | from base.exceptions import ExceededMaxAttemptsException 6 | from base.bruteforceprotection import BruteForceProtection 7 | 8 | 9 | class TestBruteForceProtection(TestCase): 10 | 11 | def setUp(self): 12 | self.brute_force_protection = BruteForceProtection( 13 | cache_key_prefix="test_hashkey_prefix", 14 | key="192.168.1.1", 15 | max_attempts=1, 16 | timeout=2 17 | ) 18 | 19 | def test_read_total_attempts(self): 20 | assert self.brute_force_protection.read_total_attempts() == 0 21 | 22 | @mock.patch('django.core.cache.cache.set') 23 | def test_increase_total_attempts(self, mock_cache_set): 24 | assert self.brute_force_protection.increase_total_attempts() == 1 25 | mock_cache_set.assert_called_once_with('test_hashkey_prefix_192.168.1.1', 1, timeout=2) 26 | 27 | @mock.patch('django.core.cache.cache.get') 28 | def test_register_attempt_returns_exception(self, mock_cache_get): 29 | mock_cache_get.return_value = 2 30 | with pytest.raises(ExceededMaxAttemptsException): 31 | self.brute_force_protection.register_attempt() 32 | -------------------------------------------------------------------------------- /src/tests/base/test_email.py: -------------------------------------------------------------------------------- 1 | import mock 2 | 3 | from base.email import send_email 4 | 5 | 6 | class TestEmail(object): 7 | @mock.patch("django.core.mail.message.EmailMultiAlternatives.send") 8 | def test_send_email_successful(self, mock_mail_send): 9 | send_email( 10 | subject="Subject", 11 | text_content="Text Content", 12 | from_email="noreply@example.com", 13 | to_email="receiver@example.com" 14 | ) 15 | 16 | mock_mail_send.assert_called_once() 17 | 18 | @mock.patch("logging.Logger.exception") 19 | @mock.patch("django.core.mail.message.EmailMultiAlternatives.send") 20 | def test_send_email_returns_exception(self, mock_mail_send, mock_log_exception): 21 | mock_mail_send.side_effect = Exception 22 | 23 | send_email( 24 | subject="Subject", 25 | text_content="Text Content", 26 | from_email="noreply@example.com", 27 | to_email="receiver@example.com" 28 | ) 29 | 30 | mock_log_exception.assert_called_once_with("Failed to send email") 31 | -------------------------------------------------------------------------------- /src/tests/base/test_mixins.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.test import TestCase 3 | 4 | from zen import zusers 5 | 6 | 7 | class BaseAuthTesterMixin(object): 8 | @pytest.mark.django_db 9 | def setUp(self): 10 | self.account = zusers.create_account('tester@zenaida.ai', account_password='123', is_active=True) 11 | self.client.login(email='tester@zenaida.ai', password='123') 12 | 13 | 14 | class TestStaffRequiredMixin(BaseAuthTesterMixin, TestCase): 15 | def test_user_is_staff(self): 16 | self.account.is_staff = True 17 | self.account.save() 18 | response = self.client.post('/board/financial-report/', data=dict(year=2019, month=1)) 19 | assert response.status_code == 200 20 | 21 | def test_user_is_not_staff(self): 22 | response = self.client.post('/board/financial-report/', data=dict(year=2019, month=1)) 23 | assert response.status_code == 302 24 | assert response.url == '/' 25 | -------------------------------------------------------------------------------- /src/tests/base/test_push_notifications.py: -------------------------------------------------------------------------------- 1 | import mock 2 | 3 | from base.push_notifications import PushNotificationService 4 | 5 | 6 | class TestPushNotificationService(object): 7 | @mock.patch("requests.post") 8 | def test_push(self, mock_post_request): 9 | mock_post_request.return_value = mock.MagicMock(status_code=200) 10 | notification = PushNotificationService(notification_message="test notification") 11 | assert notification.push() is True 12 | -------------------------------------------------------------------------------- /src/tests/billing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/tests/billing/__init__.py -------------------------------------------------------------------------------- /src/tests/billing/pay_4csonline/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/tests/billing/pay_4csonline/__init__.py -------------------------------------------------------------------------------- /src/tests/billing/pay_btcpay/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/tests/billing/pay_btcpay/__init__.py -------------------------------------------------------------------------------- /src/tests/billing/test_payments.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from billing import payments 4 | 5 | from tests import testsupport 6 | 7 | 8 | class TestPayments(TestCase): 9 | 10 | def test_latest_payment_found_started(self): 11 | tester_payment = testsupport.prepare_tester_payment() 12 | assert tester_payment == payments.latest_payment(owner=tester_payment.owner, status_in=['started', ]) 13 | assert tester_payment.transaction_id.startswith(str(tester_payment.owner.id) + '.') 14 | 15 | def test_latest_payment_not_found_processed(self): 16 | tester_payment = testsupport.prepare_tester_payment() 17 | assert payments.latest_payment(owner=tester_payment.owner, status_in=['processed', ]) is None 18 | 19 | def test_list_all_payments_of_specific_method(self): 20 | testsupport.prepare_tester_payment() 21 | assert len(payments.list_all_payments_of_specific_method(method='pay_4csonline')) == 1 22 | -------------------------------------------------------------------------------- /src/tests/board/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/tests/board/__init__.py -------------------------------------------------------------------------------- /src/tests/conftest.py: -------------------------------------------------------------------------------- 1 | from django import setup 2 | 3 | def pytest_configure(): 4 | setup() 5 | -------------------------------------------------------------------------------- /src/tests/front/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/tests/front/__init__.py -------------------------------------------------------------------------------- /src/tests/main/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/tests/main/__init__.py -------------------------------------------------------------------------------- /src/tests/main/test_apps.py: -------------------------------------------------------------------------------- 1 | 2 | from main import apps 3 | 4 | def test_ready(): 5 | import main 6 | assert apps.MainConfig('main', main).ready() is True 7 | -------------------------------------------------------------------------------- /src/tests/main/test_settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings as django_settings 2 | 3 | from main import settings as main_settings 4 | 5 | 6 | def test_settings(): 7 | assert main_settings.LOADED_OK == 'OK' 8 | assert django_settings.LOADED_OK == 'OK' 9 | -------------------------------------------------------------------------------- /src/tests/two_factor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/tests/two_factor/__init__.py -------------------------------------------------------------------------------- /src/tests/two_factor/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/tests/two_factor/management/__init__.py -------------------------------------------------------------------------------- /src/tests/two_factor/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/tests/two_factor/management/commands/__init__.py -------------------------------------------------------------------------------- /src/tests/two_factor/mixins.py: -------------------------------------------------------------------------------- 1 | from zen import zusers 2 | 3 | 4 | class UserMixin(object): 5 | def setUp(self): 6 | super().setUp() 7 | self._passwords = {} 8 | 9 | def create_user(self, email='test@example.com', password='secret', **kwargs): 10 | user = zusers.create_account(email=email, account_password=password, is_active=True) 11 | self._passwords[user] = password 12 | return user 13 | 14 | def enable_otp(self, user=None): 15 | if not user: 16 | user = list(self._passwords.keys())[0] 17 | return user.totpdevice_set.create(name='default') 18 | -------------------------------------------------------------------------------- /src/tests/zen/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/tests/zen/__init__.py -------------------------------------------------------------------------------- /src/two_factor/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'two_factor.apps.TwoFactorConfig' 2 | -------------------------------------------------------------------------------- /src/two_factor/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TwoFactorConfig(AppConfig): 5 | name = 'two_factor' 6 | verbose_name = "Django Two Factor Authentication" 7 | -------------------------------------------------------------------------------- /src/two_factor/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/two_factor/management/__init__.py -------------------------------------------------------------------------------- /src/two_factor/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/two_factor/management/commands/__init__.py -------------------------------------------------------------------------------- /src/two_factor/management/commands/two_factor_disable.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.core.management.base import BaseCommand, CommandError 3 | from django_otp import devices_for_user 4 | 5 | 6 | class Command(BaseCommand): 7 | """ 8 | Command for disabling two-factor authentication for certain users. 9 | 10 | The command accepts any number of usernames, and will remove all OTP 11 | devices for those users. 12 | 13 | Example usage:: 14 | 15 | manage.py two_factor_disable bouke steve 16 | """ 17 | help = 'Disables two-factor authentication for the given users' 18 | 19 | def add_arguments(self, parser): 20 | parser.add_argument('args', metavar='usernames', nargs='*') 21 | 22 | def handle(self, *usernames, **options): 23 | User = get_user_model() 24 | for username in usernames: 25 | try: 26 | user = User.objects.get_by_natural_key(username) 27 | except User.DoesNotExist: 28 | raise CommandError('User "%s" does not exist' % username) 29 | 30 | for device in devices_for_user(user): 31 | device.delete() 32 | -------------------------------------------------------------------------------- /src/two_factor/management/commands/two_factor_status.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.core.management.base import BaseCommand, CommandError 3 | 4 | from ...utils import default_device 5 | 6 | 7 | class Command(BaseCommand): 8 | """ 9 | Command to check two-factor authentication status for certain users. 10 | 11 | The command accepts any number of usernames, and will list if OTP is 12 | enabled or disabled for those users. 13 | 14 | Example usage:: 15 | 16 | manage.py two_factor_status bouke steve 17 | bouke: enabled 18 | steve: disabled 19 | """ 20 | help = 'Checks two-factor authentication status for the given users' 21 | 22 | def add_arguments(self, parser): 23 | parser.add_argument('args', metavar='usernames', nargs='*') 24 | 25 | def handle(self, *usernames, **options): 26 | User = get_user_model() 27 | for username in usernames: 28 | try: 29 | user = User.objects.get_by_natural_key(username) 30 | except User.DoesNotExist: 31 | raise CommandError('User "%s" does not exist' % username) 32 | 33 | self.stdout.write('%s: %s' % ( 34 | username, 35 | 'enabled' if default_device(user) else self.style.ERROR('disabled') 36 | )) 37 | -------------------------------------------------------------------------------- /src/two_factor/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, unicode_literals 2 | 3 | from django.utils.translation import ugettext_lazy as _ 4 | from django_otp.util import hex_validator 5 | 6 | 7 | def get_available_methods(): 8 | methods = [('generator', _('Token generator'))] 9 | return methods 10 | 11 | 12 | def key_validator(*args, **kwargs): 13 | """Wraps hex_validator generator, to keep makemigrations happy.""" 14 | return hex_validator()(*args, **kwargs) 15 | -------------------------------------------------------------------------------- /src/two_factor/signals.py: -------------------------------------------------------------------------------- 1 | from django.dispatch import Signal 2 | 3 | user_verified = Signal() 4 | -------------------------------------------------------------------------------- /src/two_factor/templates/two_factor/_base.html: -------------------------------------------------------------------------------- 1 | {% extends 'base/index.html' %} 2 | 3 | {% block main_content %} 4 | 5 | {% block content_wrapper %} 6 |
7 | {% block content %}{% endblock %} 8 |
9 | 10 | {% endblock %} 11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /src/two_factor/templates/two_factor/_base_focus.html: -------------------------------------------------------------------------------- 1 | {% extends "two_factor/_base.html" %} 2 | 3 | {% block content_wrapper %} 4 |
5 |
6 |
7 |
8 | {% block content %}{% endblock %} 9 |
10 |
11 |
12 |
13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /src/two_factor/templates/two_factor/_wizard_actions.html: -------------------------------------------------------------------------------- 1 | {% if cancel_url %} 2 | Cancel 4 | {% endif %} 5 | {% if wizard.steps.prev %} 6 | 9 | {% else %} 10 | 12 | {% endif %} 13 | 14 | -------------------------------------------------------------------------------- /src/two_factor/templates/two_factor/_wizard_forms.html: -------------------------------------------------------------------------------- 1 | 2 | {% bootstrap_form wizard.management_form %} 3 | {% bootstrap_form wizard.form %} 4 |
5 | -------------------------------------------------------------------------------- /src/two_factor/templates/two_factor/core/backup_tokens.html: -------------------------------------------------------------------------------- 1 | {% extends "two_factor/_base.html" %} 2 | 3 | {% block content %} 4 |

{% block title %}Backup Tokens{% endblock %}

5 |

Backup tokens can be used when your primary and backup 6 | phone numbers aren't available. The backup tokens below can be used 7 | for login verification. If you've used up all your backup tokens, you 8 | can generate a new set of backup tokens. Only the backup tokens shown 9 | below will be valid.

10 | 11 | {% if device.token_set.count %} 12 | 17 |

Print these tokens and keep them somewhere safe.

18 | {% else %} 19 |

You don't have any backup codes yet

20 | {% endif %} 21 | 22 |
{% csrf_token %}{{ form }} 23 | Back to Account Security 25 | 26 |
27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /src/two_factor/templates/two_factor/core/otp_required.html: -------------------------------------------------------------------------------- 1 | {% extends "two_factor/_base_focus.html" %} 2 | 3 | {% block content %} 4 |

{% block title %}Permission Denied{% endblock %}

5 | 6 |

The page you requested, enforces users to verify using 7 | two-factor authentication for security reasons. You need to enable these 8 | security features in order to access this page.

9 | 10 |

Two-factor authentication is not enabled for your 11 | account. Enable two-factor authentication for enhanced account 12 | security.

13 |

14 | Go back 16 | 17 | Enable Two-Factor Authentication 18 |

19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /src/two_factor/templates/two_factor/core/setup.html: -------------------------------------------------------------------------------- 1 | {% extends "two_factor/_base_focus.html" %} 2 | 3 | {% block content %} 4 |

{% block title %}Enable Two-Factor Authentication{% endblock %}

5 | {% if wizard.steps.current == 'welcome' %} 6 |

You are about to take your account security to the 7 | next level. Follow the steps in this wizard to enable two-factor 8 | authentication.

9 | {% elif wizard.steps.current == 'generator' %} 10 |

To start using a token generator, please use your 11 | smartphone to scan the QR code below. For example, use Google 12 | Authenticator. Then, enter the token generated by the app.

13 |

QR Code

14 | {% endif %} 15 | 16 |
{% csrf_token %} 17 | {% include "two_factor/_wizard_forms.html" %} 18 | 19 | {# hidden submit button to enable [enter] key #} 20 |
21 | 22 | {% if cancel_url %} 23 | Cancel 25 | {% endif %} 26 | 27 | 28 | 29 |
30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /src/two_factor/templates/two_factor/core/setup_complete.html: -------------------------------------------------------------------------------- 1 | {% extends "two_factor/_base_focus.html" %} 2 | {% load i18n %} 3 | 4 | {% block content %} 5 |

{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}

6 | 7 |

{% blocktrans %}Congratulations, you've successfully enabled two-factor 8 | authentication.{% endblocktrans %} 9 | 10 |

{% trans "Back to Account Security" %}

12 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /src/two_factor/templates/two_factor/profile/disable.html: -------------------------------------------------------------------------------- 1 | {% extends "two_factor/_base.html" %} 2 | 3 | {% block content %} 4 |

{% block title %}Disable Two-factor Authentication{% endblock %}

5 |

You are about to disable two-factor authentication. This 6 | compromises your account security, are you sure?

7 |
8 | {% csrf_token %} 9 | {{ form.understand.label }}: {{ form.understand }}
10 |
11 | 13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /src/two_factor/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/two_factor/templatetags/__init__.py -------------------------------------------------------------------------------- /src/two_factor/templatetags/two_factor.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | 4 | register = template.Library() 5 | -------------------------------------------------------------------------------- /src/two_factor/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | 3 | from two_factor.views import ( 4 | BackupTokensView, DisableView, LoginView, 5 | TwoFactorProfileView, QRGeneratorView, SetupCompleteView, SetupView, 6 | ) 7 | 8 | core = [ 9 | re_path( 10 | r'^accounts/login/$', 11 | view=LoginView.as_view(), 12 | name='login', 13 | ), 14 | re_path( 15 | r'^account/two_factor/setup/$', 16 | view=SetupView.as_view(), 17 | name='setup', 18 | ), 19 | re_path( 20 | r'^account/two_factor/qrcode/$', 21 | view=QRGeneratorView.as_view(), 22 | name='qr', 23 | ), 24 | re_path( 25 | r'^account/two_factor/setup/complete/$', 26 | view=SetupCompleteView.as_view(), 27 | name='setup_complete', 28 | ), 29 | re_path( 30 | r'^account/two_factor/backup/tokens/$', 31 | view=BackupTokensView.as_view(), 32 | name='backup_tokens', 33 | ), 34 | ] 35 | 36 | two_factor_profile = [ 37 | re_path( 38 | r'^account/two_factor/$', 39 | view=TwoFactorProfileView.as_view(), 40 | name='profile', 41 | ), 42 | re_path( 43 | r'^account/two_factor/disable/$', 44 | view=DisableView.as_view(), 45 | name='disable', 46 | ), 47 | ] 48 | 49 | urlpatterns = (core + two_factor_profile, 'two_factor') 50 | -------------------------------------------------------------------------------- /src/two_factor/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import ( 2 | BackupTokensView, LoginView, QRGeneratorView, SetupCompleteView, SetupView, 3 | ) 4 | from .mixins import OTPRequiredMixin 5 | from .profile import DisableView, TwoFactorProfileView 6 | -------------------------------------------------------------------------------- /src/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import django 4 | from django import http 5 | from django.core.handlers.wsgi import WSGIHandler, WSGIRequest, get_str_from_wsgi 6 | from django.utils.functional import cached_property 7 | 8 | # Directly assign to os.environ instead of using os.environ.setdefault as the former plays nice 9 | # with having multiple django sites run from one WSGIProcessGroup, as done on test server. 10 | # There seems to be no use case where the DJANGO_SETTINGS_MODULE needs to be defined elsewhere. 11 | # See comment in default Django project wsgi 12 | os.environ["DJANGO_SETTINGS_MODULE"] = "main.settings" 13 | 14 | 15 | # Bug in python not reading cookies that are not properly escaped or have colons in the name. 16 | # http://bugs.python.org/issue22931 17 | # https://code.djangoproject.com/ticket/24492 18 | 19 | 20 | class MyWSGIRequest(WSGIRequest): 21 | 22 | @cached_property 23 | def COOKIES(self): 24 | cookies = dict() 25 | raw_cookie = get_str_from_wsgi(self.environ, 'HTTP_COOKIE', '') 26 | for cookie in [cookie for cookie in raw_cookie.split(';')]: 27 | cookies.update(http.parse_cookie(cookie)) 28 | return cookies 29 | 30 | 31 | class MyWSGIHandler(WSGIHandler): 32 | request_class = MyWSGIRequest 33 | 34 | 35 | def get_wsgi_application(): 36 | django.setup() 37 | return MyWSGIHandler() 38 | 39 | 40 | application = get_wsgi_application() 41 | -------------------------------------------------------------------------------- /src/zen/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datahaven-net/zenaida/87d7ed4299a9dbf41cc7c76f8f5745b7b5a77ba3/src/zen/__init__.py -------------------------------------------------------------------------------- /src/zen/zzones.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from back.models.zone import Zone 4 | 5 | from django.conf import settings 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | def is_supported(tld_zone_name): 11 | """ 12 | Return `True` if given zone is supported by this Zenaida host. 13 | """ 14 | return tld_zone_name in settings.ZENAIDA_SUPPORTED_ZONES 15 | 16 | 17 | def is_exist(tld_zone_name): 18 | """ 19 | Return `True` if such zone exists, doing query in Zone table. 20 | """ 21 | return bool(Zone.zones.filter(name=tld_zone_name).first()) 22 | 23 | 24 | def make(tld_zone_name): 25 | """ 26 | Creates new zone with given tld name if it not exists, otherwise returns existing object. 27 | """ 28 | if not is_supported(tld_zone_name): 29 | raise ValueError('Zone "%s" is not supported by the server' % tld_zone_name) 30 | zon_obj = Zone.zones.filter(name=tld_zone_name).first() 31 | if not zon_obj: 32 | zon_obj = Zone(name=tld_zone_name) 33 | zon_obj.save() 34 | logger.info('new zone created: %r', zon_obj) 35 | return zon_obj 36 | --------------------------------------------------------------------------------