feat(auth): enable local user signup without SMTP, add admin user creation

- Remove SMTP hard-requirement from /api/auth/signup: when SMTP is not
  configured accounts are activated immediately (no email verification).
  When SMTP is configured the existing email-verification flow is kept.
- Inject allow_signup into global template context via app/views/base.py
- Add data-allow-signup attribute to base.html body tag
- Update common.js _renderLoggedOutAuth to show Sign Up (→ /signup) when
  signup is enabled, otherwise Get Started (→ /pricing)
- Add admin API endpoints before the /{user_id:path} catch-all:
    GET  /api/admin/users/local       – list all local accounts
    POST /api/admin/users/local       – admin-create local account (active immediately)
    DELETE /api/admin/users/local/{id} – delete local account + profile
- Add LocalUserCreate / LocalUserResponse Pydantic schemas
- Update admin_users.html with Local User Accounts section and modals
- Update .env.demo to document ALLOW_LOCAL_SIGNUP
- Update docs/BillingSetup.md: SMTP is optional, document both flows
- Update tests: test_signup_smtp_not_configured now asserts 201 + immediate
  activation; add 7 new integration tests for admin local user endpoints

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-07 19:59:10 +00:00
parent 19d8e04566
commit aa6e2fe001
10 changed files with 645 additions and 72 deletions
+9 -6
View File
@@ -304,11 +304,14 @@ function _makeMenuLink(href, iconClass, label, extraClasses = '') {
/**
* Render the login / get-started buttons for unauthenticated visitors.
* Reads the data-multi-user attribute that the server injects on <body> to
* decide whether to show a prominent "Get Started" CTA alongside the login link.
* Reads the data-multi-user and data-allow-signup attributes that the server
* injects on <body> to decide whether to show a prominent "Get Started" CTA
* alongside the login link, and whether it should link to /signup or /pricing.
*/
function _renderLoggedOutAuth(authSection, mobileAuthSection) {
const multiUser = document.body.getAttribute('data-multi-user') === 'true';
const allowSignup = document.body.getAttribute('data-allow-signup') === 'true';
const startHref = allowSignup ? '/signup' : '/pricing';
if (authSection) {
authSection.textContent = '';
@@ -324,10 +327,10 @@ function _renderLoggedOutAuth(authSection, mobileAuthSection) {
if (multiUser) {
const startLink = document.createElement('a');
startLink.href = '/pricing';
startLink.href = startHref;
startLink.className =
'px-3 py-1.5 rounded-md text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500';
startLink.textContent = 'Get Started';
startLink.textContent = allowSignup ? 'Sign Up' : 'Get Started';
row.appendChild(startLink);
}
@@ -350,14 +353,14 @@ function _renderLoggedOutAuth(authSection, mobileAuthSection) {
if (multiUser) {
const startLink = document.createElement('a');
startLink.href = '/pricing';
startLink.href = startHref;
startLink.className =
'block px-3 py-3 rounded-md text-base font-medium text-white bg-blue-600 hover:text-white hover:bg-blue-700 mt-1';
const startIcon = document.createElement('i');
startIcon.className = 'fas fa-arrow-right mr-2';
startIcon.setAttribute('aria-hidden', 'true');
startLink.appendChild(startIcon);
startLink.appendChild(document.createTextNode('Get Started'));
startLink.appendChild(document.createTextNode(allowSignup ? 'Sign Up' : 'Get Started'));
mobileAuthSection.appendChild(startLink);
}
}