feat: add email verification toggle to admin settings
Adds platform-level toggle to enable/disable email verification. When disabled, the verification banner is hidden and the send endpoint returns 403. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ from fastapi.security import OAuth2PasswordRequestForm
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
|
from app.core.settings_manager import SettingsManager
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.core.rate_limit import limiter
|
from app.core.rate_limit import limiter
|
||||||
from app.core.security import (
|
from app.core.security import (
|
||||||
@@ -595,6 +596,15 @@ async def reset_password(
|
|||||||
return {"message": "Password has been reset successfully"}
|
return {"message": "Password has been reset successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/email/verification-status")
|
||||||
|
async def get_verification_status(
|
||||||
|
db: Annotated[AsyncSession, Depends(get_db)]
|
||||||
|
):
|
||||||
|
"""Check if email verification is enabled on the platform."""
|
||||||
|
enabled = await SettingsManager.get("email_verification_enabled", db, default=True)
|
||||||
|
return {"enabled": enabled}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/email/send-verification")
|
@router.post("/email/send-verification")
|
||||||
@limiter.limit("3/minute")
|
@limiter.limit("3/minute")
|
||||||
async def send_verification_email(
|
async def send_verification_email(
|
||||||
@@ -603,6 +613,13 @@ async def send_verification_email(
|
|||||||
db: Annotated[AsyncSession, Depends(get_db)]
|
db: Annotated[AsyncSession, Depends(get_db)]
|
||||||
):
|
):
|
||||||
"""Send an email verification link to the current user."""
|
"""Send an email verification link to the current user."""
|
||||||
|
verification_enabled = await SettingsManager.get("email_verification_enabled", db, default=True)
|
||||||
|
if not verification_enabled:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Email verification is currently disabled"
|
||||||
|
)
|
||||||
|
|
||||||
if current_user.email_verified_at is not None:
|
if current_user.email_verified_at is not None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ export const authApi = {
|
|||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getVerificationStatus(): Promise<{ enabled: boolean }> {
|
||||||
|
const response = await apiClient.get<{ enabled: boolean }>('/auth/email/verification-status')
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
|
||||||
async sendVerificationEmail(): Promise<void> {
|
async sendVerificationEmail(): Promise<void> {
|
||||||
await apiClient.post('/auth/email/send-verification')
|
await apiClient.post('/auth/email/send-verification')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { AlertTriangle, X, Loader2 } from 'lucide-react'
|
import { AlertTriangle, X, Loader2 } from 'lucide-react'
|
||||||
import { authApi } from '@/api/auth'
|
import { authApi } from '@/api/auth'
|
||||||
import { useAuthStore } from '@/store/authStore'
|
import { useAuthStore } from '@/store/authStore'
|
||||||
@@ -9,8 +9,15 @@ export function EmailVerificationBanner() {
|
|||||||
const user = useAuthStore((s) => s.user)
|
const user = useAuthStore((s) => s.user)
|
||||||
const [dismissed, setDismissed] = useState(false)
|
const [dismissed, setDismissed] = useState(false)
|
||||||
const [isSending, setIsSending] = useState(false)
|
const [isSending, setIsSending] = useState(false)
|
||||||
|
const [verificationEnabled, setVerificationEnabled] = useState(true)
|
||||||
|
|
||||||
if (!user || user.email_verified_at || dismissed) return null
|
useEffect(() => {
|
||||||
|
authApi.getVerificationStatus()
|
||||||
|
.then((data) => setVerificationEnabled(data.enabled))
|
||||||
|
.catch(() => {})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!user || user.email_verified_at || dismissed || !verificationEnabled) return null
|
||||||
|
|
||||||
const handleResend = async () => {
|
const handleResend = async () => {
|
||||||
setIsSending(true)
|
setIsSending(true)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export function SettingsPage() {
|
|||||||
|
|
||||||
const maintenanceMode = Boolean(settings.maintenance_mode)
|
const maintenanceMode = Boolean(settings.maintenance_mode)
|
||||||
const maintenanceMessage = String(settings.maintenance_message || '')
|
const maintenanceMessage = String(settings.maintenance_message || '')
|
||||||
|
const emailVerificationEnabled = settings.email_verification_enabled !== false
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
@@ -46,6 +47,27 @@ export function SettingsPage() {
|
|||||||
<PageHeader title="Platform Settings" description="Global platform configuration" />
|
<PageHeader title="Platform Settings" description="Global platform configuration" />
|
||||||
|
|
||||||
<div className="max-w-xl space-y-6 bg-card border border-border rounded-xl p-6">
|
<div className="max-w-xl space-y-6 bg-card border border-border rounded-xl p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium text-foreground">Email Verification</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
When enabled, unverified users see a banner prompting them to verify their email.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setSettings({ ...settings, email_verification_enabled: !emailVerificationEnabled })}
|
||||||
|
className={cn(
|
||||||
|
'h-6 w-10 rounded-full transition-colors',
|
||||||
|
emailVerificationEnabled ? 'bg-gradient-brand' : 'bg-accent'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={cn(
|
||||||
|
'h-4 w-4 rounded-full bg-white transition-transform',
|
||||||
|
emailVerificationEnabled ? 'translate-x-5' : 'translate-x-1'
|
||||||
|
)} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium text-foreground">Maintenance Mode</h3>
|
<h3 className="font-medium text-foreground">Maintenance Mode</h3>
|
||||||
|
|||||||
Reference in New Issue
Block a user