Implement Platform Superuser UI and Fix API Role Casing

- Update API to return lowercase roles for frontend compatibility
- Fix Tenant owner lookup in platform admin serializer
- Update frontend App.tsx to match tarball implementation
- Prioritize vite.config.js for HMR support
- Include pending CSP and CORS configuration updates
This commit is contained in:
poduck
2025-11-27 02:16:05 -05:00
parent 2e111364a2
commit 249a9040d2
10 changed files with 143 additions and 8 deletions

View File

@@ -10,7 +10,6 @@ import { useCurrentUser, useMasquerade, useLogout } from './hooks/useAuth';
import { useCurrentBusiness } from './hooks/useBusiness';
import { useUpdateBusiness } from './hooks/useBusiness';
import { setCookie } from './utils/cookies';
import { DevQuickLogin } from './components/DevQuickLogin';
// Import Login Page
import LoginPage from './pages/LoginPage';
@@ -549,9 +548,8 @@ const App: React.FC = () => {
<Router>
<AppContent />
</Router>
<DevQuickLogin />
</QueryClientProvider>
);
};
export default App;
export default App;

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -14,6 +14,8 @@ SECRET_KEY = env(
# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1", ".lvh.me", "lvh.me"] # noqa: S104
from corsheaders.defaults import default_headers
# django-cors-headers
# ------------------------------------------------------------------------------
# https://github.com/adamchainz/django-cors-headers#configuration
@@ -29,6 +31,9 @@ CORS_ALLOWED_ORIGIN_REGEXES = [
r"^http://.*\.lvh\.me:517[34]$", # Allow all subdomains on ports 5173/5174
]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS = list(default_headers) + [
"x-business-subdomain",
]
# CSRF
# ------------------------------------------------------------------------------

View File

@@ -41,6 +41,7 @@ SHARED_APPS = [
'hijack.contrib.admin',
'crispy_forms',
'crispy_bootstrap5',
'csp',
]
# Tenant-specific apps - Each tenant gets isolated data in their own schema
@@ -87,6 +88,7 @@ MIDDLEWARE = [
# 2. Security middleware
'django.middleware.security.SecurityMiddleware',
'csp.middleware.CSPMiddleware',
'corsheaders.middleware.CorsMiddleware', # Moved up for better CORS handling
'whitenoise.middleware.WhiteNoiseMiddleware',
@@ -211,3 +213,40 @@ LOGGING['loggers']['smoothschedule.security.masquerade'] = {
# Create logs directory if it doesn't exist
import os
os.makedirs(BASE_DIR / 'logs', exist_ok=True)
# =============================================================================
# CONTENT SECURITY POLICY (CSP)
# =============================================================================
# https://django-csp.readthedocs.io/en/latest/configuration.html
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = (
"'self'",
"https://js.stripe.com",
"https://connect-js.stripe.com",
"https://www.googletagmanager.com",
"https://www.google-analytics.com",
"blob:", # Required for Stripe
)
CSP_STYLE_SRC = (
"'self'",
"'unsafe-inline'", # Required for Stripe and many UI libraries
)
CSP_IMG_SRC = (
"'self'",
"data:",
"https://*.stripe.com",
"https://www.google-analytics.com",
)
CSP_CONNECT_SRC = (
"'self'",
"https://api.stripe.com",
"https://www.google-analytics.com",
"https://stats.g.doubleclick.net",
)
CSP_FRAME_SRC = (
"'self'",
"https://js.stripe.com",
"https://hooks.stripe.com",
"https://connect-js.stripe.com",
)

View File

@@ -43,7 +43,7 @@ class TenantSerializer(serializers.ModelSerializer):
try:
owner = User.objects.filter(
role=User.Role.TENANT_OWNER,
# Note: We need to add a tenant reference to User model
tenant=obj
).first()
if owner:
@@ -52,7 +52,7 @@ class TenantSerializer(serializers.ModelSerializer):
'username': owner.username,
'full_name': owner.full_name,
'email': owner.email,
'role': owner.role,
'role': owner.role.lower(),
}
except:
pass
@@ -65,6 +65,7 @@ class PlatformUserSerializer(serializers.ModelSerializer):
business_name = serializers.SerializerMethodField()
business_subdomain = serializers.SerializerMethodField()
full_name = serializers.SerializerMethodField()
role = serializers.SerializerMethodField()
class Meta:
model = User
@@ -76,6 +77,9 @@ class PlatformUserSerializer(serializers.ModelSerializer):
]
read_only_fields = fields
def get_role(self, obj):
return obj.role.lower()
def get_full_name(self, obj):
"""Get user's full name"""
return obj.full_name

View File

@@ -141,6 +141,7 @@ dev = [
"factory-boy==3.3.2",
"ipdb==0.13.13",
"mypy==1.18.2",
"playwright>=1.56.0",
"pre-commit==4.5.0",
"psycopg[c]==3.2.13",
"pytest==9.0.1",
@@ -192,4 +193,5 @@ dependencies = [
"sentry-sdk==2.46.0",
"whitenoise==6.11.0",
"stripe>=7.0.0",
"django-csp==3.8.0",
]

View File

@@ -25,7 +25,7 @@ def current_business_view(request):
# Get subdomain from primary domain
subdomain = None
primary_domain = tenant.domain_set.filter(is_primary=True).first()
primary_domain = tenant.domains.filter(is_primary=True).first()
if primary_domain:
# Extract subdomain from domain (e.g., "mybusiness.lvh.me" -> "mybusiness")
domain_parts = primary_domain.domain.split('.')

View File

@@ -23,14 +23,20 @@ def current_user_view(request):
business_subdomain = None
if user.tenant:
business_name = user.tenant.name
business_subdomain = user.tenant.subdomain
# user.tenant.subdomain does not exist. Fetch from domains relation.
# Assuming 'domains' is the related_name from Domain model to Tenant
primary_domain = user.tenant.domains.filter(is_primary=True).first()
if primary_domain:
business_subdomain = primary_domain.domain.split('.')[0]
else:
business_subdomain = user.tenant.schema_name
user_data = {
'id': user.id,
'username': user.username,
'email': user.email,
'name': user.full_name,
'role': user.role,
'role': user.role.lower(),
'avatar_url': None, # TODO: Implement avatar
'email_verified': False, # TODO: Implement email verification
'is_staff': user.is_staff,

81
smoothschedule/uv.lock generated
View File

@@ -520,6 +520,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/58/ac3a11950baaf75c1f3242e3af9dfe45201f6ee10c113dd37a9c000876d2/django_crispy_forms-2.5-py3-none-any.whl", hash = "sha256:adc99d5901baca09479c53bf536b3909e80a9f2bb299438a223de4c106ebf1f9", size = 31464, upload-time = "2025-11-06T20:44:00.795Z" },
]
[[package]]
name = "django-csp"
version = "3.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/68/16/c3c65ad59997284402e54d00797c7aca96572df911aede3e1f2cc2e029f8/django_csp-3.8.tar.gz", hash = "sha256:ef0f1a9f7d8da68ae6e169c02e9ac661c0ecf04db70e0d1d85640512a68471c0", size = 13341, upload-time = "2024-03-01T14:00:30.013Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/ff/2c7a4b6706125a17bd0071802e4894c28772cfcdea20a086a2be3c5fafda/django_csp-3.8-py3-none-any.whl", hash = "sha256:19b2978b03fcd73517d7d67acbc04fbbcaec0facc3e83baa502965892d1e0719", size = 17410, upload-time = "2024-03-01T14:00:28.135Z" },
]
[[package]]
name = "django-debug-toolbar"
version = "6.1.0"
@@ -822,6 +834,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/ff/ee2f67c0ff146ec98b5df1df637b2bc2d17beeb05df9f427a67bd7a7d79c/flower-2.0.1-py2.py3-none-any.whl", hash = "sha256:9db2c621eeefbc844c8dd88be64aef61e84e2deb29b271e02ab2b5b9f01068e2", size = 383553, upload-time = "2023-08-13T14:37:41.552Z" },
]
[[package]]
name = "greenlet"
version = "3.2.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" },
{ url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" },
{ url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" },
{ url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" },
{ url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" },
{ url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" },
{ url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" },
{ url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" },
{ url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" },
{ url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" },
{ url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" },
]
[[package]]
name = "gunicorn"
version = "23.0.0"
@@ -1222,6 +1253,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
]
[[package]]
name = "playwright"
version = "1.56.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "greenlet" },
{ name = "pyee" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/31/a5362cee43f844509f1f10d8a27c9cc0e2f7bdce5353d304d93b2151c1b1/playwright-1.56.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:b33eb89c516cbc6723f2e3523bada4a4eb0984a9c411325c02d7016a5d625e9c", size = 40611424, upload-time = "2025-11-11T18:39:10.175Z" },
{ url = "https://files.pythonhosted.org/packages/ef/95/347eef596d8778fb53590dc326c344d427fa19ba3d42b646fce2a4572eb3/playwright-1.56.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b228b3395212b9472a4ee5f1afe40d376eef9568eb039fcb3e563de8f4f4657b", size = 39400228, upload-time = "2025-11-11T18:39:13.915Z" },
{ url = "https://files.pythonhosted.org/packages/b9/54/6ad97b08b2ca1dfcb4fbde4536c4f45c0d9d8b1857a2d20e7bbfdf43bf15/playwright-1.56.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:0ef7e6fd653267798a8a968ff7aa2dcac14398b7dd7440ef57524e01e0fbbd65", size = 40611424, upload-time = "2025-11-11T18:39:17.093Z" },
{ url = "https://files.pythonhosted.org/packages/e4/76/6d409e37e82cdd5dda3df1ab958130ae32b46e42458bd4fc93d7eb8749cb/playwright-1.56.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:404be089b49d94bc4c1fe0dfb07664bda5ffe87789034a03bffb884489bdfb5c", size = 46263122, upload-time = "2025-11-11T18:39:20.619Z" },
{ url = "https://files.pythonhosted.org/packages/4f/84/fb292cc5d45f3252e255ea39066cd1d2385c61c6c1596548dfbf59c88605/playwright-1.56.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64cda7cf4e51c0d35dab55190841bfcdfb5871685ec22cb722cd0ad2df183e34", size = 46110645, upload-time = "2025-11-11T18:39:24.005Z" },
{ url = "https://files.pythonhosted.org/packages/61/bd/8c02c3388ae14edc374ac9f22cbe4e14826c6a51b2d8eaf86e89fabee264/playwright-1.56.0-py3-none-win32.whl", hash = "sha256:d87b79bcb082092d916a332c27ec9732e0418c319755d235d93cc6be13bdd721", size = 35639837, upload-time = "2025-11-11T18:39:27.174Z" },
{ url = "https://files.pythonhosted.org/packages/64/27/f13b538fbc6b7a00152f4379054a49f6abc0bf55ac86f677ae54bc49fb82/playwright-1.56.0-py3-none-win_amd64.whl", hash = "sha256:3c7fc49bb9e673489bf2622855f9486d41c5101bbed964638552b864c4591f94", size = 35639843, upload-time = "2025-11-11T18:39:30.851Z" },
{ url = "https://files.pythonhosted.org/packages/f2/c7/3ee8b556107995846576b4fe42a08ed49b8677619421f2afacf6ee421138/playwright-1.56.0-py3-none-win_arm64.whl", hash = "sha256:2745490ae8dd58d27e5ea4d9aa28402e8e2991eb84fb4b2fd5fbde2106716f6f", size = 31248959, upload-time = "2025-11-11T18:39:33.998Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
@@ -1318,6 +1368,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
]
[[package]]
name = "pyee"
version = "13.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/03/1fd98d5841cd7964a27d729ccf2199602fe05eb7a405c1462eb7277945ed/pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37", size = 31250, upload-time = "2025-03-17T18:53:15.955Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/4d/b9add7c84060d4c1906abe9a7e5359f2a60f7a9a4f67268b2766673427d8/pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498", size = 15730, upload-time = "2025-03-17T18:53:14.532Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
@@ -1633,6 +1695,7 @@ dependencies = [
{ name = "django-celery-beat" },
{ name = "django-cors-headers" },
{ name = "django-crispy-forms" },
{ name = "django-csp" },
{ name = "django-environ" },
{ name = "django-hijack" },
{ name = "django-model-utils" },
@@ -1649,6 +1712,7 @@ dependencies = [
{ name = "python-slugify" },
{ name = "redis" },
{ name = "sentry-sdk" },
{ name = "stripe" },
{ name = "whitenoise" },
]
@@ -1664,6 +1728,7 @@ dev = [
{ name = "factory-boy" },
{ name = "ipdb" },
{ name = "mypy" },
{ name = "playwright" },
{ name = "pre-commit" },
{ name = "psycopg", extra = ["c"] },
{ name = "pytest" },
@@ -1687,6 +1752,7 @@ requires-dist = [
{ name = "django-celery-beat", specifier = "==2.8.1" },
{ name = "django-cors-headers", specifier = "==4.9.0" },
{ name = "django-crispy-forms", specifier = "==2.5" },
{ name = "django-csp", specifier = "==3.8.0" },
{ name = "django-environ", specifier = "==0.12.0" },
{ name = "django-hijack", specifier = ">=3.4" },
{ name = "django-model-utils", specifier = "==5.0.0" },
@@ -1703,6 +1769,7 @@ requires-dist = [
{ name = "python-slugify", specifier = "==8.0.4" },
{ name = "redis", specifier = "==7.1.0" },
{ name = "sentry-sdk", specifier = "==2.46.0" },
{ name = "stripe", specifier = ">=7.0.0" },
{ name = "whitenoise", specifier = "==6.11.0" },
]
@@ -1718,6 +1785,7 @@ dev = [
{ name = "factory-boy", specifier = "==3.3.2" },
{ name = "ipdb", specifier = "==0.13.13" },
{ name = "mypy", specifier = "==1.18.2" },
{ name = "playwright", specifier = ">=1.56.0" },
{ name = "pre-commit", specifier = "==4.5.0" },
{ name = "psycopg", extras = ["c"], specifier = "==3.2.13" },
{ name = "pytest", specifier = "==9.0.1" },
@@ -1882,6 +1950,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" },
]
[[package]]
name = "stripe"
version = "14.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "requests" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2b/49/08df0acc094587f4d76c2ab31ebbecb8a37312ab558cddaa6a4c2ff19579/stripe-14.0.1.tar.gz", hash = "sha256:f2d56345bf5d41c1f21f814b00174a3173a0b5eb4e8fc46a8f779e3d7a2efc6e", size = 1362960, upload-time = "2025-11-22T01:07:48.862Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d3/88/0db878a84d333a188714f4ade57c9ae765a14a0b81862eb133ad7864711c/stripe-14.0.1-py3-none-any.whl", hash = "sha256:ff25c5e5f085beaa98b6b9c2c729d22ad99068196cbd83fdf82669fd08311b76", size = 1970603, upload-time = "2025-11-22T01:07:47.309Z" },
]
[[package]]
name = "termcolor"
version = "3.2.0"