Renew with 2FA and Podman
This commit is contained in:
5
Makefile
5
Makefile
@@ -8,7 +8,10 @@ stop:
|
||||
docker-compose --env-file=stack.env down
|
||||
|
||||
log:
|
||||
docker-compose --env-file=stack.env logs -f
|
||||
docker-compose --env-file=stack.env logs -f --names
|
||||
|
||||
test:
|
||||
docker-compose --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py test $(MODULES) -v 2"
|
||||
|
||||
migrate:
|
||||
docker-compose --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py migrate"
|
||||
|
||||
@@ -13,7 +13,7 @@ ALLOWED_HOSTS = getenv("ALLOWED_HOSTS", f"127.0.0.1,{DOMAIN}").split(",")
|
||||
CSRF_TRUSTED_ORIGINS = getenv("CSRF_TRUSTED_ORIGINS", URL).split(",")
|
||||
|
||||
# Stripe
|
||||
STRIPE_ENABLED = getenv("STRIPE_ENABLED", "false").lower() in trues
|
||||
BILLING_ENABLED = getenv("BILLING_ENABLED", "false").lower() in trues
|
||||
STRIPE_TEST = getenv("STRIPE_TEST", "true") in trues
|
||||
STRIPE_API_KEY_TEST = getenv("STRIPE_API_KEY_TEST", "")
|
||||
STRIPE_PUBLIC_API_KEY_TEST = getenv("STRIPE_PUBLIC_API_KEY_TEST", "")
|
||||
@@ -27,6 +27,12 @@ SECRET_KEY = getenv("SECRET_KEY", "")
|
||||
|
||||
STRIPE_ADMIN_COUPON = getenv("STRIPE_ADMIN_COUPON", "")
|
||||
|
||||
REGISTRATION_OPEN = getenv("REGISTRATION_OPEN", "false").lower() in trues
|
||||
|
||||
LAGO_API_KEY = getenv("LAGO_API_KEY", "")
|
||||
LAGO_ORG_ID = getenv("LAGO_ORG_ID", "")
|
||||
LAGO_URL = getenv("LAGO_URL", "")
|
||||
|
||||
DEBUG = getenv("DEBUG", "false") in trues
|
||||
PROFILER = getenv("PROFILER", "false") in trues
|
||||
|
||||
@@ -39,4 +45,4 @@ if DEBUG:
|
||||
"10.0.2.2",
|
||||
]
|
||||
|
||||
SETTINGS_EXPORT = ["STRIPE_ENABLED"]
|
||||
SETTINGS_EXPORT = ["BILLING_ENABLED"]
|
||||
|
||||
@@ -30,6 +30,7 @@ ALLOWED_HOSTS = []
|
||||
INSTALLED_APPS = [
|
||||
"core",
|
||||
"django.contrib.admin",
|
||||
# 'core.apps.LibraryAdminConfig', # our custom OTP'ed admin
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
@@ -42,7 +43,39 @@ INSTALLED_APPS = [
|
||||
"crispy_bulma",
|
||||
# "django_tables2",
|
||||
# "django_tables2_bulma_template",
|
||||
"django_otp",
|
||||
"django_otp.plugins.otp_totp",
|
||||
# "django_otp.plugins.otp_email",
|
||||
# 'django_otp.plugins.otp_hotp',
|
||||
"django_otp.plugins.otp_static",
|
||||
"two_factor",
|
||||
# "two_factor.plugins.phonenumber",
|
||||
# "two_factor.plugins.email",
|
||||
# "two_factor.plugins.yubikey",
|
||||
# "otp_yubikey",
|
||||
"mixins",
|
||||
"cachalot",
|
||||
]
|
||||
|
||||
# Performance optimisations
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
# "LOCATION": "unix:///var/run/socks/redis.sock",
|
||||
# "LOCATION": f"redis://{REDIS_HOST}:{REDIS_PORT}",
|
||||
"LOCATION": "unix:///var/run/envelope-redis.sock",
|
||||
"OPTIONS": {
|
||||
"db": 10,
|
||||
# "parser_class": "django_redis.cache.RedisCache",
|
||||
# "PASSWORD": REDIS_PASSWORD,
|
||||
"pool_class": "redis.BlockingConnectionPool",
|
||||
},
|
||||
}
|
||||
}
|
||||
# CACHE_MIDDLEWARE_ALIAS = 'default'
|
||||
# CACHE_MIDDLEWARE_SECONDS = '600'
|
||||
# CACHE_MIDDLEWARE_KEY_PREFIX = ''
|
||||
|
||||
CRISPY_TEMPLATE_PACK = "bulma"
|
||||
CRISPY_ALLOWED_TEMPLATE_PACKS = ("bulma",)
|
||||
DJANGO_TABLES2_TEMPLATE = "django-tables2/bulma.html"
|
||||
@@ -51,9 +84,12 @@ MIDDLEWARE = [
|
||||
"debug_toolbar.middleware.DebugToolbarMiddleware",
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
# 'django.middleware.cache.UpdateCacheMiddleware',
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
# 'django.middleware.cache.FetchFromCacheMiddleware',
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django_otp.middleware.OTPMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"django_htmx.middleware.HtmxMiddleware",
|
||||
@@ -132,7 +168,9 @@ AUTH_USER_MODEL = "core.User"
|
||||
|
||||
LOGIN_REDIRECT_URL = "home"
|
||||
LOGOUT_REDIRECT_URL = "home"
|
||||
LOGIN_URL = "/accounts/login/"
|
||||
# 2FA
|
||||
LOGIN_URL = "two_factor:login"
|
||||
# LOGIN_REDIRECT_URL = 'two_factor:profile'
|
||||
|
||||
# ALLOWED_PAYMENT_METHODS = ["bacs_debit", "card"]
|
||||
ALLOWED_PAYMENT_METHODS = ["card"]
|
||||
@@ -164,6 +202,7 @@ DEBUG_TOOLBAR_PANELS = [
|
||||
"debug_toolbar.panels.logging.LoggingPanel",
|
||||
"debug_toolbar.panels.redirects.RedirectsPanel",
|
||||
"debug_toolbar.panels.profiling.ProfilingPanel",
|
||||
"cachalot.panels.CachalotPanel",
|
||||
]
|
||||
|
||||
from app.local_settings import * # noqa
|
||||
@@ -179,3 +218,12 @@ if PROFILER: # noqa - trust me its there
|
||||
# "region": f'{os.getenv("REGION")}',
|
||||
# }
|
||||
)
|
||||
|
||||
|
||||
def show_toolbar(request):
|
||||
return DEBUG # noqa: from local imports
|
||||
|
||||
|
||||
DEBUG_TOOLBAR_CONFIG = {
|
||||
"SHOW_TOOLBAR_CALLBACK": show_toolbar,
|
||||
}
|
||||
|
||||
31
app/urls.py
31
app/urls.py
@@ -16,32 +16,25 @@ Including another URLconf
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.views import LogoutView
|
||||
from django.urls import include, path
|
||||
from django.views.generic import TemplateView
|
||||
from two_factor.urls import urlpatterns as tf_urls
|
||||
|
||||
from core.views import base, demo
|
||||
from core.views.callbacks import Callback
|
||||
from core.views import base, notifications
|
||||
|
||||
urlpatterns = [
|
||||
path("__debug__/", include("debug_toolbar.urls")),
|
||||
path("", base.Home.as_view(), name="home"),
|
||||
path("callback", Callback.as_view(), name="callback"),
|
||||
path("billing/", base.Billing.as_view(), name="billing"),
|
||||
path("order/<str:plan_name>/", base.Order.as_view(), name="order"),
|
||||
path(
|
||||
"cancel_subscription/<str:plan_name>/",
|
||||
base.Cancel.as_view(),
|
||||
name="cancel_subscription",
|
||||
),
|
||||
path(
|
||||
"success/", TemplateView.as_view(template_name="success.html"), name="success"
|
||||
),
|
||||
path("cancel/", TemplateView.as_view(template_name="cancel.html"), name="cancel"),
|
||||
path("portal", base.Portal.as_view(), name="portal"),
|
||||
path("admin/", admin.site.urls),
|
||||
path("accounts/", include("django.contrib.auth.urls")),
|
||||
# 2FA login urls
|
||||
path("", include(tf_urls)),
|
||||
path("accounts/signup/", base.Signup.as_view(), name="signup"),
|
||||
path("demo/modal/", demo.DemoModal.as_view(), name="modal"),
|
||||
path("demo/widget/", demo.DemoWidget.as_view(), name="widget"),
|
||||
path("demo/window/", demo.DemoWindow.as_view(), name="window"),
|
||||
path("accounts/logout/", LogoutView.as_view(), name="logout"),
|
||||
# Notifications
|
||||
path(
|
||||
"notifications/<str:type>/update/",
|
||||
notifications.NotificationsUpdate.as_view(),
|
||||
name="notifications_update",
|
||||
),
|
||||
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
|
||||
import stripe
|
||||
# import stripe
|
||||
from django.conf import settings
|
||||
|
||||
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
|
||||
@@ -8,7 +8,7 @@ os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
|
||||
|
||||
# r = StrictRedis(unix_socket_path="/var/run/redis/redis.sock", db=0)
|
||||
|
||||
if settings.STRIPE_TEST:
|
||||
stripe.api_key = settings.STRIPE_API_KEY_TEST
|
||||
else:
|
||||
stripe.api_key = settings.STRIPE_API_KEY_PROD
|
||||
# if settings.STRIPE_TEST:
|
||||
# stripe.api_key = settings.STRIPE_API_KEY_TEST
|
||||
# else:
|
||||
# stripe.api_key = settings.STRIPE_API_KEY_PROD
|
||||
|
||||
@@ -2,32 +2,35 @@ from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
|
||||
from .forms import CustomUserCreationForm
|
||||
from .models import Plan, Session, User
|
||||
from .models import NotificationSettings, User
|
||||
|
||||
|
||||
# Register your models here.
|
||||
class CustomUserAdmin(UserAdmin):
|
||||
list_filter = ["plans"]
|
||||
# list_filter = ["plans"]
|
||||
model = User
|
||||
add_form = CustomUserCreationForm
|
||||
fieldsets = (
|
||||
*UserAdmin.fieldsets,
|
||||
(
|
||||
"Stripe information",
|
||||
{"fields": ("stripe_id",)},
|
||||
),
|
||||
(
|
||||
"Payment information",
|
||||
{
|
||||
"fields": (
|
||||
"plans",
|
||||
"last_payment",
|
||||
)
|
||||
},
|
||||
"Billing information",
|
||||
{"fields": ("billing_provider_id", "customer_id", "stripe_id")},
|
||||
),
|
||||
# (
|
||||
# "Payment information",
|
||||
# {
|
||||
# "fields": (
|
||||
# # "plans",
|
||||
# "last_payment",
|
||||
# )
|
||||
# },
|
||||
# ),
|
||||
)
|
||||
|
||||
|
||||
class NotificationSettingsAdmin(admin.ModelAdmin):
|
||||
list_display = ("user", "ntfy_topic", "ntfy_url")
|
||||
|
||||
|
||||
admin.site.register(User, CustomUserAdmin)
|
||||
admin.site.register(Plan)
|
||||
admin.site.register(Session)
|
||||
admin.site.register(NotificationSettings, NotificationSettingsAdmin)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from django.forms import ModelForm
|
||||
from mixins.restrictions import RestrictedFormMixin
|
||||
|
||||
from .models import User
|
||||
from .models import NotificationSettings, User
|
||||
|
||||
# Create your forms here.
|
||||
|
||||
@@ -28,6 +30,19 @@ class NewUserForm(UserCreationForm):
|
||||
return user
|
||||
|
||||
|
||||
class NotificationSettingsForm(RestrictedFormMixin, ModelForm):
|
||||
class Meta:
|
||||
model = NotificationSettings
|
||||
fields = (
|
||||
"ntfy_topic",
|
||||
"ntfy_url",
|
||||
)
|
||||
help_texts = {
|
||||
"ntfy_topic": "The topic to send notifications to.",
|
||||
"ntfy_url": "Custom NTFY server. Leave blank to use the default server.",
|
||||
}
|
||||
|
||||
|
||||
class CustomUserCreationForm(UserCreationForm):
|
||||
class Meta:
|
||||
model = User
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import logging
|
||||
|
||||
import stripe
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def expand_name(first_name, last_name):
|
||||
"""
|
||||
Convert two name variables into one.
|
||||
Last name without a first name is ignored.
|
||||
"""
|
||||
name = None
|
||||
if first_name:
|
||||
name = first_name
|
||||
# We only want to put the last name if we have a first name
|
||||
if last_name:
|
||||
name += f" {last_name}"
|
||||
return name
|
||||
|
||||
|
||||
def get_or_create(email, first_name, last_name):
|
||||
"""
|
||||
Get a customer ID from Stripe if one with the given email exists.
|
||||
Create a customer if one does not.
|
||||
Raise an exception if two or more customers matching the given email exist.
|
||||
"""
|
||||
# Let's see if we're just missing the ID
|
||||
matching_customers = stripe.Customer.list(email=email, limit=2)
|
||||
if len(matching_customers) == 2:
|
||||
# Something is horribly wrong
|
||||
logger.error(f"Two customers found for email {email}")
|
||||
raise Exception(f"Two customers found for email {email}")
|
||||
|
||||
elif len(matching_customers) == 1:
|
||||
# We found a customer. Let's copy the ID
|
||||
customer = matching_customers["data"][0]
|
||||
customer_id = customer["id"]
|
||||
return customer_id
|
||||
|
||||
else:
|
||||
# We didn't find anything. Create the customer
|
||||
|
||||
# Create a name, since we have 2 variables which could be null
|
||||
name = expand_name(first_name, last_name)
|
||||
cast = {"email": email}
|
||||
if name:
|
||||
cast["name"] = name
|
||||
customer = stripe.Customer.create(**cast)
|
||||
logger.info(f"Created new Stripe customer {customer.id} with email {email}")
|
||||
|
||||
return customer.id
|
||||
|
||||
|
||||
def update_customer_fields(stripe_id, email=None, first_name=None, last_name=None):
|
||||
"""
|
||||
Update the customer fields in Stripe.
|
||||
"""
|
||||
if email:
|
||||
stripe.Customer.modify(stripe_id, email=email)
|
||||
logger.info(f"Modified Stripe customer {stripe_id} to have email {email}")
|
||||
if first_name or last_name:
|
||||
name = expand_name(first_name, last_name)
|
||||
stripe.Customer.modify(stripe_id, name=name)
|
||||
logger.info(f"Modified Stripe customer {stripe_id} to have email {name}")
|
||||
38
core/lib/notify.py
Normal file
38
core/lib/notify.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import requests
|
||||
|
||||
from core.util import logs
|
||||
|
||||
NTFY_URL = "https://ntfy.sh"
|
||||
|
||||
log = logs.get_logger(__name__)
|
||||
|
||||
|
||||
# Actual function to send a message to a topic
|
||||
def raw_sendmsg(msg, title=None, priority=None, tags=None, url=None, topic=None):
|
||||
if url is None:
|
||||
url = NTFY_URL
|
||||
headers = {"Title": "GIA"}
|
||||
if title:
|
||||
headers["Title"] = title
|
||||
if priority:
|
||||
headers["Priority"] = priority
|
||||
if tags:
|
||||
headers["Tags"] = tags
|
||||
requests.post(
|
||||
f"{url}/{topic}",
|
||||
data=msg,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
||||
# Sendmsg helper to send a message to a user's notification settings
|
||||
def sendmsg(user, *args, **kwargs):
|
||||
notification_settings = user.get_notification_settings()
|
||||
|
||||
if notification_settings.ntfy_topic is None:
|
||||
# No topic set, so don't send
|
||||
return
|
||||
else:
|
||||
topic = notification_settings.ntfy_topic
|
||||
|
||||
raw_sendmsg(*args, **kwargs, url=notification_settings.ntfy_url, topic=topic)
|
||||
@@ -1,19 +0,0 @@
|
||||
from core.models import Plan
|
||||
|
||||
|
||||
def assemble_plan_map(product_id_filter=None):
|
||||
"""
|
||||
Get all the plans from the database and create an object Stripe wants.
|
||||
"""
|
||||
line_items = []
|
||||
for plan in Plan.objects.all():
|
||||
if product_id_filter:
|
||||
if plan.product_id != product_id_filter:
|
||||
continue
|
||||
line_items.append(
|
||||
{
|
||||
"price": plan.product_id,
|
||||
"quantity": 1,
|
||||
}
|
||||
)
|
||||
return line_items
|
||||
0
core/management/__init__.py
Normal file
0
core/management/__init__.py
Normal file
0
core/management/commands/__init__.py
Normal file
0
core/management/commands/__init__.py
Normal file
11
core/management/commands/processing.py
Normal file
11
core/management/commands/processing.py
Normal file
@@ -0,0 +1,11 @@
|
||||
import msgpack
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.conf import settings
|
||||
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger("processing")
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
...
|
||||
41
core/management/commands/scheduling.py
Normal file
41
core/management/commands/scheduling.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import asyncio
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from asgiref.sync import sync_to_async
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger("scheduling")
|
||||
|
||||
INTERVALS = [5, 60, 900, 1800, 3600, 14400, 86400]
|
||||
|
||||
|
||||
async def job(interval_seconds):
|
||||
"""
|
||||
Run all schedules matching the given interval.
|
||||
:param interval_seconds: The interval to run.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
"""
|
||||
Start the scheduling process.
|
||||
"""
|
||||
scheduler = AsyncIOScheduler()
|
||||
for interval in INTERVALS:
|
||||
log.debug(f"Scheduling {interval} second job")
|
||||
scheduler.add_job(job, "interval", seconds=interval, args=[interval])
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
scheduler._eventloop = loop
|
||||
scheduler.start()
|
||||
try:
|
||||
loop.run_forever()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
log.info("Process terminating")
|
||||
finally:
|
||||
loop.close()
|
||||
@@ -1,4 +1,6 @@
|
||||
# Generated by Django 4.0.6 on 2022-07-10 19:54
|
||||
# Generated by Django 4.2.6 on 2023-10-17 19:26
|
||||
|
||||
import uuid
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
@@ -31,9 +33,11 @@ class Migration(migrations.Migration):
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('stripe_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('last_payment', models.DateTimeField(blank=True, null=True)),
|
||||
('customer_id', models.UUIDField(blank=True, default=uuid.uuid4, null=True)),
|
||||
('billing_provider_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('email', models.EmailField(max_length=254, unique=True)),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'user',
|
||||
@@ -45,34 +49,12 @@ class Migration(migrations.Migration):
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Plan',
|
||||
name='NotificationSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, unique=True)),
|
||||
('description', models.CharField(blank=True, max_length=1024, null=True)),
|
||||
('cost', models.IntegerField()),
|
||||
('product_id', models.CharField(blank=True, max_length=255, null=True, unique=True)),
|
||||
('image', models.CharField(blank=True, max_length=1024, null=True)),
|
||||
('ntfy_topic', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('ntfy_url', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Session',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('request', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('subscription_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('plan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='core.plan')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='plans',
|
||||
field=models.ManyToManyField(blank=True, to='core.plan'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='user_permissions',
|
||||
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.0.6 on 2022-10-12 09:08
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='session',
|
||||
name='session',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,77 +1,35 @@
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
import stripe
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
|
||||
from core.lib.customers import get_or_create, update_customer_fields
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Plan(models.Model):
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
description = models.CharField(max_length=1024, null=True, blank=True)
|
||||
cost = models.IntegerField()
|
||||
product_id = models.CharField(max_length=255, unique=True, null=True, blank=True)
|
||||
image = models.CharField(max_length=1024, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} (£{self.cost})"
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
# Stripe customer ID
|
||||
stripe_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
last_payment = models.DateTimeField(null=True, blank=True)
|
||||
plans = models.ManyToManyField(Plan, blank=True)
|
||||
customer_id = models.UUIDField(default=uuid.uuid4, null=True, blank=True)
|
||||
billing_provider_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
email = models.EmailField(unique=True)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._original = self
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Override the save function to create a Stripe customer.
|
||||
"""
|
||||
if settings.STRIPE_ENABLED:
|
||||
if not self.stripe_id: # stripe ID not stored
|
||||
self.stripe_id = get_or_create(
|
||||
self.email, self.first_name, self.last_name
|
||||
)
|
||||
|
||||
to_update = {}
|
||||
if self.email != self._original.email:
|
||||
to_update["email"] = self.email
|
||||
if self.first_name != self._original.first_name:
|
||||
to_update["first_name"] = self.first_name
|
||||
if self.last_name != self._original.last_name:
|
||||
to_update["last_name"] = self.last_name
|
||||
|
||||
update_customer_fields(self.stripe_id, **to_update)
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
if settings.STRIPE_ENABLED:
|
||||
if self.stripe_id:
|
||||
stripe.Customer.delete(self.stripe_id)
|
||||
logger.info(f"Deleted Stripe customer {self.stripe_id}")
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
def has_plan(self, plan):
|
||||
plan_list = [plan.name for plan in self.plans.all()]
|
||||
return plan in plan_list
|
||||
def get_notification_settings(self):
|
||||
return NotificationSettings.objects.get_or_create(user=self)[0]
|
||||
|
||||
|
||||
class Session(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
request = models.CharField(max_length=255, null=True, blank=True)
|
||||
session = models.CharField(max_length=255, null=True, blank=True)
|
||||
subscription_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
plan = models.ForeignKey(Plan, null=True, blank=True, on_delete=models.CASCADE)
|
||||
class NotificationSettings(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
ntfy_topic = models.CharField(max_length=255, null=True, blank=True)
|
||||
ntfy_url = models.CharField(max_length=255, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Notification settings for {self.user}"
|
||||
|
||||
|
||||
# class Perms(models.Model):
|
||||
|
||||
4
core/static/css/bulma.min.css
vendored
4
core/static/css/bulma.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -6,16 +6,24 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>XF - {{ request.path_info }}</title>
|
||||
<title>GIA - {{ request.path_info }}</title>
|
||||
<link rel="shortcut icon" href="{% static 'favicon.ico' %}">
|
||||
<link rel="manifest" href="{% static 'manifest.webmanifest' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma-tooltip.min.css' %}">
|
||||
<link rel="stylesheet" href="https://site-assets.fontawesome.com/releases/v6.1.1/css/all.css">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma-slider.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma-calendar.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma-tagsinput.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma-switch.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/gridstack.min.css' %}">
|
||||
<script src="{% static 'js/bulma-calendar.min.js' %}" integrity="sha384-DThNif0xGXbopX7+PE+UabkuClfI/zELNhaVqoGLutaWB76dyMw0vIQBGmUxSfVQ" crossorigin="anonymous"></script>
|
||||
<script src="{% static 'js/bulma-slider.min.js' %}" integrity="sha384-wbyps8iLG8QzJE02viYc/27BtT5HSa11+b5V7QPR1/huVuA8f4LRTNGc82qAIeIZ" crossorigin="anonymous"></script>
|
||||
<script src="{% static 'js/htmx.min.js' %}" integrity="sha384-cZuAZ+ZbwkNRnrKi05G/fjBX+azI9DNOkNYysZ0I/X5ZFgsmMiBXgDZof30F5ofc" crossorigin="anonymous"></script>
|
||||
<script defer src="{% static 'js/hyperscript.min.js' %}" integrity="sha384-6GYN8BDHOJkkru6zcpGOUa//1mn+5iZ/MyT6mq34WFIpuOeLF52kSi721q0SsYF9" crossorigin="anonymous"></script>
|
||||
<script defer src="{% static 'js/remove-me.js' %}" integrity="sha384-6fHcFNoQ8QEI3ZDgw9Z/A6Brk64gF7AnFbLgdrumo8/kBbsKQ/wo7wPegj5WkzuG" crossorigin="anonymous"></script>
|
||||
<script defer src="{% static 'js/hyperscript.min.js' %}" integrity="sha384-6GYN8BDHOJkkru6zcpGOUa//1mn+5iZ/MyT6mq34WFIpuOeLF52kSi721q0SsYF9" crossorigin="anonymous"></script>
|
||||
<script src="{% static 'js/bulma-tagsinput.min.js' %}"></script>
|
||||
<script src="{% static 'js/jquery.min.js' %}"></script>
|
||||
<script src="{% static 'js/gridstack-all.js' %}"></script>
|
||||
<script defer src="{% static 'js/magnet.min.js' %}"></script>
|
||||
<script>
|
||||
@@ -112,12 +120,37 @@
|
||||
cursor:pointer;
|
||||
background-color:rgba(221, 224, 255, 0.3) !important;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background-color: rgba(84, 84, 84, 0.9) !important;
|
||||
--modal-color: rgba(81, 81, 81, 0.9) !important;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--background-color-light: rgba(210, 210, 210, 0.9) !important;
|
||||
--background-color-dark: rgba(84, 84, 84, 0.9) !important;
|
||||
--background-color-modal-light: rgba(250, 250, 250, 0.5) !important;
|
||||
--background-color-modal-dark: rgba(210, 210, 210, 0.9) !important;
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--background-color: var(--background-color-light);
|
||||
--modal-color: var(--background-color-modal-light);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--background-color: var(--background-color-dark);
|
||||
--modal-color: var(--background-color-modal-dark);
|
||||
}
|
||||
|
||||
.panel, .box, .modal {
|
||||
background-color:rgba(250, 250, 250, 0.5) !important;
|
||||
/* background-color:rgba(250, 250, 250, 0.5) !important; */
|
||||
background-color: var(--modal-color) !important;
|
||||
}
|
||||
.modal, .modal.box{
|
||||
background-color:rgba(210, 210, 210, 0.9) !important;
|
||||
/* background-color:rgba(210, 210, 210, 0.9) !important; */
|
||||
background-color: var(--background-color) !important;
|
||||
}
|
||||
.modal-background{
|
||||
background-color:rgba(255, 255, 255, 0.3) !important;
|
||||
@@ -201,25 +234,34 @@
|
||||
<a class="navbar-item" href="{% url 'home' %}">
|
||||
Home
|
||||
</a>
|
||||
{% if settings.STRIPE_ENABLED %}
|
||||
{% if user.is_authenticated %}
|
||||
<a class="navbar-item" href="{% url 'billing' %}">
|
||||
Billing
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if user.is_superuser %}
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
Admin
|
||||
Account
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="{% url 'two_factor:profile' %}">
|
||||
Security
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'notifications_update' type='page' %}">
|
||||
Notifications
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
Services
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="#">
|
||||
Admin1
|
||||
Signal
|
||||
</a>
|
||||
<a class="navbar-item" href="#">
|
||||
Admin2
|
||||
Instagram
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -236,7 +278,7 @@
|
||||
<a class="button is-info" href="{% url 'signup' %}">
|
||||
<strong>Sign up</strong>
|
||||
</a>
|
||||
<a class="button is-light" href="{% url 'login' %}">
|
||||
<a class="button" href="{% url 'two_factor:login' %}">
|
||||
Log in
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -283,8 +325,16 @@
|
||||
{% endblock %}
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
{% block content %}
|
||||
{% block content_wrapper %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
<div id="modals-here">
|
||||
</div>
|
||||
<div id="windows-here">
|
||||
</div>
|
||||
<div id="widgets-here" style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<article class="panel is-info">
|
||||
<p class="panel-heading">
|
||||
User information
|
||||
</p>
|
||||
<a class="panel-block is-active">
|
||||
<span class="panel-icon">
|
||||
<i class="fas fa-id-card" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="tag is-info">{{ user.first_name }} {{ user.last_name }}</span>
|
||||
</a>
|
||||
<a class="panel-block">
|
||||
<span class="panel-icon">
|
||||
<i class="fas fa-binary" aria-hidden="true"></i>
|
||||
</span>
|
||||
{% for plan in user.plans.all %}
|
||||
<span class="tag is-info">{{ plan.name }}</span>
|
||||
{% endfor %}
|
||||
</a>
|
||||
<a class="panel-block">
|
||||
<span class="panel-icon">
|
||||
<i class="fas fa-credit-card" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span class="tag">{{ user.last_payment }}</span>
|
||||
</a>
|
||||
<a class="panel-block" href="{% url 'portal' %}">
|
||||
<span class="panel-icon">
|
||||
<i class="fa-brands fa-stripe-s" aria-hidden="true"></i>
|
||||
</span>
|
||||
Subscription management
|
||||
</a>
|
||||
</article>
|
||||
{% include "partials/product-list.html" %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,19 +4,6 @@
|
||||
{% block outer_content %}
|
||||
|
||||
<div class="grid-stack" id="grid-stack-main">
|
||||
<div class="grid-stack-item" gs-w="7" gs-h="10" gs-y="0" gs-x="1">
|
||||
<div class="grid-stack-item-content">
|
||||
<nav class="panel">
|
||||
<p class="panel-heading" style="padding: .2em; line-height: .5em;">
|
||||
<i class="fa-solid fa-arrows-up-down-left-right has-text-grey-light"></i>
|
||||
Home
|
||||
</p>
|
||||
<article class="panel-block is-active">
|
||||
{% include 'window-content/main.html' %}
|
||||
</article>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -34,62 +21,73 @@
|
||||
|
||||
// a widget is ready to be loaded
|
||||
document.addEventListener('load-widget', function(event) {
|
||||
let container = htmx.find('#widget');
|
||||
// get the scripts, they won't be run on the new element so we need to eval them
|
||||
var scripts = htmx.findAll(container, "script");
|
||||
let widgetelement = container.firstElementChild.cloneNode(true);
|
||||
var new_id = widgetelement.id;
|
||||
let containers = htmx.findAll('#widget');
|
||||
for (let x = 0, len = containers.length; x < len; x++) {
|
||||
container = containers[x];
|
||||
// get the scripts, they won't be run on the new element so we need to eval them
|
||||
let widgetelement = container.firstElementChild.cloneNode(true);
|
||||
console.log(widgetelement);
|
||||
var scripts = htmx.findAll(widgetelement, "script");
|
||||
var new_id = widgetelement.id;
|
||||
|
||||
// check if there's an existing element like the one we want to swap
|
||||
let grid_element = htmx.find('#grid-stack-main');
|
||||
let existing_widget = htmx.find(grid_element, "#"+new_id);
|
||||
// check if there's an existing element like the one we want to swap
|
||||
let grid_element = htmx.find('#grid-stack-main');
|
||||
let existing_widget = htmx.find(grid_element, "#"+new_id);
|
||||
|
||||
// get the size and position attributes
|
||||
if (existing_widget) {
|
||||
let attrs = existing_widget.getAttributeNames();
|
||||
for (let i = 0, len = attrs.length; i < len; i++) {
|
||||
if (attrs[i].startsWith('gs-')) { // only target gridstack attributes
|
||||
widgetelement.setAttribute(attrs[i], existing_widget.getAttribute(attrs[i]));
|
||||
// get the size and position attributes
|
||||
if (existing_widget) {
|
||||
let attrs = existing_widget.getAttributeNames();
|
||||
for (let i = 0, len = attrs.length; i < len; i++) {
|
||||
if (attrs[i].startsWith('gs-')) { // only target gridstack attributes
|
||||
widgetelement.setAttribute(attrs[i], existing_widget.getAttribute(attrs[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// clear the queue element
|
||||
container.outerHTML = "";
|
||||
grid.addWidget(widgetelement);
|
||||
// clear the queue element
|
||||
container.outerHTML = "";
|
||||
// container.firstElementChild.outerHTML = "";
|
||||
grid.addWidget(widgetelement);
|
||||
|
||||
// re-create the HTMX JS listeners, otherwise HTMX won't work inside the grid
|
||||
htmx.process(widgetelement);
|
||||
// re-create the HTMX JS listeners, otherwise HTMX won't work inside the grid
|
||||
htmx.process(widgetelement);
|
||||
|
||||
// update the size of the widget according to its content
|
||||
var added_widget = htmx.find(grid_element, "#"+new_id);
|
||||
var itemContent = htmx.find(added_widget, ".control");
|
||||
var scrollheight = itemContent.scrollHeight+80;
|
||||
var verticalmargin = 0;
|
||||
var cellheight = grid.opts.cellHeight;
|
||||
var height = Math.ceil((scrollheight + verticalmargin) / (cellheight + verticalmargin));
|
||||
var opts = {
|
||||
h: height,
|
||||
}
|
||||
grid.update(
|
||||
added_widget,
|
||||
opts
|
||||
);
|
||||
// update the size of the widget according to its content
|
||||
var added_widget = htmx.find(grid_element, "#"+new_id);
|
||||
var itemContent = htmx.find(added_widget, ".control");
|
||||
var scrollheight = itemContent.scrollHeight+80;
|
||||
var verticalmargin = 0;
|
||||
var cellheight = grid.opts.cellHeight;
|
||||
var height = Math.ceil((scrollheight + verticalmargin) / (cellheight + verticalmargin));
|
||||
var opts = {
|
||||
h: height,
|
||||
}
|
||||
grid.update(
|
||||
added_widget,
|
||||
opts
|
||||
);
|
||||
|
||||
// run the JS scripts inside the added element again
|
||||
for (var i = 0; i < scripts.length; i++) {
|
||||
eval(scripts[i].innerHTML);
|
||||
// run the JS scripts inside the added element again
|
||||
for (var i = 0; i < scripts.length; i++) {
|
||||
eval(scripts[i].innerHTML);
|
||||
}
|
||||
}
|
||||
// clear the containers we just added
|
||||
// for (let x = 0, len = containers.length; x < len; x++) {
|
||||
// container = containers[x];
|
||||
// container.inner = "";
|
||||
// }
|
||||
grid.compact();
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
<div>
|
||||
<!-- <div
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="#"
|
||||
hx-target="#widgets-here"
|
||||
hx-trigger="load"
|
||||
hx-swap="afterend"
|
||||
style="display: none;"></div> -->
|
||||
|
||||
<div id="modals-here">
|
||||
</div>
|
||||
<div id="items-here">
|
||||
</div>
|
||||
<div id="widgets-here" style="display: none;">
|
||||
</div>
|
||||
<script>
|
||||
|
||||
</script>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{% extends 'wm/modal.html' %}
|
||||
|
||||
{% block modal_content %}
|
||||
{% include 'window-content/main.html' %}
|
||||
{% endblock %}
|
||||
@@ -1,48 +0,0 @@
|
||||
{% load static %}
|
||||
|
||||
{% for plan in plans %}
|
||||
|
||||
|
||||
<div class="box">
|
||||
<article class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-64x64">
|
||||
<img src="{% static plan.image %}" alt="Image">
|
||||
</figure>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<p>
|
||||
<strong>{{ plan.name }}</strong> <small>£{{ plan.cost }}</small>
|
||||
{% if plan in user_plans %}
|
||||
<i class="fas fa-check" aria-hidden="true"></i>
|
||||
{% endif %}
|
||||
<br>
|
||||
{{ plan.description }}
|
||||
</p>
|
||||
</div>
|
||||
<nav class="level is-mobile">
|
||||
<div class="level-left">
|
||||
{% if plan not in user_plans %}
|
||||
<a class="level-item" href="/order/{{ plan.name }}">
|
||||
<span class="icon is-small has-text-success">
|
||||
<i class="fas fa-plus" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if plan in user_plans %}
|
||||
<a class="level-item" href="/cancel_subscription/{{ plan.name }}">
|
||||
<span class="icon is-small has-text-info">
|
||||
<i class="fas fa-cancel" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-5-tablet is-4-desktop is-3-widescreen">
|
||||
<div class="column is-5-tablet is-5-desktop is-4-widescreen">
|
||||
<form method="POST" class="box">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
<div class="field">
|
||||
<button class="button is-success">
|
||||
<button class="button">
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
|
||||
1
core/templates/two_factor/_base.html
Normal file
1
core/templates/two_factor/_base.html
Normal file
@@ -0,0 +1 @@
|
||||
{% extends 'base.html' %}
|
||||
16
core/templates/two_factor/_base_focus.html
Normal file
16
core/templates/two_factor/_base_focus.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% extends "two_factor/_base.html" %}
|
||||
|
||||
{% block content_wrapper %}
|
||||
<section class="hero is-fullheight">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<div class="columns is-centered">
|
||||
<div class="column box is-5-tablet is-5-desktop is-4-widescreen">
|
||||
{% block content %}{% endblock content %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
16
core/templates/two_factor/_wizard_actions.html
Normal file
16
core/templates/two_factor/_wizard_actions.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div class="buttons">
|
||||
{% if cancel_url %}
|
||||
<a href="{{ cancel_url }}"
|
||||
class="button">{% trans "Cancel" %}</a>
|
||||
{% endif %}
|
||||
{% if wizard.steps.prev %}
|
||||
<button name="wizard_goto_step" type="submit"
|
||||
value="{{ wizard.steps.prev }}"
|
||||
class="button">{% trans "Back" %}</button>
|
||||
{% else %}
|
||||
<button disabled name="" type="button" class="button">{% trans "Back" %}</button>
|
||||
{% endif %}
|
||||
<button type="submit" class="button">{% trans "Next" %}</button>
|
||||
</div>
|
||||
6
core/templates/two_factor/_wizard_forms.html
Normal file
6
core/templates/two_factor/_wizard_forms.html
Normal file
@@ -0,0 +1,6 @@
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
<table class="is-3">
|
||||
{{ wizard.management_form|crispy }}
|
||||
{{ wizard.form|crispy }}
|
||||
</table>
|
||||
28
core/templates/two_factor/core/backup_tokens.html
Normal file
28
core/templates/two_factor/core/backup_tokens.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends "two_factor/_base_focus.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="title">{% block title %}{% trans "Backup Tokens" %}{% endblock %}</h1>
|
||||
<p class="subtitle">{% blocktrans trimmed %}Backup tokens can be used when your primary and backup
|
||||
phone numbers aren't available. The backup tokens below can be used
|
||||
for login verification. If you've used up all your backup tokens, you
|
||||
can generate a new set of backup tokens. Only the backup tokens shown
|
||||
below will be valid.{% endblocktrans %}</p>
|
||||
|
||||
{% if device.token_set.count %}
|
||||
<ul>
|
||||
{% for token in device.token_set.all %}
|
||||
<li>{{ token.token }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p class="subtitle">{% blocktrans %}Print these tokens and keep them somewhere safe.{% endblocktrans %}</p>
|
||||
{% else %}
|
||||
<p class="subtitle">{% trans "You don't have any backup codes yet." %}</p>
|
||||
{% endif %}
|
||||
|
||||
<form method="post">{% csrf_token %}{{ form }}
|
||||
<a href="{% url 'two_factor:profile'%}"
|
||||
class="float-right button">{% trans "Back to Account Security" %}</a>
|
||||
<button class="button" type="submit">{% trans "Generate Tokens" %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
52
core/templates/two_factor/core/login.html
Normal file
52
core/templates/two_factor/core/login.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends "two_factor/_base_focus.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="title">{% block title %}{% trans "Login" %}{% endblock %}</h1>
|
||||
|
||||
{% if wizard.steps.current == 'auth' %}
|
||||
<p class="subtitle">{% blocktrans %}Enter your credentials.{% endblocktrans %}</p>
|
||||
{% elif wizard.steps.current == 'token' %}
|
||||
{% if device.method == 'call' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}We are calling your phone right now, please enter the
|
||||
digits you hear.{% endblocktrans %}</p>
|
||||
{% elif device.method == 'sms' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}We sent you a text message, please enter the tokens we
|
||||
sent.{% endblocktrans %}</p>
|
||||
{% else %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}Please enter the tokens generated by your token
|
||||
generator.{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
{% elif wizard.steps.current == 'backup' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}Use this form for entering backup tokens for logging in.
|
||||
These tokens have been generated for you to print and keep safe. Please
|
||||
enter one of these backup tokens to login to your account.{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
|
||||
<form action="" method="post">{% csrf_token %}
|
||||
{% include "two_factor/_wizard_forms.html" %}
|
||||
|
||||
{# hidden submit button to enable [enter] key #}
|
||||
<input type="submit" value="" style="display:none" />
|
||||
|
||||
{% if other_devices %}
|
||||
<p class="subtitle">{% trans "Or, alternatively, use one of your backup phones:" %}</p>
|
||||
<p class="subtitle">
|
||||
{% for other in other_devices %}
|
||||
<button name="challenge_device" value="{{ other.persistent_id }}"
|
||||
class="button" type="submit">
|
||||
{{ other.generate_challenge_button_title }}
|
||||
</button>
|
||||
{% endfor %}</p>
|
||||
{% endif %}
|
||||
{% if backup_tokens %}
|
||||
<p class="subtitle">{% trans "As a last resort, you can use a backup token:" %}</p>
|
||||
<p class="subtitle">
|
||||
<button name="wizard_goto_step" type="submit" value="backup"
|
||||
class="button">{% trans "Use Backup Token" %}</button>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% include "two_factor/_wizard_actions.html" %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
22
core/templates/two_factor/core/otp_required.html
Normal file
22
core/templates/two_factor/core/otp_required.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{% extends "two_factor/_base_focus.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="title">{% block title %}{% trans "Permission Denied" %}{% endblock %}</h1>
|
||||
|
||||
<p class="subtitle">{% blocktrans trimmed %}The page you requested, enforces users to verify using
|
||||
two-factor authentication for security reasons. You need to enable these
|
||||
security features in order to access this page.{% endblocktrans %}</p>
|
||||
|
||||
<p class="subtitle">{% blocktrans trimmed %}Two-factor authentication is not enabled for your
|
||||
account. Enable two-factor authentication for enhanced account
|
||||
security.{% endblocktrans %}</p>
|
||||
<div class="buttons">
|
||||
|
||||
<a href="javascript:history.go(-1)"
|
||||
class="float-right button">{% trans "Go back" %}</a>
|
||||
<a href="{% url 'two_factor:setup' %}" class="button">
|
||||
{% trans "Enable Two-Factor Authentication" %}</a>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
24
core/templates/two_factor/core/phone_register.html
Normal file
24
core/templates/two_factor/core/phone_register.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends "two_factor/_base_focus.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="title">{% block title %}{% trans "Add Backup Phone" %}{% endblock %}</h1>
|
||||
|
||||
{% if wizard.steps.current == 'setup' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}You'll be adding a backup phone number to your
|
||||
account. This number will be used if your primary method of
|
||||
registration is not available.{% endblocktrans %}</p>
|
||||
{% elif wizard.steps.current == 'validation' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}We've sent a token to your phone number. Please
|
||||
enter the token you've received.{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
|
||||
<form action="" method="post">{% csrf_token %}
|
||||
{% include "two_factor/_wizard_forms.html" %}
|
||||
|
||||
{# hidden submit button to enable [enter] key #}
|
||||
<input type="submit" value="" style="display:none" />
|
||||
|
||||
{% include "two_factor/_wizard_actions.html" %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
56
core/templates/two_factor/core/setup.html
Normal file
56
core/templates/two_factor/core/setup.html
Normal file
@@ -0,0 +1,56 @@
|
||||
{% extends "two_factor/_base_focus.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="title">{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}</h1>
|
||||
{% if wizard.steps.current == 'welcome' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}You are about to take your account security to the
|
||||
next level. Follow the steps in this wizard to enable two-factor
|
||||
authentication.{% endblocktrans %}</p>
|
||||
{% elif wizard.steps.current == 'method' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}Please select which authentication method you would
|
||||
like to use.{% endblocktrans %}</p>
|
||||
{% elif wizard.steps.current == 'generator' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}To start using a token generator, please use your
|
||||
smartphone to scan the QR code below. For example, use Google
|
||||
Authenticator. Then, enter the token generated by the app.
|
||||
{% endblocktrans %}</p>
|
||||
<p class="subtitle"><img src="{{ QR_URL }}" alt="QR Code" class="bg-white"/></p>
|
||||
{% elif wizard.steps.current == 'sms' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}Please enter the phone number you wish to receive the
|
||||
text messages on. This number will be validated in the next step.
|
||||
{% endblocktrans %}</p>
|
||||
{% elif wizard.steps.current == 'call' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}Please enter the phone number you wish to be called on.
|
||||
This number will be validated in the next step. {% endblocktrans %}</p>
|
||||
{% elif wizard.steps.current == 'validation' %}
|
||||
{% if challenge_succeeded %}
|
||||
{% if device.method == 'call' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}We are calling your phone right now, please enter the
|
||||
digits you hear.{% endblocktrans %}</p>
|
||||
{% elif device.method == 'sms' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}We sent you a text message, please enter the tokens we
|
||||
sent.{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="alert alert-warning" role="alert">{% blocktrans trimmed %}We've
|
||||
encountered an issue with the selected authentication method. Please
|
||||
go back and verify that you entered your information correctly, try
|
||||
again, or use a different authentication method instead. If the issue
|
||||
persists, contact the site administrator.{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
{% elif wizard.steps.current == 'yubikey' %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}To identify and verify your YubiKey, please insert a
|
||||
token in the field below. Your YubiKey will be linked to your
|
||||
account.{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
|
||||
<form action="" method="post">{% csrf_token %}
|
||||
{% include "two_factor/_wizard_forms.html" %}
|
||||
|
||||
{# hidden submit button to enable [enter] key #}
|
||||
<input type="submit" value="" style="display:none" />
|
||||
|
||||
{% include "two_factor/_wizard_actions.html" %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
24
core/templates/two_factor/core/setup_complete.html
Normal file
24
core/templates/two_factor/core/setup_complete.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends "two_factor/_base_focus.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="title">{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}</h1>
|
||||
|
||||
<p class="subtitle">{% blocktrans trimmed %}Congratulations, you've successfully enabled two-factor
|
||||
authentication.{% endblocktrans %}</p>
|
||||
|
||||
{% if not phone_methods %}
|
||||
<p class="subtitle"><a href="{% url 'two_factor:profile' %}"
|
||||
class="button">{% trans "Back to Account Security" %}</a></p>
|
||||
{% else %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}However, it might happen that you don't have access to
|
||||
your primary token device. To enable account recovery, add a phone
|
||||
number.{% endblocktrans %}</p>
|
||||
|
||||
<a href="{% url 'two_factor:profile' %}"
|
||||
class="float-right button">{% trans "Back to Account Security" %}</a>
|
||||
<p class="subtitle"><a href="{% url 'two_factor:phone_create' %}"
|
||||
class="button">{% trans "Add Phone Number" %}</a></p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
14
core/templates/two_factor/profile/disable.html
Normal file
14
core/templates/two_factor/profile/disable.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends "two_factor/_base_focus.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="title">{% block title %}{% trans "Disable Two-factor Authentication" %}{% endblock %}</h1>
|
||||
<p class="subtitle">{% blocktrans trimmed %}You are about to disable two-factor authentication. This
|
||||
weakens your account security, are you sure?{% endblocktrans %}</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<table>{{ form }}</table>
|
||||
<button class="button"
|
||||
type="submit">{% trans "Disable" %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
63
core/templates/two_factor/profile/profile.html
Normal file
63
core/templates/two_factor/profile/profile.html
Normal file
@@ -0,0 +1,63 @@
|
||||
{% extends "two_factor/_base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="title">{% block title %}{% trans "Account Security" %}{% endblock %}</h1>
|
||||
|
||||
{% if default_device %}
|
||||
{% if default_device_type == 'TOTPDevice' %}
|
||||
<p class="subtitle">{% trans "Tokens will be generated by your token generator." %}</p>
|
||||
{% elif default_device_type == 'PhoneDevice' %}
|
||||
<p class="subtitle">{% blocktrans with primary=default_device.generate_challenge_button_title %}Primary method: {{ primary }}{% endblocktrans %}</p>
|
||||
{% elif default_device_type == 'RemoteYubikeyDevice' %}
|
||||
<p class="subtitle">{% blocktrans %}Tokens will be generated by your YubiKey.{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if available_phone_methods %}
|
||||
<h2 class="title is-4">{% trans "Backup Phone Numbers" %}</h2>
|
||||
<p class="subtitle">{% blocktrans trimmed %}If your primary method is not available, we are able to
|
||||
send backup tokens to the phone numbers listed below.{% endblocktrans %}</p>
|
||||
<ul>
|
||||
{% for phone in backup_phones %}
|
||||
<li>
|
||||
{{ phone.generate_challenge_button_title }}
|
||||
<form method="post" action="{% url 'two_factor:phone_delete' phone.id %}"
|
||||
onsubmit="return confirm({% trans 'Are you sure?' %})">
|
||||
{% csrf_token %}
|
||||
<button class="button is-warning"
|
||||
type="submit">{% trans "Unregister" %}</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p class="subtitle"><a href="{% url 'two_factor:phone_create' %}"
|
||||
class="button">{% trans "Add Phone Number" %}</a></p>
|
||||
{% endif %}
|
||||
|
||||
<h2 class="title is-4">{% trans "Backup Tokens" %}</h2>
|
||||
<p class="subtitle">
|
||||
{% blocktrans trimmed %}If you don't have any device with you, you can access
|
||||
your account using backup tokens.{% endblocktrans %}
|
||||
{% blocktrans trimmed count counter=backup_tokens %}
|
||||
You have only one backup token remaining.
|
||||
{% plural %}
|
||||
You have {{ counter }} backup tokens remaining.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p class="subtitle"><a href="{% url 'two_factor:backup_tokens' %}"
|
||||
class="button">{% trans "Show Codes" %}</a></p>
|
||||
|
||||
<h3 class="title is-5">{% trans "Disable Two-Factor Authentication" %}</h3>
|
||||
<p class="subtitle">{% blocktrans trimmed %}However we strongly discourage you to do so, you can
|
||||
also disable two-factor authentication for your account.{% endblocktrans %}</p>
|
||||
<p class="subtitle"><a class="button" href="{% url 'two_factor:disable' %}">
|
||||
{% trans "Disable Two-Factor Authentication" %}</a></p>
|
||||
{% else %}
|
||||
<p class="subtitle">{% blocktrans trimmed %}Two-factor authentication is not enabled for your
|
||||
account. Enable two-factor authentication for enhanced account
|
||||
security.{% endblocktrans %}</p>
|
||||
<p class="subtitle"><a href="{% url 'two_factor:setup' %}" class="button">
|
||||
{% trans "Enable Two-Factor Authentication" %}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
7
core/templates/two_factor/twilio/press_a_key.xml
Normal file
7
core/templates/two_factor/twilio/press_a_key.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
{% load i18n %}<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Response>
|
||||
<Gather timeout="15" numDigits="1" finishOnKey="">
|
||||
<Say language="{{ locale }}">{% blocktrans %}Hi, this is {{ site_name }} calling. Press any key to continue.{% endblocktrans %}</Say>
|
||||
</Gather>
|
||||
<Say language="{{ locale }}">{% trans "You didn’t press any keys. Good bye." %}</Say>
|
||||
</Response>
|
||||
5
core/templates/two_factor/twilio/sms_message.html
Normal file
5
core/templates/two_factor/twilio/sms_message.html
Normal file
@@ -0,0 +1,5 @@
|
||||
{% load i18n %}
|
||||
{% blocktrans trimmed %}
|
||||
Your OTP token is {{ token }}
|
||||
{% endblocktrans %}
|
||||
|
||||
12
core/templates/two_factor/twilio/token.xml
Normal file
12
core/templates/two_factor/twilio/token.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
{% load i18n %}<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Response>
|
||||
<Say language="{{ locale }}">{% trans "Your token is:" %}</Say>
|
||||
<Pause>
|
||||
{% for digit in token %} <Say language="{{ locale }}">{{ digit }}</Say>
|
||||
<Pause>
|
||||
{% endfor %} <Say language="{{ locale }}">{% trans "Repeat:" %}</Say>
|
||||
<Pause>
|
||||
{% for digit in token %} <Say language="{{ locale }}">{{ digit }}</Say>
|
||||
<Pause>
|
||||
{% endfor %} <Say language="{{ locale }}">{% trans "Good bye." %}</Say>
|
||||
</Response>
|
||||
@@ -1,20 +0,0 @@
|
||||
{% extends 'wm/widget.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block widget_options %}
|
||||
gs-w="10" gs-h="1" gs-y="10" gs-x="1"
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
Widget
|
||||
{% endblock %}
|
||||
|
||||
{% block close_button %}
|
||||
<i
|
||||
class="fa-solid fa-xmark has-text-grey-light float-right"
|
||||
onclick='grid.removeWidget("widget-{{ unique }}"); //grid.compact();'></i>
|
||||
{% endblock %}
|
||||
|
||||
{% block panel_content %}
|
||||
{% include 'window-content/main.html' %}
|
||||
{% endblock %}
|
||||
@@ -1,44 +0,0 @@
|
||||
<p class="title">This is a demo panel</p>
|
||||
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'modal' %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
class="button is-info">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-list"></i>
|
||||
</span>
|
||||
<span>Open modal</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'widget' %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#widgets-here"
|
||||
class="button is-info">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-list"></i>
|
||||
</span>
|
||||
<span>Open widget</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'window' %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#items-here"
|
||||
hx-swap="afterend"
|
||||
class="button is-info">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-list"></i>
|
||||
</span>
|
||||
<span>Open window</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1,9 +0,0 @@
|
||||
{% extends 'wm/magnet.html' %}
|
||||
|
||||
{% block heading %}
|
||||
Window
|
||||
{% endblock %}
|
||||
|
||||
{% block panel_content %}
|
||||
{% include 'window-content/main.html' %}
|
||||
{% endblock %}
|
||||
@@ -1,20 +0,0 @@
|
||||
{% load static %}
|
||||
|
||||
<script src="{% static 'modal.js' %}"></script>
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
{% endblock %}
|
||||
|
||||
<div id="modal" class="modal is-active is-clipped">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-content">
|
||||
<div class="box">
|
||||
{% block modal_content %}
|
||||
{% include window_content %}
|
||||
{% endblock %}
|
||||
{% include 'partials/close-modal.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% include window_content %}
|
||||
{% endblock %}
|
||||
@@ -1,17 +0,0 @@
|
||||
|
||||
<nav class="panel">
|
||||
<p class="panel-heading" style="padding: .2em; line-height: .5em;">
|
||||
<i class="fa-solid fa-arrows-up-down-left-right has-text-grey-light"></i>
|
||||
{% block close_button %}
|
||||
{% include 'partials/close-window.html' %}
|
||||
{% endblock %}
|
||||
{% block heading %}
|
||||
{% endblock %}
|
||||
</p>
|
||||
<article class="panel-block is-active">
|
||||
<div class="control">
|
||||
{% block panel_content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</article>
|
||||
</nav>
|
||||
@@ -1,37 +0,0 @@
|
||||
<div id="widget">
|
||||
<div id="widget-{{ unique }}" class="grid-stack-item" {% block widget_options %}gs-w="10" gs-h="1" gs-y="10" gs-x="1"{% endblock %}>
|
||||
<div class="grid-stack-item-content">
|
||||
|
||||
<nav class="panel">
|
||||
<p class="panel-heading" style="padding: .2em; line-height: .5em;">
|
||||
<i class="fa-solid fa-arrows-up-down-left-right has-text-grey-light"></i>
|
||||
{% block close_button %}
|
||||
{% include 'partials/close-widget.html' %}
|
||||
{% endblock %}
|
||||
<i
|
||||
class="fa-solid fa-arrows-minimize has-text-grey-light float-right"
|
||||
onclick='grid.compact();'></i>
|
||||
{% block heading %}
|
||||
{{ title }}
|
||||
{% endblock %}
|
||||
</p>
|
||||
<article class="panel-block is-active">
|
||||
<div class="control">
|
||||
{% block panel_content %}
|
||||
{% include window_content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</article>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
{% block custom_script %}
|
||||
{% endblock %}
|
||||
var widget_event = new Event('load-widget');
|
||||
document.dispatchEvent(widget_event);
|
||||
</script>
|
||||
{% block custom_end %}
|
||||
{% endblock %}
|
||||
@@ -1,10 +0,0 @@
|
||||
<magnet-block attract-distance="10" align-to="outer|center" class="floating-window">
|
||||
{% extends 'wm/panel.html' %}
|
||||
{% block heading %}
|
||||
{{ title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block panel_content %}
|
||||
{% include window_content %}
|
||||
{% endblock %}
|
||||
</magnet-block>
|
||||
0
core/tests/__init__.py
Normal file
0
core/tests/__init__.py
Normal file
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
|
||||
import stripe
|
||||
# import stripe
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import JsonResponse
|
||||
@@ -10,8 +10,7 @@ from django.views import View
|
||||
from django.views.generic.edit import CreateView
|
||||
|
||||
from core.forms import NewUserForm
|
||||
from core.lib.products import assemble_plan_map
|
||||
from core.models import Plan, Session
|
||||
from core.lib.notify import raw_sendmsg
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,81 +24,22 @@ class Home(View):
|
||||
return render(request, self.template_name)
|
||||
|
||||
|
||||
class Billing(LoginRequiredMixin, View):
|
||||
template_name = "billing.html"
|
||||
|
||||
def get(self, request):
|
||||
if not settings.STRIPE_ENABLED:
|
||||
return redirect(reverse("home"))
|
||||
plans = Plan.objects.all()
|
||||
user_plans = request.user.plans.all()
|
||||
context = {"plans": plans, "user_plans": user_plans}
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
|
||||
class Order(LoginRequiredMixin, View):
|
||||
def get(self, request, plan_name):
|
||||
if not settings.STRIPE_ENABLED:
|
||||
return redirect(reverse("home"))
|
||||
plan = Plan.objects.get(name=plan_name)
|
||||
try:
|
||||
cast = {
|
||||
"payment_method_types": settings.ALLOWED_PAYMENT_METHODS,
|
||||
"mode": "subscription",
|
||||
"customer": request.user.stripe_id,
|
||||
"line_items": assemble_plan_map(product_id_filter=plan.product_id),
|
||||
"success_url": request.build_absolute_uri(reverse("success")),
|
||||
"cancel_url": request.build_absolute_uri(reverse("cancel")),
|
||||
}
|
||||
if request.user.is_superuser:
|
||||
cast["discounts"] = [{"coupon": settings.STRIPE_ADMIN_COUPON}]
|
||||
session = stripe.checkout.Session.create(**cast)
|
||||
Session.objects.create(user=request.user, session=session.id)
|
||||
return redirect(session.url)
|
||||
# return JsonResponse({'id': session.id})
|
||||
except Exception as e:
|
||||
# Raise a server error
|
||||
return JsonResponse({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
class Cancel(LoginRequiredMixin, View):
|
||||
def get(self, request, plan_name):
|
||||
if not settings.STRIPE_ENABLED:
|
||||
return redirect(reverse("home"))
|
||||
plan = Plan.objects.get(name=plan_name)
|
||||
try:
|
||||
subscriptions = stripe.Subscription.list(
|
||||
customer=request.user.stripe_id, price=plan.product_id
|
||||
)
|
||||
for subscription in subscriptions["data"]:
|
||||
items = subscription["items"]["data"]
|
||||
for item in items:
|
||||
stripe.Subscription.delete(item["subscription"])
|
||||
return render(request, "subscriptioncancel.html", {"plan": plan})
|
||||
# return JsonResponse({'id': session.id})
|
||||
except Exception as e:
|
||||
# Raise a server error
|
||||
logging.error(f"Error cancelling subscription for user: {e}")
|
||||
return JsonResponse({"error": "True"}, status=500)
|
||||
|
||||
|
||||
class Signup(CreateView):
|
||||
form_class = NewUserForm
|
||||
success_url = reverse_lazy("login")
|
||||
success_url = reverse_lazy("two_factor:login")
|
||||
template_name = "registration/signup.html"
|
||||
|
||||
def form_valid(self, form):
|
||||
"""If the form is valid, save the associated model."""
|
||||
self.object = form.save()
|
||||
raw_sendmsg(
|
||||
f"New user signup: {self.object.username} - {self.object.email}",
|
||||
title="New user",
|
||||
topic=settings.NOTIFY_TOPIC,
|
||||
)
|
||||
return super().form_valid(form)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if not settings.REGISTRATION_OPEN:
|
||||
return render(request, "registration/registration_closed.html")
|
||||
super().get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class Portal(LoginRequiredMixin, View):
|
||||
def get(self, request):
|
||||
if not settings.STRIPE_ENABLED:
|
||||
return redirect(reverse("home"))
|
||||
session = stripe.billing_portal.Session.create(
|
||||
customer=request.user.stripe_id,
|
||||
return_url=request.build_absolute_uri(reverse("billing")),
|
||||
)
|
||||
return redirect(session.url)
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import stripe
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from rest_framework.parsers import JSONParser
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from core.models import Plan, Session, User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Callback(APIView):
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
@csrf_exempt
|
||||
def post(self, request):
|
||||
payload = request.body
|
||||
sig_header = request.META["HTTP_STRIPE_SIGNATURE"]
|
||||
try:
|
||||
stripe.Webhook.construct_event(
|
||||
payload, sig_header, settings.STRIPE_ENDPOINT_SECRET
|
||||
)
|
||||
except ValueError:
|
||||
# Invalid payload
|
||||
logger.error("Invalid payload")
|
||||
return HttpResponse(status=400)
|
||||
except stripe.error.SignatureVerificationError:
|
||||
# Invalid signature
|
||||
logger.error("Invalid signature")
|
||||
return HttpResponse(status=400)
|
||||
|
||||
if request.data is None:
|
||||
return JsonResponse({"success": False}, status=500)
|
||||
if "type" in request.data.keys():
|
||||
rtype = request.data["type"]
|
||||
if rtype == "checkout.session.completed":
|
||||
session = request.data["data"]["object"]["id"]
|
||||
subscription_id = request.data["data"]["object"]["subscription"]
|
||||
session_map = Session.objects.get(session=session)
|
||||
if not session_map:
|
||||
return JsonResponse({"success": False}, status=500)
|
||||
user = session_map.user
|
||||
session_map.subscription_id = subscription_id
|
||||
session_map.save()
|
||||
|
||||
if rtype == "customer.subscription.updated":
|
||||
stripe_id = request.data["data"]["object"]["customer"]
|
||||
if not stripe_id:
|
||||
logging.error("No stripe id")
|
||||
return JsonResponse({"success": False}, status=500)
|
||||
user = User.objects.get(stripe_id=stripe_id)
|
||||
# ssubscription_active
|
||||
subscription_id = request.data["data"]["object"]["id"]
|
||||
sessions = Session.objects.filter(user=user)
|
||||
session = None
|
||||
for session_iter in sessions:
|
||||
if session_iter.subscription_id == subscription_id:
|
||||
session = session_iter
|
||||
if not session:
|
||||
logging.error(
|
||||
f"No session found for subscription id {subscription_id}"
|
||||
)
|
||||
return JsonResponse({"success": False}, status=500)
|
||||
# query Session objects
|
||||
# iterate and check against product_id
|
||||
session.request = request.data["request"]["id"]
|
||||
product_id = request.data["data"]["object"]["plan"]["id"]
|
||||
plan = Plan.objects.get(product_id=product_id)
|
||||
if not plan:
|
||||
logging.error(f"Plan not found: {product_id}")
|
||||
return JsonResponse({"success": False}, status=500)
|
||||
session.plan = plan
|
||||
session.save()
|
||||
|
||||
elif rtype == "payment_intent.succeeded":
|
||||
customer = request.data["data"]["object"]["customer"]
|
||||
user = User.objects.get(stripe_id=customer)
|
||||
if not user:
|
||||
logging.error(f"No user found for customer: {customer}")
|
||||
return JsonResponse({"success": False}, status=500)
|
||||
session = Session.objects.get(request=request.data["request"]["id"])
|
||||
|
||||
user.plans.add(session.plan)
|
||||
user.last_payment = datetime.utcnow()
|
||||
user.save()
|
||||
|
||||
elif rtype == "customer.subscription.deleted":
|
||||
customer = request.data["data"]["object"]["customer"]
|
||||
user = User.objects.get(stripe_id=customer)
|
||||
if not user:
|
||||
logging.error(f"No user found for customer {customer}")
|
||||
return JsonResponse({"success": False}, status=500)
|
||||
product_id = request.data["data"]["object"]["plan"]["id"]
|
||||
plan = Plan.objects.get(product_id=product_id)
|
||||
user.plans.remove(plan)
|
||||
user.save()
|
||||
else:
|
||||
return JsonResponse({"success": False}, status=500)
|
||||
return JsonResponse({"success": True})
|
||||
@@ -1,26 +0,0 @@
|
||||
import uuid
|
||||
|
||||
from django.shortcuts import render
|
||||
from django.views import View
|
||||
|
||||
|
||||
class DemoModal(View):
|
||||
template_name = "modals/modal.html"
|
||||
|
||||
def get(self, request):
|
||||
return render(request, self.template_name)
|
||||
|
||||
|
||||
class DemoWidget(View):
|
||||
template_name = "widgets/widget.html"
|
||||
|
||||
def get(self, request):
|
||||
unique = str(uuid.uuid4())[:8]
|
||||
return render(request, self.template_name, {"unique": unique})
|
||||
|
||||
|
||||
class DemoWindow(View):
|
||||
template_name = "windows/window.html"
|
||||
|
||||
def get(self, request):
|
||||
return render(request, self.template_name)
|
||||
30
core/views/notifications.py
Normal file
30
core/views/notifications.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from mixins.views import ObjectUpdate
|
||||
|
||||
from core.forms import NotificationSettingsForm
|
||||
from core.models import NotificationSettings
|
||||
|
||||
|
||||
# Notifications - we create a new notification settings object if there isn't one
|
||||
# Hence, there is only an update view, not a create view.
|
||||
class NotificationsUpdate(LoginRequiredMixin, ObjectUpdate):
|
||||
model = NotificationSettings
|
||||
form_class = NotificationSettingsForm
|
||||
|
||||
page_title = "Update your notification settings"
|
||||
page_subtitle = (
|
||||
"At least the topic must be set if you want to receive notifications."
|
||||
)
|
||||
|
||||
submit_url_name = "notifications_update"
|
||||
submit_url_args = ["type"]
|
||||
|
||||
pk_required = False
|
||||
|
||||
hide_cancel = True
|
||||
|
||||
def get_object(self, **kwargs):
|
||||
notification_settings, _ = NotificationSettings.objects.get_or_create(
|
||||
user=self.request.user
|
||||
)
|
||||
return notification_settings
|
||||
0
core/views/signal.py
Normal file
0
core/views/signal.py
Normal file
@@ -9,26 +9,140 @@ services:
|
||||
args:
|
||||
OPERATION: ${OPERATION}
|
||||
volumes:
|
||||
- ${PORTAINER_GIT_DIR}:/code
|
||||
- ${PORTAINER_GIT_DIR}/docker/uwsgi.ini:/conf/uwsgi.ini
|
||||
- ${REPO_DIR}:/code
|
||||
- ${REPO_DIR}/docker/uwsgi.ini:/conf/uwsgi.ini
|
||||
- ${APP_DATABASE_FILE}:/conf/db.sqlite3
|
||||
- app_static:${STATIC_ROOT}
|
||||
#ports:
|
||||
# - "8000:8000" # uwsgi socket
|
||||
env_file:
|
||||
- stack.env
|
||||
volumes_from:
|
||||
- tmp
|
||||
- type: bind
|
||||
source: /code/vrun
|
||||
target: /var/run
|
||||
environment:
|
||||
APP_PORT: "${APP_PORT}"
|
||||
REPO_DIR: "${REPO_DIR}"
|
||||
APP_LOCAL_SETTINGS: "${APP_LOCAL_SETTINGS}"
|
||||
APP_DATABASE_FILE: "${APP_DATABASE_FILE}"
|
||||
DOMAIN: "${DOMAIN}"
|
||||
URL: "${URL}"
|
||||
ALLOWED_HOSTS: "${ALLOWED_HOSTS}"
|
||||
NOTIFY_TOPIC: "${NOTIFY_TOPIC}"
|
||||
CSRF_TRUSTED_ORIGINS: "${CSRF_TRUSTED_ORIGINS}"
|
||||
DEBUG: "${DEBUG}"
|
||||
SECRET_KEY: "${SECRET_KEY}"
|
||||
STATIC_ROOT: "${STATIC_ROOT}"
|
||||
REGISTRATION_OPEN: "${REGISTRATION_OPEN}"
|
||||
OPERATION: "${OPERATION}"
|
||||
depends_on:
|
||||
# redis:
|
||||
# condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
migration:
|
||||
condition: service_started
|
||||
collectstatic:
|
||||
condition: service_started
|
||||
networks:
|
||||
- default
|
||||
- xf
|
||||
# deploy:
|
||||
# resources:
|
||||
# limits:
|
||||
# cpus: '0.1'
|
||||
# memory: 0.25G
|
||||
#network_mode: host
|
||||
|
||||
|
||||
signal-cli-rest-api:
|
||||
image: bbernhard/signal-cli-rest-api:latest
|
||||
container_name: signal
|
||||
environment:
|
||||
- MODE=normal #supported modes: json-rpc, native, normal
|
||||
- AUTO_RECEIVE_SCHEDULE=0 22 * * *
|
||||
# ports:
|
||||
# - "8080:8080"
|
||||
volumes:
|
||||
- "./signal-cli-config:/home/.local/share/signal-cli"
|
||||
|
||||
processing:
|
||||
image: xf/envelope:prod
|
||||
container_name: processing_envelope
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
OPERATION: ${OPERATION}
|
||||
command: sh -c '. /venv/bin/activate && python manage.py processing'
|
||||
volumes:
|
||||
- ${REPO_DIR}:/code
|
||||
- ${REPO_DIR}/docker/uwsgi.ini:/conf/uwsgi.ini
|
||||
- ${APP_DATABASE_FILE}:/conf/db.sqlite3
|
||||
- type: bind
|
||||
source: /code/vrun
|
||||
target: /var/run
|
||||
environment:
|
||||
APP_PORT: "${APP_PORT}"
|
||||
REPO_DIR: "${REPO_DIR}"
|
||||
APP_LOCAL_SETTINGS: "${APP_LOCAL_SETTINGS}"
|
||||
APP_DATABASE_FILE: "${APP_DATABASE_FILE}"
|
||||
DOMAIN: "${DOMAIN}"
|
||||
URL: "${URL}"
|
||||
ALLOWED_HOSTS: "${ALLOWED_HOSTS}"
|
||||
NOTIFY_TOPIC: "${NOTIFY_TOPIC}"
|
||||
CSRF_TRUSTED_ORIGINS: "${CSRF_TRUSTED_ORIGINS}"
|
||||
DEBUG: "${DEBUG}"
|
||||
SECRET_KEY: "${SECRET_KEY}"
|
||||
STATIC_ROOT: "${STATIC_ROOT}"
|
||||
REGISTRATION_OPEN: "${REGISTRATION_OPEN}"
|
||||
OPERATION: "${OPERATION}"
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
migration:
|
||||
condition: service_started
|
||||
collectstatic:
|
||||
condition: service_started
|
||||
# deploy:
|
||||
# resources:
|
||||
# limits:
|
||||
# cpus: '0.25'
|
||||
# memory: 0.25G
|
||||
#network_mode: host
|
||||
|
||||
scheduling:
|
||||
image: xf/envelope:prod
|
||||
container_name: scheduling_envelope
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
OPERATION: ${OPERATION}
|
||||
command: sh -c '. /venv/bin/activate && python manage.py scheduling'
|
||||
volumes:
|
||||
- ${REPO_DIR}:/code
|
||||
- ${REPO_DIR}/docker/uwsgi.ini:/conf/uwsgi.ini
|
||||
- ${APP_DATABASE_FILE}:/conf/db.sqlite3
|
||||
- type: bind
|
||||
source: /code/vrun
|
||||
target: /var/run
|
||||
environment:
|
||||
APP_PORT: "${APP_PORT}"
|
||||
REPO_DIR: "${REPO_DIR}"
|
||||
APP_LOCAL_SETTINGS: "${APP_LOCAL_SETTINGS}"
|
||||
APP_DATABASE_FILE: "${APP_DATABASE_FILE}"
|
||||
DOMAIN: "${DOMAIN}"
|
||||
URL: "${URL}"
|
||||
ALLOWED_HOSTS: "${ALLOWED_HOSTS}"
|
||||
NOTIFY_TOPIC: "${NOTIFY_TOPIC}"
|
||||
CSRF_TRUSTED_ORIGINS: "${CSRF_TRUSTED_ORIGINS}"
|
||||
DEBUG: "${DEBUG}"
|
||||
SECRET_KEY: "${SECRET_KEY}"
|
||||
STATIC_ROOT: "${STATIC_ROOT}"
|
||||
REGISTRATION_OPEN: "${REGISTRATION_OPEN}"
|
||||
OPERATION: "${OPERATION}"
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
migration:
|
||||
condition: service_started
|
||||
collectstatic:
|
||||
condition: service_started
|
||||
# deploy:
|
||||
# resources:
|
||||
# limits:
|
||||
# cpus: '0.25'
|
||||
# memory: 0.25G
|
||||
#network_mode: host
|
||||
|
||||
migration:
|
||||
image: xf/envelope:prod
|
||||
@@ -39,11 +153,32 @@ services:
|
||||
OPERATION: ${OPERATION}
|
||||
command: sh -c '. /venv/bin/activate && python manage.py migrate --noinput'
|
||||
volumes:
|
||||
- ${PORTAINER_GIT_DIR}:/code
|
||||
- ${REPO_DIR}:/code
|
||||
- ${APP_DATABASE_FILE}:/conf/db.sqlite3
|
||||
- app_static:${STATIC_ROOT}
|
||||
env_file:
|
||||
- stack.env
|
||||
- type: bind
|
||||
source: /code/vrun
|
||||
target: /var/run
|
||||
environment:
|
||||
APP_PORT: "${APP_PORT}"
|
||||
REPO_DIR: "${REPO_DIR}"
|
||||
APP_LOCAL_SETTINGS: "${APP_LOCAL_SETTINGS}"
|
||||
APP_DATABASE_FILE: "${APP_DATABASE_FILE}"
|
||||
DOMAIN: "${DOMAIN}"
|
||||
URL: "${URL}"
|
||||
ALLOWED_HOSTS: "${ALLOWED_HOSTS}"
|
||||
NOTIFY_TOPIC: "${NOTIFY_TOPIC}"
|
||||
CSRF_TRUSTED_ORIGINS: "${CSRF_TRUSTED_ORIGINS}"
|
||||
DEBUG: "${DEBUG}"
|
||||
SECRET_KEY: "${SECRET_KEY}"
|
||||
STATIC_ROOT: "${STATIC_ROOT}"
|
||||
REGISTRATION_OPEN: "${REGISTRATION_OPEN}"
|
||||
OPERATION: "${OPERATION}"
|
||||
# deploy:
|
||||
# resources:
|
||||
# limits:
|
||||
# cpus: '0.25'
|
||||
# memory: 0.25G
|
||||
#network_mode: host
|
||||
|
||||
collectstatic:
|
||||
image: xf/envelope:prod
|
||||
@@ -54,72 +189,54 @@ services:
|
||||
OPERATION: ${OPERATION}
|
||||
command: sh -c '. /venv/bin/activate && python manage.py collectstatic --noinput'
|
||||
volumes:
|
||||
- ${PORTAINER_GIT_DIR}:/code
|
||||
- ${REPO_DIR}:/code
|
||||
- ${APP_DATABASE_FILE}:/conf/db.sqlite3
|
||||
- app_static:${STATIC_ROOT}
|
||||
env_file:
|
||||
- stack.env
|
||||
- type: bind
|
||||
source: /code/vrun
|
||||
target: /var/run
|
||||
environment:
|
||||
APP_PORT: "${APP_PORT}"
|
||||
REPO_DIR: "${REPO_DIR}"
|
||||
APP_LOCAL_SETTINGS: "${APP_LOCAL_SETTINGS}"
|
||||
APP_DATABASE_FILE: "${APP_DATABASE_FILE}"
|
||||
DOMAIN: "${DOMAIN}"
|
||||
URL: "${URL}"
|
||||
ALLOWED_HOSTS: "${ALLOWED_HOSTS}"
|
||||
NOTIFY_TOPIC: "${NOTIFY_TOPIC}"
|
||||
CSRF_TRUSTED_ORIGINS: "${CSRF_TRUSTED_ORIGINS}"
|
||||
DEBUG: "${DEBUG}"
|
||||
SECRET_KEY: "${SECRET_KEY}"
|
||||
STATIC_ROOT: "${STATIC_ROOT}"
|
||||
REGISTRATION_OPEN: "${REGISTRATION_OPEN}"
|
||||
OPERATION: "${OPERATION}"
|
||||
# deploy:
|
||||
# resources:
|
||||
# limits:
|
||||
# cpus: '0.25'
|
||||
# memory: 0.25G
|
||||
#network_mode: host
|
||||
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
container_name: nginx_envelope
|
||||
ports:
|
||||
- ${APP_PORT}:9999
|
||||
ulimits:
|
||||
nproc: 65535
|
||||
nofile:
|
||||
soft: 65535
|
||||
hard: 65535
|
||||
redis:
|
||||
image: redis
|
||||
container_name: redis_envelope
|
||||
command: redis-server /etc/redis.conf
|
||||
volumes:
|
||||
- ${PORTAINER_GIT_DIR}:/code
|
||||
- ${PORTAINER_GIT_DIR}/docker/nginx/conf.d/${OPERATION}.conf:/etc/nginx/conf.d/default.conf
|
||||
- app_static:${STATIC_ROOT}
|
||||
volumes_from:
|
||||
- tmp
|
||||
networks:
|
||||
- default
|
||||
- xf
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_started
|
||||
|
||||
# volumes_from:
|
||||
# - tmp
|
||||
# depends_on:
|
||||
# redis:
|
||||
# condition: service_healthy
|
||||
|
||||
tmp:
|
||||
image: busybox
|
||||
container_name: tmp_envelope
|
||||
command: chmod -R 777 /var/run/socks
|
||||
volumes:
|
||||
- /var/run/socks
|
||||
|
||||
# redis:
|
||||
# image: redis
|
||||
# command: redis-server /etc/redis.conf
|
||||
# ulimits:
|
||||
# nproc: 65535
|
||||
# nofile:
|
||||
# soft: 65535
|
||||
# hard: 65535
|
||||
# volumes:
|
||||
# - ${PORTAINER_GIT_DIR}/docker/redis.conf:/etc/redis.conf
|
||||
# - redis_data:/data
|
||||
# volumes_from:
|
||||
# - tmp
|
||||
# healthcheck:
|
||||
# test: "redis-cli -s /var/run/redis/redis.sock ping"
|
||||
# interval: 2s
|
||||
# timeout: 2s
|
||||
# retries: 15
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
xf:
|
||||
external: true
|
||||
- ${REPO_DIR}/docker/redis.conf:/etc/redis.conf
|
||||
- envelope_redis_data:/data
|
||||
- type: bind
|
||||
source: /code/vrun
|
||||
target: /var/run
|
||||
healthcheck:
|
||||
test: "redis-cli ping"
|
||||
interval: 2s
|
||||
timeout: 2s
|
||||
retries: 15
|
||||
# deploy:
|
||||
# resources:
|
||||
# limits:
|
||||
# cpus: '0.25'
|
||||
# memory: 0.25G
|
||||
#network_mode: host
|
||||
|
||||
volumes:
|
||||
app_static: {}
|
||||
envelope_redis_data: {}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
upstream django {
|
||||
#server app:8000;
|
||||
#server unix:///var/run/socks/app.sock;
|
||||
server app:8000;
|
||||
server unix:///var/run/uwsgi-envelope.sock;
|
||||
}
|
||||
|
||||
server {
|
||||
@@ -14,10 +13,12 @@ server {
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://django;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $host;
|
||||
include /etc/nginx/uwsgi_params; # the uwsgi_params file you installed
|
||||
uwsgi_pass django;
|
||||
uwsgi_param Host $host;
|
||||
uwsgi_param X-Real-IP $remote_addr;
|
||||
uwsgi_param X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
uwsgi_param X-Forwarded-Proto $http_x_forwarded_proto;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
unixsocket /var/run/redis/redis.sock
|
||||
unixsocket /var/run/envelope-redis.sock
|
||||
unixsocketperm 777
|
||||
port 0
|
||||
@@ -4,9 +4,19 @@ module=app.wsgi:application
|
||||
env=DJANGO_SETTINGS_MODULE=app.settings
|
||||
master=1
|
||||
pidfile=/tmp/project-master.pid
|
||||
socket=0.0.0.0:8000
|
||||
#socket=0.0.0.0:8000
|
||||
socket=/var/run/uwsgi-envelope.sock
|
||||
# socket 777
|
||||
chmod-socket=777
|
||||
harakiri=20
|
||||
max-requests=100000
|
||||
#max-requests=100000
|
||||
# Set a lower value for max-requests to prevent memory leaks from building up over time
|
||||
max-requests=1000
|
||||
# Ensure old worker processes are cleaned up properly
|
||||
reload-on-as=512
|
||||
reload-on-rss=256
|
||||
vacuum=1
|
||||
home=/venv
|
||||
processes=12
|
||||
processes=4
|
||||
threads=2
|
||||
log-level=debug
|
||||
@@ -2,16 +2,31 @@ wheel
|
||||
uwsgi
|
||||
django
|
||||
pre-commit
|
||||
django-crispy-forms
|
||||
django-crispy-forms==1.14.0
|
||||
crispy-bulma
|
||||
stripe
|
||||
# stripe
|
||||
django-rest-framework
|
||||
uvloop
|
||||
uvicorn[standard]
|
||||
gunicorn
|
||||
django-htmx
|
||||
cryptography
|
||||
django-debug-toolbar
|
||||
django-debug-toolbar-template-profiler
|
||||
orjson
|
||||
msgpack
|
||||
apscheduler
|
||||
watchfiles
|
||||
django-otp
|
||||
django-two-factor-auth
|
||||
django-otp-yubikey
|
||||
phonenumbers
|
||||
qrcode
|
||||
pydantic
|
||||
# glom
|
||||
git+https://git.zm.is/XF/django-crud-mixins
|
||||
# pyroscope-io
|
||||
# For caching
|
||||
redis
|
||||
hiredis
|
||||
django-cachalot
|
||||
django_redis
|
||||
requests
|
||||
|
||||
15
stack.env.example
Normal file
15
stack.env.example
Normal file
@@ -0,0 +1,15 @@
|
||||
APP_PORT=5006
|
||||
REPO_DIR=.
|
||||
APP_LOCAL_SETTINGS=./app/local_settings.py
|
||||
APP_DATABASE_FILE=./db.sqlite3
|
||||
DOMAIN=example.com
|
||||
URL=https://example.com
|
||||
ALLOWED_HOSTS=example.com
|
||||
NOTIFY_TOPIC=example-topic
|
||||
CSRF_TRUSTED_ORIGINS=https://example.com
|
||||
DEBUG=y
|
||||
SECRET_KEY=
|
||||
STATIC_ROOT=/code/static
|
||||
REGISTRATION_OPEN=0
|
||||
OPERATION=uwsgi
|
||||
BILLING_ENABLED=0
|
||||
Reference in New Issue
Block a user