improve registration flows
Build and Push Docker Images / Build and Push Docker Images (push) Failing after 5s

This commit is contained in:
2026-08-04 01:11:20 +05:00
parent 9d8b5cd26f
commit c1f661cdb6
14 changed files with 485 additions and 21 deletions
+2
View File
@@ -64,3 +64,5 @@ TG_CHAT_ID=""
# Optional forum topic id. If empty/unset, message_thread_id is omitted and # Optional forum topic id. If empty/unset, message_thread_id is omitted and
# messages post to the group's General topic. # messages post to the group's General topic.
TG_TOPIC_ID="" TG_TOPIC_ID=""
# Public frontend base URL used in SMS/Telegram links
FRONTEND_URL=https://portal.sarlink.net
+2
View File
@@ -164,3 +164,5 @@ cython_debug/
staticfiles/ staticfiles/
postgres_data/ postgres_data/
media/ media/
# Uploaded ID/passport photos (bind-mounted volume in production)
storage/
@@ -0,0 +1,66 @@
# Generated by Django 5.2 on 2026-08-03 17:40
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0018_user_agreement"),
]
operations = [
migrations.AddField(
model_name="user",
name="id_card_photo",
field=models.ImageField(
blank=True,
help_text="ID card / passport photo uploaded by the user for manual review.",
null=True,
upload_to="id_cards/",
),
),
migrations.AddField(
model_name="user",
name="status",
field=models.CharField(
choices=[
("pending", "Pending"),
("verified", "Verified"),
("id_required", "ID required"),
("id_submitted", "ID submitted"),
],
db_index=True,
default="pending",
max_length=20,
),
),
migrations.CreateModel(
name="IdUploadToken",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("key", models.CharField(db_index=True, max_length=64, unique=True)),
("created_at", models.DateTimeField(default=django.utils.timezone.now)),
("expires_at", models.DateTimeField()),
("used_at", models.DateTimeField(blank=True, null=True)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="id_upload_tokens",
to=settings.AUTH_USER_MODEL,
),
),
],
),
]
@@ -0,0 +1,24 @@
# Generated by Django 5.2 on 2026-08-03 18:00
import api.storages
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0019_user_id_card_photo_user_status_iduploadtoken"),
]
operations = [
migrations.AlterField(
model_name="user",
name="id_card_photo",
field=models.ImageField(
blank=True,
help_text="ID card / passport photo uploaded by the user for manual review.",
null=True,
storage=api.storages.idcard_storage,
upload_to="",
),
),
]
@@ -0,0 +1,24 @@
# Generated by Django 5.2 on 2026-08-03 18:24
import api.storages
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0020_alter_user_id_card_photo"),
]
operations = [
migrations.AlterField(
model_name="user",
name="id_card_photo",
field=models.FileField(
blank=True,
help_text="ID card / passport (JPG, PNG or PDF) uploaded by the user for manual review.",
null=True,
storage=api.storages.idcard_storage,
upload_to="",
),
),
]
+65
View File
@@ -2,16 +2,31 @@
This is the models module for api. This is the models module for api.
""" """
import secrets
from datetime import timedelta from datetime import timedelta
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.db import models from django.db import models
from .managers import CustomUserManager from .managers import CustomUserManager
from .storages import idcard_storage
from django.utils import timezone from django.utils import timezone
import pyotp import pyotp
from billing.models import WalletTransaction from billing.models import WalletTransaction
class User(AbstractUser): class User(AbstractUser):
# Registration workflow states. `verified` (below) stays the login gate and
# is kept in sync: verified is True iff status == STATUS_VERIFIED.
STATUS_PENDING = "pending" # created, awaiting/undergoing auto-verify
STATUS_VERIFIED = "verified" # approved, may log in
STATUS_ID_REQUIRED = "id_required" # must (re)upload an ID/passport photo
STATUS_ID_SUBMITTED = "id_submitted" # photo uploaded, awaiting admin review
STATUS_CHOICES = [
(STATUS_PENDING, "Pending"),
(STATUS_VERIFIED, "Verified"),
(STATUS_ID_REQUIRED, "ID required"),
(STATUS_ID_SUBMITTED, "ID submitted"),
]
address = models.CharField(max_length=255, blank=True) address = models.CharField(max_length=255, blank=True)
email = models.EmailField(blank=True, null=True, unique=True) email = models.EmailField(blank=True, null=True, unique=True)
mobile = models.CharField( mobile = models.CharField(
@@ -23,6 +38,16 @@ class User(AbstractUser):
max_length=255, blank=True, unique=True, null=True, db_index=True max_length=255, blank=True, unique=True, null=True, db_index=True
) )
verified = models.BooleanField(default=False) verified = models.BooleanField(default=False)
status = models.CharField(
max_length=20, choices=STATUS_CHOICES, default=STATUS_PENDING, db_index=True
)
id_card_photo = models.FileField(
upload_to="",
storage=idcard_storage,
blank=True,
null=True,
help_text="ID card / passport (JPG, PNG or PDF) uploaded by the user for manual review.",
)
is_admin = models.BooleanField(default=False) is_admin = models.BooleanField(default=False)
dob = models.DateField(blank=True, null=True) dob = models.DateField(blank=True, null=True)
terms_accepted = models.BooleanField(default=False) terms_accepted = models.BooleanField(default=False)
@@ -79,6 +104,46 @@ class User(AbstractUser):
# by createsuperuser. # by createsuperuser.
REQUIRED_FIELDS = [] REQUIRED_FIELDS = []
def set_status(self, status, *, save=True):
"""Set workflow status and keep the `verified` login gate in sync."""
self.status = status
self.verified = status == self.STATUS_VERIFIED
if save:
self.save(update_fields=["status", "verified", "updated_at"])
class IdUploadToken(models.Model):
"""
One-time-ish magic-link token letting an unverified user reach the public
ID-upload page without logging in. Issuing a fresh token supersedes any
outstanding ones for the same user.
"""
key = models.CharField(max_length=64, unique=True, db_index=True)
user = models.ForeignKey(
"User", on_delete=models.CASCADE, related_name="id_upload_tokens"
)
created_at = models.DateTimeField(default=timezone.now)
expires_at = models.DateTimeField()
used_at = models.DateTimeField(null=True, blank=True)
@classmethod
def issue(cls, user, ttl_hours=48):
cls.objects.filter(user=user, used_at__isnull=True).update(
used_at=timezone.now()
)
return cls.objects.create(
key=secrets.token_urlsafe(32),
user=user,
expires_at=timezone.now() + timedelta(hours=ttl_hours),
)
def is_valid(self):
return self.used_at is None and self.expires_at > timezone.now()
def __str__(self) -> str:
return f"IdUploadToken(user={self.user_id}, valid={self.is_valid()})"
class TemporaryUser(models.Model): class TemporaryUser(models.Model):
t_id = models.AutoField(primary_key=True) t_id = models.AutoField(primary_key=True)
+2
View File
@@ -121,6 +121,8 @@ class CustomReadOnlyUserSerializer(serializers.ModelSerializer):
"id_card", "id_card",
"agreement", "agreement",
"wallet_balance", "wallet_balance",
"status",
"id_card_photo",
) )
depth = 1 depth = 1
+26
View File
@@ -0,0 +1,26 @@
"""Storage backends for the api app."""
from django.conf import settings
from django.core.files.storage import FileSystemStorage
class IdCardStorage(FileSystemStorage):
"""Filesystem storage for uploaded ID/passport photos.
Location/URL come from settings (IDCARD_STORAGE_ROOT / IDCARD_STORAGE_URL)
so the directory can be bind-mounted as a persistent volume in production.
"""
def __init__(self, **kwargs):
kwargs.setdefault("location", settings.IDCARD_STORAGE_ROOT)
kwargs.setdefault("base_url", settings.IDCARD_STORAGE_URL)
super().__init__(**kwargs)
def idcard_storage():
"""Callable referenced by the model field.
Using a named callable keeps the resolved location out of migrations, so
changing the mount path never requires a new migration.
"""
return IdCardStorage()
+3 -2
View File
@@ -180,15 +180,16 @@ def verify_user_with_person_api_task(user_id: int):
f"{(t_user.t_island.name if t_user.t_island else 'N/A')}" f"{(t_user.t_island.name if t_user.t_island else 'N/A')}"
) )
frontend_url = env.str("FRONTEND_URL", default="https://portal.sarlink.net")
verification_failed_message = ( verification_failed_message = (
f"⚠️ *User verification failed*\n\n{user_details}\n" f"⚠️ *User verification failed*\n\n{user_details}\n"
f"Visit [SAR Link Portal](https://portal.sarlink.net/users/{user_id}/details) " f"Visit [SAR Link Portal]({frontend_url}/users/{user_id}/details) "
f"to manually verify this user." f"to manually verify this user."
) )
verification_success_message = ( verification_success_message = (
f"✅ *New user registered and verified*\n\n{user_details}\n" f"✅ *New user registered and verified*\n\n{user_details}\n"
f"View on [SAR Link Portal](https://portal.sarlink.net/users/{user_id}/details)." f"View on [SAR Link Portal]({frontend_url}/users/{user_id}/details)."
) )
def _run(coro) -> None: def _run(coro) -> None:
+25
View File
@@ -18,9 +18,14 @@ from .views import (
filter_user, filter_user,
filter_temporary_user, filter_temporary_user,
VerifyOTPView, VerifyOTPView,
ResendRegistrationOTPView,
UserVerifyAPIView, UserVerifyAPIView,
UserUpdateAPIView, UserUpdateAPIView,
UserRejectAPIView, UserRejectAPIView,
RequestIdUploadAPIView,
IdUploadTokenInfoAPIView,
IdUploadStartAPIView,
IdUploadSubmitAPIView,
AgreementUpdateAPIView, AgreementUpdateAPIView,
PersonVerifyAPIView, PersonVerifyAPIView,
) )
@@ -29,6 +34,11 @@ from .views import (
urlpatterns = [ urlpatterns = [
path("register/", CreateTemporaryUserView.as_view(), name="register"), path("register/", CreateTemporaryUserView.as_view(), name="register"),
path("register/verify/", VerifyOTPView.as_view(), name="verify-otp"), path("register/verify/", VerifyOTPView.as_view(), name="verify-otp"),
path(
"register/resend-otp/",
ResendRegistrationOTPView.as_view(),
name="resend-registration-otp",
),
path("profile/", UserprofileAPIView.as_view(), name="profile"), path("profile/", UserprofileAPIView.as_view(), name="profile"),
path("login/", LoginView.as_view(), name="knox_login"), path("login/", LoginView.as_view(), name="knox_login"),
path("logout/", knox_views.LogoutView.as_view(), name="knox_logout"), path("logout/", knox_views.LogoutView.as_view(), name="knox_logout"),
@@ -48,6 +58,21 @@ urlpatterns = [
name="user-agreement-update", name="user-agreement-update",
), ),
path("users/<int:pk>/reject/", UserRejectAPIView.as_view(), name="user-reject"), path("users/<int:pk>/reject/", UserRejectAPIView.as_view(), name="user-reject"),
path(
"users/<int:pk>/request-id-card/",
RequestIdUploadAPIView.as_view(),
name="user-request-id-card",
),
# Public, unauthenticated self-service ID-upload flow (single-use magic link)
path(
"id-upload/token/", IdUploadTokenInfoAPIView.as_view(), name="id-upload-token"
),
path(
"id-upload/start/", IdUploadStartAPIView.as_view(), name="id-upload-start"
),
path(
"id-upload/submit/", IdUploadSubmitAPIView.as_view(), name="id-upload-submit"
),
path("person/<str:id_card>/", PersonVerifyAPIView.as_view(), name="person-verify"), path("person/<str:id_card>/", PersonVerifyAPIView.as_view(), name="person-verify"),
path("healthcheck/", healthcheck, name="healthcheck"), path("healthcheck/", healthcheck, name="healthcheck"),
path("atolls/", ListAtollView.as_view(), name="atolls"), path("atolls/", ListAtollView.as_view(), name="atolls"),
+231 -18
View File
@@ -8,12 +8,15 @@ from rest_framework.authtoken.serializers import AuthTokenSerializer
from api.filters import UserFilter from api.filters import UserFilter
from api.mixins import StaffEditorPermissionMixin from api.mixins import StaffEditorPermissionMixin
from api.permissions import IsAdminOrStaffPermission, user_is_admin from api.permissions import IsAdminOrStaffPermission, user_is_admin
from api.models import User, Atoll, Island, TemporaryUser from api.models import User, Atoll, Island, TemporaryUser, IdUploadToken
from api.notifications import send_sms from api.notifications import send_sms
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.parsers import MultiPartParser, FormParser
from rest_framework.decorators import api_view, permission_classes from rest_framework.decorators import api_view, permission_classes
from decouple import config
import os
from api.serializers import ( from api.serializers import (
AtollSerializer, AtollSerializer,
IslandSerializer, IslandSerializer,
@@ -54,6 +57,48 @@ def healthcheck(request):
return Response({"status": "Good"}, status=status.HTTP_200_OK) return Response({"status": "Good"}, status=status.HTTP_200_OK)
FRONTEND_URL = config("FRONTEND_URL", default="https://portal.sarlink.net")
# File types accepted for the ID card / passport upload.
ID_PHOTO_CONTENT_TYPES = [
"image/jpeg",
"image/jpg",
"image/png",
"application/pdf",
]
# Default editable body of the "please upload your ID" SMS. The greeting
# ("Dear <name>,"), the secure upload link, and the "- SAR Link" signature are
# always added by request_id_upload() and are NOT part of the editable body.
DEFAULT_ID_UPLOAD_BODY = (
"We're sorry, but your SAR Link account registration could not be "
"approved automatically.\n\n"
"Please upload a clear photo of your ID card / passport at the "
"link below."
)
def request_id_upload(user, message_body=""):
"""
Mark a user as needing an ID/passport photo, mint a fresh magic-link token,
and SMS them the upload link. Used both by the automatic auto-verify-fail
path and by the admin "Request ID Upload" action.
`message_body` is the admin-editable middle of the message; when blank the
default body is used. The greeting, the one-time upload link, and the
signature are always appended here so the link is never exposed to the admin.
"""
token = IdUploadToken.issue(user)
user.set_status(User.STATUS_ID_REQUIRED)
full_name = f"{user.first_name} {user.last_name}".strip() or "Customer"
link = f"{FRONTEND_URL}/upload-id?token={token.key}"
body = (message_body or "").strip() or DEFAULT_ID_UPLOAD_BODY
message = f"Dear {full_name},\n\n{body}\n\n{link}\n\n- SAR Link"
if user.mobile:
send_sms(mobile=user.mobile, message=message)
return token
class CreateTemporaryUserView(generics.CreateAPIView): class CreateTemporaryUserView(generics.CreateAPIView):
serializer_class = TemporaryUserSerializer serializer_class = TemporaryUserSerializer
@@ -173,7 +218,7 @@ class VerifyOTPView(generics.GenericAPIView):
return Response({"message": "Invalid OTP."}, status=400) return Response({"message": "Invalid OTP."}, status=400)
# Create real user # Create real user
User.objects.create_user( user = User.objects.create_user(
first_name=temp_user.t_first_name, first_name=temp_user.t_first_name,
last_name=temp_user.t_last_name, last_name=temp_user.t_last_name,
username=str(temp_user.t_username), username=str(temp_user.t_username),
@@ -190,28 +235,72 @@ class VerifyOTPView(generics.GenericAPIView):
policy_accepted=temp_user.t_policy_accepted, policy_accepted=temp_user.t_policy_accepted,
) )
if temp_user.t_verified:
send_sms(
t_user.t_mobile,
f"Dear {temp_user.t_first_name} {temp_user.t_last_name}, \n\nYour account has been successfully verified. \n\nYou can now manage your devices and make payments through our portal at https://portal.sarlink.net. \n\n - SAR Link",
)
else:
send_sms(
t_user.t_mobile,
f"Dear {t_user.t_first_name} {t_user.t_last_name}, \n\nYour account registration is being processed. \n\nWe will notify you once verification is complete. \n\n - SAR Link",
)
# You can now trigger registry verification as a signal or task
temp_user.otp_verified = True temp_user.otp_verified = True
temp_user.save() temp_user.save()
if temp_user.t_verified:
user.set_status(User.STATUS_VERIFIED)
send_sms(
t_user.t_mobile,
f"Dear {temp_user.t_first_name} {temp_user.t_last_name}, \n\nYour account has been successfully verified. \n\nYou can now manage your devices and make payments through our portal at {FRONTEND_URL}. \n\n - SAR Link",
)
return Response( return Response(
{ {
"message": "User created successfully.", "message": "User created successfully.",
"verified": temp_user.t_verified "verified": True,
"status": user.status,
} }
) )
# Auto-verification failed -> ask the user to upload their ID/passport.
token = request_id_upload(user)
return Response(
{
"message": "User created successfully.",
"verified": False,
"status": user.status,
"upload_token": token.key,
}
)
class ResendRegistrationOTPView(generics.GenericAPIView):
"""Resend the registration OTP for a pending (not-yet-verified) signup.
Handles the "registered but closed the browser before entering the OTP"
case: logging in again finds the TemporaryUser and resends a fresh code
instead of dead-ending at the signup form (which rejects the existing
mobile as already taken).
"""
permission_classes = (permissions.AllowAny,)
throttle_classes = []
def post(self, request, *args, **kwargs):
mobile = request.data.get("mobile", "")
t_user = TemporaryUser.objects.filter(t_mobile=mobile).first()
if (
not t_user
or t_user.otp_verified
or User.objects.filter(mobile=mobile).exists()
):
return Response(
{"message": "No pending registration for this number."},
status=status.HTTP_400_BAD_REQUEST,
)
# Reset the OTP validity window (is_expired() is based on created_at)
# and resend a fresh code.
t_user.created_at = timezone.now()
t_user.save(update_fields=["created_at"])
otp = t_user.generate_otp()
otp_expiry = timezone.now() + timezone.timedelta(minutes=3)
formatted_time = otp_expiry.strftime("%d/%m/%Y %H:%M:%S")
send_otp(
str(t_user.t_mobile),
f"Your Registration SARLink OTP: {otp}. \nExpires at {formatted_time}. \n\n- SAR Link",
)
return Response({"message": "OTP resent.", "t_username": t_user.t_username})
class LoginView(KnoxLoginView): class LoginView(KnoxLoginView):
# login view extending KnoxLoginView # login view extending KnoxLoginView
@@ -447,16 +536,140 @@ class UserRejectAPIView(generics.DestroyAPIView):
{"message": "You are not authorized to reject this user."}, {"message": "You are not authorized to reject this user."},
status=status.HTTP_403_FORBIDDEN, status=status.HTTP_403_FORBIDDEN,
) )
full_name = f"{user.first_name} {user.last_name}".strip() or "Customer"
rejection_message = (
f"Dear {full_name}, \n\n"
"We're sorry, but your SAR Link account registration could not be "
"approved at this time. \n\n"
f"Reason: {rejection_details} \n\n"
"Register again at "
f"{FRONTEND_URL}, or contact us for assistance. \n\n"
" - SAR Link"
)
user.delete() user.delete()
t_user = get_object_or_404(TemporaryUser, t_mobile=user.mobile) t_user = get_object_or_404(TemporaryUser, t_mobile=user.mobile)
t_user.delete() t_user.delete()
send_sms(message=rejection_details, mobile=mobile_number) send_sms(message=rejection_message, mobile=mobile_number)
return Response( return Response(
{"message": "User successfully rejected."}, {"message": "User successfully rejected."},
status=status.HTTP_204_NO_CONTENT, status=status.HTTP_204_NO_CONTENT,
) )
class RequestIdUploadAPIView(generics.GenericAPIView):
"""Admin action: ask a user to (re)upload their ID/passport photo."""
permission_classes = [IsAdminOrStaffPermission]
queryset = User.objects.all()
lookup_field = "pk"
def post(self, request, *args, **kwargs):
user = get_object_or_404(User, pk=kwargs.get("pk"))
if user.is_superuser:
return Response(
{"message": "You cannot modify a superuser."},
status=status.HTTP_403_FORBIDDEN,
)
if not user.mobile:
return Response(
{"message": "User does not have a mobile number."},
status=status.HTTP_400_BAD_REQUEST,
)
message_body = request.data.get("message", "")
request_id_upload(user, message_body=message_body)
return Response({"message": "ID upload request sent to the user."})
class IdUploadTokenInfoAPIView(generics.GenericAPIView):
"""Public: validate a magic-link token and return whose upload it is."""
permission_classes = [permissions.AllowAny]
def get(self, request, *args, **kwargs):
key = request.query_params.get("token", "")
token = IdUploadToken.objects.filter(key=key).select_related("user").first()
if not token or not token.is_valid():
return Response(
{"message": "This upload link is invalid or has expired."},
status=status.HTTP_400_BAD_REQUEST,
)
user = token.user
return Response(
{
"first_name": user.first_name,
"last_name": user.last_name,
"status": user.status,
}
)
class IdUploadStartAPIView(generics.GenericAPIView):
"""Public: mint a fresh upload token for a mobile whose account needs an ID.
Lets the login flow send an `id_required` user straight to the upload page
instead of a "pending verification" dead-end. Only works for accounts
actually in the id_required state.
"""
permission_classes = [permissions.AllowAny]
def post(self, request, *args, **kwargs):
mobile = request.data.get("mobile", "")
user = User.objects.filter(mobile=mobile).first()
if not user or user.status != User.STATUS_ID_REQUIRED:
return Response(
{"message": "No ID upload is pending for this number."},
status=status.HTTP_400_BAD_REQUEST,
)
token = IdUploadToken.issue(user)
return Response({"token": token.key})
class IdUploadSubmitAPIView(generics.GenericAPIView):
"""Public: accept the ID/passport photo for a valid token."""
permission_classes = [permissions.AllowAny]
parser_classes = [MultiPartParser, FormParser]
def post(self, request, *args, **kwargs):
key = request.data.get("token", "")
token = IdUploadToken.objects.filter(key=key).select_related("user").first()
if not token or not token.is_valid():
return Response(
{"message": "This upload link is invalid or has expired."},
status=status.HTTP_400_BAD_REQUEST,
)
photo = request.data.get("id_card_photo")
if not photo:
return Response(
{"message": "An ID card / passport photo is required."},
status=status.HTTP_400_BAD_REQUEST,
)
if photo.size > 10 * 1024 * 1024:
return Response(
{"message": "File size exceeds the 10 MB limit."},
status=status.HTTP_400_BAD_REQUEST,
)
if getattr(photo, "content_type", "") not in ID_PHOTO_CONTENT_TYPES:
return Response(
{
"message": "Invalid file type. Please upload a JPG, PNG or PDF file."
},
status=status.HTTP_400_BAD_REQUEST,
)
user = token.user
ext = os.path.splitext(photo.name)[1].lower() or ".jpg"
photo.name = f"{uuid.uuid4()}_{user.id}_id_card{ext}"
user.id_card_photo = photo
user.set_status(User.STATUS_ID_SUBMITTED, save=False)
user.save()
# Single-use: burn the token so the link can't be reused for another
# upload. A fresh link is minted if an admin requests another upload.
token.used_at = timezone.now()
token.save(update_fields=["used_at"])
return Response({"message": "Your ID has been submitted for review."})
@api_view(["GET"]) @api_view(["GET"])
def filter_user(request): def filter_user(request):
id_card = request.GET.get("id_card", "").strip() or None id_card = request.GET.get("id_card", "").strip() or None
@@ -473,13 +686,13 @@ def filter_user(request):
elif mobile: elif mobile:
filters = Q(mobile=mobile) filters = Q(mobile=mobile)
user = User.objects.only("id", "verified").filter(filters).first() user = User.objects.only("id", "verified", "status").filter(filters).first()
print(f"Querying with filters: {filters}") print(f"Querying with filters: {filters}")
print(f"Found user: {user}") print(f"Found user: {user}")
return Response( return Response(
{"ok": True, "verified": user.verified} {"ok": True, "verified": user.verified, "status": user.status}
if user if user
else {"ok": False, "verified": False} else {"ok": False, "verified": False}
) )
+8
View File
@@ -201,6 +201,14 @@ STATICFILES_DIRS = [
MEDIA_URL = "/media/" MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media") MEDIA_ROOT = os.path.join(BASE_DIR, "media")
# Uploaded ID/passport photos live in a dedicated directory so it can be
# bind-mounted as a persistent volume in production. Override with the
# IDCARD_STORAGE_ROOT / IDCARD_STORAGE_URL env vars to point at the mount.
IDCARD_STORAGE_ROOT = os.environ.get(
"IDCARD_STORAGE_ROOT", os.path.join(BASE_DIR, "storage", "idcards")
)
IDCARD_STORAGE_URL = os.environ.get("IDCARD_STORAGE_URL", "/storage/idcards/")
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
+3
View File
@@ -37,6 +37,9 @@ urlpatterns = [
] ]
if settings.DEBUG: if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(
settings.IDCARD_STORAGE_URL, document_root=settings.IDCARD_STORAGE_ROOT
)
urlpatterns += (path("api-auth/", include("rest_framework.urls")),) urlpatterns += (path("api-auth/", include("rest_framework.urls")),)
urlpatterns += (path("__debug__/", include("debug_toolbar.urls")),) urlpatterns += (path("__debug__/", include("debug_toolbar.urls")),)
urlpatterns += (path("api/schema/", SpectacularAPIView.as_view(), name="schema"),) urlpatterns += (path("api/schema/", SpectacularAPIView.as_view(), name="schema"),)
+3
View File
@@ -0,0 +1,3 @@
"We're sorry, but your SAR Link account registration could not be approved automatically. \n
f{note} or if empty "Please upload a clear photo of your ID card / passport here: \n"