feat(auth): Convert login system to use email as username
- Backend login now accepts 'email' field (with backward compatibility) - User creation (signup, invitation, customer) uses email as username - Frontend login form updated with email input and validation - Updated test users to use email addresses as usernames - Updated all translation files (en, es, fr, de) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
import apiClient from './client';
|
||||
|
||||
export interface LoginCredentials {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useQueryClient } from '@tanstack/react-query';
|
||||
import { getBaseDomain, buildSubdomainUrl } from '../utils/domain';
|
||||
|
||||
export interface TestUser {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
role: string;
|
||||
label: string;
|
||||
@@ -14,56 +14,56 @@ export interface TestUser {
|
||||
|
||||
const testUsers: TestUser[] = [
|
||||
{
|
||||
username: 'superuser',
|
||||
email: 'superuser@platform.com',
|
||||
password: 'test123',
|
||||
role: 'SUPERUSER',
|
||||
label: 'Platform Superuser',
|
||||
color: 'bg-purple-600 hover:bg-purple-700',
|
||||
},
|
||||
{
|
||||
username: 'platform_manager',
|
||||
email: 'manager@platform.com',
|
||||
password: 'test123',
|
||||
role: 'PLATFORM_MANAGER',
|
||||
label: 'Platform Manager',
|
||||
color: 'bg-blue-600 hover:bg-blue-700',
|
||||
},
|
||||
{
|
||||
username: 'platform_sales',
|
||||
email: 'sales@platform.com',
|
||||
password: 'test123',
|
||||
role: 'PLATFORM_SALES',
|
||||
label: 'Platform Sales',
|
||||
color: 'bg-green-600 hover:bg-green-700',
|
||||
},
|
||||
{
|
||||
username: 'platform_support',
|
||||
email: 'support@platform.com',
|
||||
password: 'test123',
|
||||
role: 'PLATFORM_SUPPORT',
|
||||
label: 'Platform Support',
|
||||
color: 'bg-yellow-600 hover:bg-yellow-700',
|
||||
},
|
||||
{
|
||||
username: 'tenant_owner',
|
||||
email: 'owner@demo.com',
|
||||
password: 'test123',
|
||||
role: 'TENANT_OWNER',
|
||||
label: 'Business Owner',
|
||||
color: 'bg-indigo-600 hover:bg-indigo-700',
|
||||
},
|
||||
{
|
||||
username: 'tenant_manager',
|
||||
email: 'manager@demo.com',
|
||||
password: 'test123',
|
||||
role: 'TENANT_MANAGER',
|
||||
label: 'Business Manager',
|
||||
color: 'bg-pink-600 hover:bg-pink-700',
|
||||
},
|
||||
{
|
||||
username: 'tenant_staff',
|
||||
email: 'staff@demo.com',
|
||||
password: 'test123',
|
||||
role: 'TENANT_STAFF',
|
||||
label: 'Staff Member',
|
||||
color: 'bg-teal-600 hover:bg-teal-700',
|
||||
},
|
||||
{
|
||||
username: 'customer',
|
||||
email: 'customer@demo.com',
|
||||
password: 'test123',
|
||||
role: 'CUSTOMER',
|
||||
label: 'Customer',
|
||||
@@ -86,11 +86,11 @@ export function DevQuickLogin({ embedded = false }: DevQuickLoginProps) {
|
||||
}
|
||||
|
||||
const handleQuickLogin = async (user: TestUser) => {
|
||||
setLoading(user.username);
|
||||
setLoading(user.email);
|
||||
try {
|
||||
// Call token auth API
|
||||
// Call token auth API - username field contains email since we use email as username
|
||||
const response = await apiClient.post('/auth-token/', {
|
||||
username: user.username,
|
||||
username: user.email,
|
||||
password: user.password,
|
||||
});
|
||||
|
||||
@@ -176,12 +176,12 @@ export function DevQuickLogin({ embedded = false }: DevQuickLoginProps) {
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{testUsers.map((user) => (
|
||||
<button
|
||||
key={user.username}
|
||||
key={user.email}
|
||||
onClick={() => handleQuickLogin(user)}
|
||||
disabled={loading !== null}
|
||||
className={`${user.color} text-white px-3 py-2 rounded text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
>
|
||||
{loading === user.username ? (
|
||||
{loading === user.email ? (
|
||||
<span className="flex items-center justify-center">
|
||||
<svg className="animate-spin h-4 w-4 mr-2" viewBox="0 0 24 24">
|
||||
<circle
|
||||
|
||||
@@ -31,12 +31,12 @@
|
||||
"signIn": "Anmelden",
|
||||
"signOut": "Abmelden",
|
||||
"signingIn": "Anmeldung läuft...",
|
||||
"username": "Benutzername",
|
||||
"email": "E-Mail",
|
||||
"password": "Passwort",
|
||||
"enterUsername": "Geben Sie Ihren Benutzernamen ein",
|
||||
"enterEmail": "Geben Sie Ihre E-Mail-Adresse ein",
|
||||
"enterPassword": "Geben Sie Ihr Passwort ein",
|
||||
"welcomeBack": "Willkommen zurück",
|
||||
"pleaseEnterDetails": "Bitte geben Sie Ihre Daten ein, um sich anzumelden.",
|
||||
"pleaseEnterDetails": "Bitte geben Sie Ihre E-Mail-Adresse und Ihr Passwort ein, um sich anzumelden.",
|
||||
"authError": "Authentifizierungsfehler",
|
||||
"invalidCredentials": "Ungültige Anmeldedaten",
|
||||
"orContinueWith": "Oder fortfahren mit",
|
||||
|
||||
@@ -56,12 +56,12 @@
|
||||
"signIn": "Sign in",
|
||||
"signOut": "Sign Out",
|
||||
"signingIn": "Signing in...",
|
||||
"username": "Username",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"enterUsername": "Enter your username",
|
||||
"enterEmail": "Enter your email",
|
||||
"enterPassword": "Enter your password",
|
||||
"welcomeBack": "Welcome back",
|
||||
"pleaseEnterDetails": "Please enter your details to sign in.",
|
||||
"pleaseEnterDetails": "Please enter your email and password to sign in.",
|
||||
"authError": "Authentication Error",
|
||||
"invalidCredentials": "Invalid credentials",
|
||||
"orContinueWith": "Or continue with",
|
||||
|
||||
@@ -31,12 +31,12 @@
|
||||
"signIn": "Iniciar sesión",
|
||||
"signOut": "Cerrar Sesión",
|
||||
"signingIn": "Iniciando sesión...",
|
||||
"username": "Nombre de usuario",
|
||||
"email": "Correo electrónico",
|
||||
"password": "Contraseña",
|
||||
"enterUsername": "Ingresa tu nombre de usuario",
|
||||
"enterEmail": "Ingresa tu correo electrónico",
|
||||
"enterPassword": "Ingresa tu contraseña",
|
||||
"welcomeBack": "Bienvenido de nuevo",
|
||||
"pleaseEnterDetails": "Por favor ingresa tus datos para iniciar sesión.",
|
||||
"pleaseEnterDetails": "Por favor ingresa tu correo electrónico y contraseña para iniciar sesión.",
|
||||
"authError": "Error de Autenticación",
|
||||
"invalidCredentials": "Credenciales inválidas",
|
||||
"orContinueWith": "O continuar con",
|
||||
|
||||
@@ -31,12 +31,12 @@
|
||||
"signIn": "Se connecter",
|
||||
"signOut": "Déconnexion",
|
||||
"signingIn": "Connexion en cours...",
|
||||
"username": "Nom d'utilisateur",
|
||||
"email": "E-mail",
|
||||
"password": "Mot de passe",
|
||||
"enterUsername": "Entrez votre nom d'utilisateur",
|
||||
"enterEmail": "Entrez votre e-mail",
|
||||
"enterPassword": "Entrez votre mot de passe",
|
||||
"welcomeBack": "Bon retour",
|
||||
"pleaseEnterDetails": "Veuillez entrer vos informations pour vous connecter.",
|
||||
"pleaseEnterDetails": "Veuillez entrer votre e-mail et mot de passe pour vous connecter.",
|
||||
"authError": "Erreur d'Authentification",
|
||||
"invalidCredentials": "Identifiants invalides",
|
||||
"orContinueWith": "Ou continuer avec",
|
||||
|
||||
@@ -11,11 +11,11 @@ import SmoothScheduleLogo from '../components/SmoothScheduleLogo';
|
||||
import OAuthButtons from '../components/OAuthButtons';
|
||||
import LanguageSelector from '../components/LanguageSelector';
|
||||
import { DevQuickLogin } from '../components/DevQuickLogin';
|
||||
import { AlertCircle, Loader2, User, Lock, ArrowRight } from 'lucide-react';
|
||||
import { AlertCircle, Loader2, Mail, Lock, ArrowRight } from 'lucide-react';
|
||||
|
||||
const LoginPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [username, setUsername] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
@@ -27,7 +27,7 @@ const LoginPage: React.FC = () => {
|
||||
setError('');
|
||||
|
||||
loginMutation.mutate(
|
||||
{ username, password },
|
||||
{ email, password },
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
// Check if MFA is required
|
||||
@@ -202,25 +202,25 @@ const LoginPage: React.FC = () => {
|
||||
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
<div className="space-y-4">
|
||||
{/* Username */}
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label htmlFor="username" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{t('auth.username')}
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{t('auth.email')}
|
||||
</label>
|
||||
<div className="relative rounded-md shadow-sm">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<User className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
<Mail className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
className="focus:ring-brand-500 focus:border-brand-500 block w-full pl-10 sm:text-sm border-gray-300 dark:border-gray-700 rounded-lg py-3 bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 transition-colors"
|
||||
placeholder={t('auth.enterUsername')}
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder={t('auth.enterEmail')}
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,21 +61,15 @@ class CustomerSerializer(serializers.ModelSerializer):
|
||||
read_only_fields = ['id']
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Create a customer with auto-generated username"""
|
||||
"""Create a customer with email as username"""
|
||||
import uuid
|
||||
email = validated_data.get('email', '')
|
||||
# Generate username from email or use a UUID if no email
|
||||
# Use email as username, or generate a UUID-based username if no email
|
||||
if email:
|
||||
base_username = email.split('@')[0]
|
||||
username = base_username
|
||||
counter = 1
|
||||
while User.objects.filter(username=username).exists():
|
||||
username = f"{base_username}{counter}"
|
||||
counter += 1
|
||||
validated_data['username'] = email.lower()
|
||||
else:
|
||||
username = f"customer_{uuid.uuid4().hex[:8]}"
|
||||
validated_data['username'] = f"customer_{uuid.uuid4().hex[:8]}"
|
||||
|
||||
validated_data['username'] = username
|
||||
return super().create(validated_data)
|
||||
|
||||
def get_name(self, obj):
|
||||
|
||||
@@ -26,7 +26,7 @@ from django_tenants.utils import schema_context
|
||||
@permission_classes([AllowAny])
|
||||
def login_view(request):
|
||||
"""
|
||||
Login user with username/email and password.
|
||||
Login user with email and password.
|
||||
POST /api/auth/login/
|
||||
|
||||
If MFA is enabled:
|
||||
@@ -36,22 +36,21 @@ def login_view(request):
|
||||
If MFA is not enabled or device is trusted:
|
||||
- Returns access/refresh tokens and user data
|
||||
"""
|
||||
username = request.data.get('username', '').strip()
|
||||
# Accept both 'email' and 'username' fields for backward compatibility
|
||||
email = request.data.get('email', '') or request.data.get('username', '')
|
||||
email = email.strip().lower()
|
||||
password = request.data.get('password', '')
|
||||
|
||||
if not username or not password:
|
||||
if not email or not password:
|
||||
return Response(
|
||||
{'error': 'Username and password are required'},
|
||||
{'error': 'Email and password are required'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Authenticate user (supports username or email)
|
||||
user = authenticate(request, username=username, password=password)
|
||||
|
||||
# If authentication with username failed, try email
|
||||
if user is None:
|
||||
# Look up user by email and authenticate
|
||||
user = None
|
||||
try:
|
||||
user_by_email = User.objects.get(email__iexact=username)
|
||||
user_by_email = User.objects.get(email__iexact=email)
|
||||
user = authenticate(request, username=user_by_email.username, password=password)
|
||||
except User.DoesNotExist:
|
||||
pass
|
||||
@@ -732,20 +731,12 @@ def accept_invitation_view(request, token):
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Create the user
|
||||
username = invitation.email.split('@')[0]
|
||||
# Ensure username is unique
|
||||
base_username = username
|
||||
counter = 1
|
||||
while User.objects.filter(username=username).exists():
|
||||
username = f"{base_username}{counter}"
|
||||
counter += 1
|
||||
|
||||
# Create the user with email as username
|
||||
# Determine sandbox mode from request (set by middleware)
|
||||
is_sandbox = getattr(request, 'sandbox_mode', False)
|
||||
|
||||
user = User.objects.create_user(
|
||||
username=username,
|
||||
username=invitation.email.lower(), # Use email as username
|
||||
email=invitation.email,
|
||||
password=password,
|
||||
first_name=first_name,
|
||||
@@ -1010,7 +1001,7 @@ def signup_view(request):
|
||||
|
||||
# 5. Create User (Owner)
|
||||
user = User.objects.create_user(
|
||||
username=email.split('@')[0], # Fallback username
|
||||
username=email, # Use email as username
|
||||
email=email,
|
||||
password=password,
|
||||
first_name=data.get('first_name', ''),
|
||||
|
||||
@@ -21,9 +21,9 @@ class Command(BaseCommand):
|
||||
)
|
||||
|
||||
test_users = [
|
||||
# Platform users (no tenant)
|
||||
# Platform users (no tenant) - username is set to email
|
||||
{
|
||||
'username': 'superuser',
|
||||
'username': 'superuser@platform.com',
|
||||
'email': 'superuser@platform.com',
|
||||
'password': 'test123',
|
||||
'role': User.Role.SUPERUSER,
|
||||
@@ -32,7 +32,7 @@ class Command(BaseCommand):
|
||||
'tenant': None,
|
||||
},
|
||||
{
|
||||
'username': 'platform_manager',
|
||||
'username': 'manager@platform.com',
|
||||
'email': 'manager@platform.com',
|
||||
'password': 'test123',
|
||||
'role': User.Role.PLATFORM_MANAGER,
|
||||
@@ -41,7 +41,7 @@ class Command(BaseCommand):
|
||||
'tenant': None,
|
||||
},
|
||||
{
|
||||
'username': 'platform_sales',
|
||||
'username': 'sales@platform.com',
|
||||
'email': 'sales@platform.com',
|
||||
'password': 'test123',
|
||||
'role': User.Role.PLATFORM_SALES,
|
||||
@@ -50,7 +50,7 @@ class Command(BaseCommand):
|
||||
'tenant': None,
|
||||
},
|
||||
{
|
||||
'username': 'platform_support',
|
||||
'username': 'support@platform.com',
|
||||
'email': 'support@platform.com',
|
||||
'password': 'test123',
|
||||
'role': User.Role.PLATFORM_SUPPORT,
|
||||
@@ -58,9 +58,9 @@ class Command(BaseCommand):
|
||||
'last_name': 'Agent',
|
||||
'tenant': None,
|
||||
},
|
||||
# Tenant users (with tenant)
|
||||
# Tenant users (with tenant) - username is set to email
|
||||
{
|
||||
'username': 'tenant_owner',
|
||||
'username': 'owner@demo.com',
|
||||
'email': 'owner@demo.com',
|
||||
'password': 'test123',
|
||||
'role': User.Role.TENANT_OWNER,
|
||||
@@ -69,7 +69,7 @@ class Command(BaseCommand):
|
||||
'tenant': demo_tenant,
|
||||
},
|
||||
{
|
||||
'username': 'tenant_manager',
|
||||
'username': 'manager@demo.com',
|
||||
'email': 'manager@demo.com',
|
||||
'password': 'test123',
|
||||
'role': User.Role.TENANT_MANAGER,
|
||||
@@ -78,7 +78,7 @@ class Command(BaseCommand):
|
||||
'tenant': demo_tenant,
|
||||
},
|
||||
{
|
||||
'username': 'tenant_staff',
|
||||
'username': 'staff@demo.com',
|
||||
'email': 'staff@demo.com',
|
||||
'password': 'test123',
|
||||
'role': User.Role.TENANT_STAFF,
|
||||
@@ -87,7 +87,7 @@ class Command(BaseCommand):
|
||||
'tenant': demo_tenant,
|
||||
},
|
||||
{
|
||||
'username': 'customer',
|
||||
'username': 'customer@demo.com',
|
||||
'email': 'customer@demo.com',
|
||||
'password': 'test123',
|
||||
'role': User.Role.CUSTOMER,
|
||||
|
||||
Reference in New Issue
Block a user