mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-08 23:21:58 +02:00
Fix Advanced Authentication cleanups: external URL, forgot password dialog, edit user modal, info box, i18n
Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com>
This commit is contained in:
co-authored by
cadtoolbox
parent
a20fceed2b
commit
b024d02a04
@@ -41,6 +41,7 @@ from backend.app.services.email_service import (
|
||||
save_smtp_settings,
|
||||
send_email,
|
||||
)
|
||||
from backend.app.api.routes.settings import get_setting
|
||||
|
||||
|
||||
def _user_to_response(user: User) -> UserResponse:
|
||||
@@ -556,8 +557,13 @@ async def forgot_password(request: ForgotPasswordRequest, db: AsyncSession = Dep
|
||||
user.password_hash = get_password_hash(new_password)
|
||||
await db.commit()
|
||||
|
||||
# Get login URL from environment or use default
|
||||
login_url = os.environ.get("APP_URL", "http://localhost:5173") + "/login"
|
||||
# Use external_url from settings if available, otherwise fall back to APP_URL env var
|
||||
external_url = await get_setting(db, "external_url")
|
||||
if external_url:
|
||||
external_url = external_url.rstrip("/")
|
||||
else:
|
||||
external_url = os.environ.get("APP_URL", "http://localhost:5173")
|
||||
login_url = external_url + "/login"
|
||||
|
||||
# Send password reset email
|
||||
subject, text_body, html_body = create_password_reset_email(user.username, new_password, login_url)
|
||||
@@ -632,8 +638,13 @@ async def reset_user_password(
|
||||
user.password_hash = get_password_hash(new_password)
|
||||
await db.commit()
|
||||
|
||||
# Get login URL from environment or use default
|
||||
login_url = os.environ.get("APP_URL", "http://localhost:5173") + "/login"
|
||||
# Use external_url from settings if available, otherwise fall back to APP_URL env var
|
||||
external_url = await get_setting(db, "external_url")
|
||||
if external_url:
|
||||
external_url = external_url.rstrip("/")
|
||||
else:
|
||||
external_url = os.environ.get("APP_URL", "http://localhost:5173")
|
||||
login_url = external_url + "/login"
|
||||
|
||||
# Send password reset email
|
||||
subject, text_body, html_body = create_password_reset_email(user.username, new_password, login_url)
|
||||
|
||||
@@ -24,6 +24,7 @@ from backend.app.services.email_service import (
|
||||
get_smtp_settings,
|
||||
send_email,
|
||||
)
|
||||
from backend.app.api.routes.settings import get_setting
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["users"])
|
||||
|
||||
@@ -152,7 +153,13 @@ async def create_user(
|
||||
try:
|
||||
smtp_settings = await get_smtp_settings(db)
|
||||
if smtp_settings:
|
||||
login_url = os.environ.get("APP_URL", "http://localhost:5173") + "/login"
|
||||
# Use external_url from settings if available, otherwise fall back to APP_URL env var
|
||||
external_url = await get_setting(db, "external_url")
|
||||
if external_url:
|
||||
external_url = external_url.rstrip("/")
|
||||
else:
|
||||
external_url = os.environ.get("APP_URL", "http://localhost:5173")
|
||||
login_url = external_url + "/login"
|
||||
subject, text_body, html_body = create_welcome_email(new_user.username, password, login_url)
|
||||
send_email(smtp_settings, new_user.email, subject, text_body, html_body)
|
||||
logger.info(f"Welcome email sent to {new_user.email}")
|
||||
|
||||
@@ -84,7 +84,7 @@ export function CreateUserAdvancedAuthModal({
|
||||
{/* Username Field */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white mb-2">
|
||||
{t('users.form.username')}
|
||||
{t('users.form.username')} <span className="text-red-400">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -93,6 +93,7 @@ export function CreateUserAdvancedAuthModal({
|
||||
className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
|
||||
placeholder={t('users.form.usernamePlaceholder')}
|
||||
autoComplete="username"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1021,6 +1021,7 @@ export default {
|
||||
saving: 'Wird gespeichert...',
|
||||
advancedAuth: 'Erweiterte Authentifizierung',
|
||||
advancedAuthEnabled: 'Erweiterte Authentifizierung ist aktiviert',
|
||||
advancedAuthEnabledDesc: 'E-Mail-basierte Benutzerverwaltungsfunktionen sind aktiv. Neue Benutzer erhalten automatisch generierte Passwörter per E-Mail und können ihr Passwort über die Passwort vergessen Funktion zurücksetzen.',
|
||||
advancedAuthDisabled: 'Erweiterte Authentifizierung ist deaktiviert',
|
||||
advancedAuthDisabledDesc: 'Aktivieren Sie die erweiterte Authentifizierung, um E-Mail-basierte Funktionen für die Benutzerverwaltung zu aktivieren.',
|
||||
enable: 'Aktivieren',
|
||||
@@ -1623,6 +1624,7 @@ export default {
|
||||
creating: 'Erstellen...',
|
||||
saving: 'Speichern...',
|
||||
saveChanges: 'Änderungen speichern',
|
||||
advancedAuthSubtitle: 'mit erweiterter Authentifizierung',
|
||||
},
|
||||
form: {
|
||||
username: 'Benutzername',
|
||||
@@ -1639,6 +1641,7 @@ export default {
|
||||
groups: 'Gruppen',
|
||||
optional: 'optional',
|
||||
autoGeneratedPassword: 'Ein sicheres Passwort wird automatisch generiert und per E-Mail an den Benutzer gesendet.',
|
||||
passwordManagedByAdvancedAuth: 'Das Passwort wird durch erweiterte Authentifizierung verwaltet. Verwenden Sie "Passwort zurücksetzen", um ein neues Passwort per E-Mail an den Benutzer zu senden.',
|
||||
},
|
||||
deleteModal: {
|
||||
title: 'Benutzer löschen',
|
||||
|
||||
@@ -1021,6 +1021,7 @@ export default {
|
||||
saving: 'Saving...',
|
||||
advancedAuth: 'Advanced Authentication',
|
||||
advancedAuthEnabled: 'Advanced Authentication is enabled',
|
||||
advancedAuthEnabledDesc: 'Email-based user management features are active. New users will receive auto-generated passwords via email, and users can reset their passwords through the forgot password feature.',
|
||||
advancedAuthDisabled: 'Advanced Authentication is disabled',
|
||||
advancedAuthDisabledDesc: 'Enable advanced authentication to activate email-based features for user management.',
|
||||
enable: 'Enable',
|
||||
@@ -1623,6 +1624,7 @@ export default {
|
||||
creating: 'Creating...',
|
||||
saving: 'Saving...',
|
||||
saveChanges: 'Save Changes',
|
||||
advancedAuthSubtitle: 'with Advanced Authentication',
|
||||
},
|
||||
form: {
|
||||
username: 'Username',
|
||||
@@ -1639,6 +1641,7 @@ export default {
|
||||
groups: 'Groups',
|
||||
optional: 'optional',
|
||||
autoGeneratedPassword: 'A secure password will be automatically generated and emailed to the user.',
|
||||
passwordManagedByAdvancedAuth: 'Password is managed by Advanced Authentication. Use "Reset Password" to send a new password to the user via email.',
|
||||
},
|
||||
deleteModal: {
|
||||
title: 'Delete User',
|
||||
|
||||
@@ -1173,6 +1173,7 @@ export default {
|
||||
saving: '保存中...',
|
||||
advancedAuth: '高度な認証',
|
||||
advancedAuthEnabled: '高度な認証が有効です',
|
||||
advancedAuthEnabledDesc: 'メールベースのユーザー管理機能が有効になっています。新規ユーザーには自動生成されたパスワードがメールで送信され、ユーザーはパスワード忘れ機能でパスワードをリセットできます。',
|
||||
advancedAuthDisabled: '高度な認証が無効です',
|
||||
advancedAuthDisabledDesc: '高度な認証を有効にして、ユーザー管理のメールベース機能を有効化してください。',
|
||||
enable: '有効にする',
|
||||
@@ -2172,6 +2173,7 @@ export default {
|
||||
groups: 'グループ',
|
||||
optional: 'オプション',
|
||||
autoGeneratedPassword: '安全なパスワードが自動的に生成され、ユーザーにメールで送信されます。',
|
||||
passwordManagedByAdvancedAuth: 'パスワードは高度な認証によって管理されています。「パスワードのリセット」を使用して、メールで新しいパスワードをユーザーに送信してください。',
|
||||
},
|
||||
modal: {
|
||||
createUser: 'ユーザーを作成',
|
||||
@@ -2180,6 +2182,7 @@ export default {
|
||||
creating: '作成中...',
|
||||
saving: '保存中...',
|
||||
saveChanges: '変更を保存',
|
||||
advancedAuthSubtitle: '高度な認証を使用',
|
||||
},
|
||||
deleteModal: {
|
||||
title: 'ユーザーを削除',
|
||||
|
||||
@@ -7,6 +7,8 @@ import { useToast } from '../contexts/ToastContext';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { X, Mail } from 'lucide-react';
|
||||
import { api } from '../api/client';
|
||||
import { Card, CardHeader, CardContent } from '../components/Card';
|
||||
import { Button } from '../components/Button';
|
||||
|
||||
export function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
@@ -151,97 +153,103 @@ export function LoginPage() {
|
||||
{/* Forgot Password Modal */}
|
||||
{showForgotPassword && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4"
|
||||
className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4"
|
||||
onClick={() => setShowForgotPassword(false)}
|
||||
>
|
||||
<div
|
||||
className="w-full max-w-md bg-bambu-card rounded-xl border border-bambu-dark-tertiary shadow-lg p-6"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
<Card
|
||||
className="w-full max-w-md"
|
||||
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Mail className="w-5 h-5 text-bambu-green" />
|
||||
<h2 className="text-lg font-semibold text-white">{t('login.forgotPasswordTitle')}</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowForgotPassword(false);
|
||||
setForgotEmail('');
|
||||
}}
|
||||
className="p-1 rounded-lg hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{advancedAuthStatus?.advanced_auth_enabled ? (
|
||||
<form onSubmit={handleForgotPassword} className="space-y-4">
|
||||
<p className="text-bambu-gray text-sm">
|
||||
{t('login.forgotPasswordEmailMessage') || 'Enter your email address and we\'ll send you a new password.'}
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<label htmlFor="forgot-email" className="block text-sm font-medium text-white mb-2">
|
||||
{t('login.emailAddress') || 'Email Address'}
|
||||
</label>
|
||||
<input
|
||||
id="forgot-email"
|
||||
type="email"
|
||||
required
|
||||
value={forgotEmail}
|
||||
onChange={(e) => setForgotEmail(e.target.value)}
|
||||
className="block w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
|
||||
placeholder={t('login.emailPlaceholder') || 'your.email@example.com'}
|
||||
/>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Mail className="w-5 h-5 text-bambu-green" />
|
||||
<h2 className="text-lg font-semibold text-white">{t('login.forgotPasswordTitle')}</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowForgotPassword(false);
|
||||
setForgotEmail('');
|
||||
}}
|
||||
className="flex-1 py-2 px-4 bg-bambu-dark-tertiary hover:bg-bambu-dark text-white rounded-lg transition-colors"
|
||||
>
|
||||
{t('login.cancel') || 'Cancel'}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={forgotPasswordMutation.isPending}
|
||||
className="flex-1 py-2 px-4 bg-bambu-green hover:bg-bambu-green-light text-white rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{forgotPasswordMutation.isPending
|
||||
? (t('login.sending') || 'Sending...')
|
||||
: (t('login.sendResetEmail') || 'Send Reset Email')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<p className="text-bambu-gray">
|
||||
{t('login.forgotPasswordMessage')}
|
||||
</p>
|
||||
|
||||
<div className="bg-bambu-dark rounded-lg p-4 space-y-2">
|
||||
<p className="text-sm text-white font-medium">{t('login.howToReset')}</p>
|
||||
<ol className="text-sm text-bambu-gray space-y-1 list-decimal list-inside">
|
||||
<li>{t('login.resetStep1')}</li>
|
||||
<li>{t('login.resetStep2')}</li>
|
||||
<li>{t('login.resetStep3')}</li>
|
||||
<li>{t('login.resetStep4')}</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setShowForgotPassword(false)}
|
||||
className="w-full py-2 px-4 bg-bambu-dark-tertiary hover:bg-bambu-dark text-white rounded-lg transition-colors"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setShowForgotPassword(false);
|
||||
setForgotEmail('');
|
||||
}}
|
||||
>
|
||||
{t('login.gotIt')}
|
||||
</button>
|
||||
<X className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{advancedAuthStatus?.advanced_auth_enabled ? (
|
||||
<form onSubmit={handleForgotPassword} className="space-y-4">
|
||||
<p className="text-bambu-gray text-sm">
|
||||
{t('login.forgotPasswordEmailMessage') || 'Enter your email address and we\'ll send you a new password.'}
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<label htmlFor="forgot-email" className="block text-sm font-medium text-white mb-2">
|
||||
{t('login.emailAddress') || 'Email Address'}
|
||||
</label>
|
||||
<input
|
||||
id="forgot-email"
|
||||
type="email"
|
||||
required
|
||||
value={forgotEmail}
|
||||
onChange={(e) => setForgotEmail(e.target.value)}
|
||||
className="block w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
|
||||
placeholder={t('login.emailPlaceholder') || 'your.email@example.com'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
setShowForgotPassword(false);
|
||||
setForgotEmail('');
|
||||
}}
|
||||
>
|
||||
{t('login.cancel') || 'Cancel'}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="flex-1"
|
||||
disabled={forgotPasswordMutation.isPending}
|
||||
>
|
||||
{forgotPasswordMutation.isPending
|
||||
? (t('login.sending') || 'Sending...')
|
||||
: (t('login.sendResetEmail') || 'Send Reset Email')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<p className="text-bambu-gray">
|
||||
{t('login.forgotPasswordMessage')}
|
||||
</p>
|
||||
|
||||
<div className="bg-bambu-dark rounded-lg p-4 space-y-2">
|
||||
<p className="text-sm text-white font-medium">{t('login.howToReset')}</p>
|
||||
<ol className="text-sm text-bambu-gray space-y-1 list-decimal list-inside">
|
||||
<li>{t('login.resetStep1')}</li>
|
||||
<li>{t('login.resetStep2')}</li>
|
||||
<li>{t('login.resetStep3')}</li>
|
||||
<li>{t('login.resetStep4')}</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
onClick={() => setShowForgotPassword(false)}
|
||||
>
|
||||
{t('login.gotIt')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3426,6 +3426,25 @@ export function SettingsPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Advanced Authentication Notice Box */}
|
||||
{advancedAuthStatus?.advanced_auth_enabled && (
|
||||
<Card className="border-blue-500/30 bg-blue-500/5">
|
||||
<CardContent className="py-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-full flex items-center justify-center bg-blue-500/20 flex-shrink-0">
|
||||
<Mail className="w-5 h-5 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-white font-medium">{t('settings.email.advancedAuthEnabled') || 'Advanced Authentication is enabled'}</h3>
|
||||
<p className="text-sm text-bambu-gray mt-1">
|
||||
{t('settings.email.advancedAuthEnabledDesc') || 'Email-based user management features are active. New users will receive auto-generated passwords via email, and users can reset their passwords through the forgot password feature.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{authEnabled && (
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||||
{/* Left Column: Current User + User List */}
|
||||
@@ -3860,8 +3879,11 @@ export function SettingsPage() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{/* Username Field */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white mb-2">{t('settings.username')}</label>
|
||||
<label className="block text-sm font-medium text-white mb-2">
|
||||
{t('settings.username')} {advancedAuthStatus?.advanced_auth_enabled && <span className="text-red-400">*</span>}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={userFormData.username}
|
||||
@@ -3871,43 +3893,75 @@ export function SettingsPage() {
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Email Field */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white mb-2">
|
||||
Password <span className="text-bambu-gray font-normal">(leave blank to keep current)</span>
|
||||
{t('users.form.email') || 'Email'} {advancedAuthStatus?.advanced_auth_enabled ? <span className="text-red-400">*</span> : <span className="text-bambu-gray font-normal">({t('users.form.optional') || 'optional'})</span>}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={userFormData.password}
|
||||
onChange={(e) => setUserFormData({ ...userFormData, password: e.target.value, confirmPassword: '' })}
|
||||
type="email"
|
||||
value={userFormData.email}
|
||||
onChange={(e) => setUserFormData({ ...userFormData, email: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
|
||||
placeholder={t('settings.enterNewPassword')}
|
||||
autoComplete="new-password"
|
||||
minLength={6}
|
||||
placeholder={t('users.form.emailPlaceholder') || 'user@example.com'}
|
||||
required={advancedAuthStatus?.advanced_auth_enabled}
|
||||
/>
|
||||
</div>
|
||||
{userFormData.password && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white mb-2">{t('settings.confirmPassword')}</label>
|
||||
<input
|
||||
type="password"
|
||||
value={userFormData.confirmPassword}
|
||||
onChange={(e) => setUserFormData({ ...userFormData, confirmPassword: e.target.value })}
|
||||
className={`w-full px-4 py-3 bg-bambu-dark-secondary border rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors ${
|
||||
userFormData.confirmPassword && userFormData.password !== userFormData.confirmPassword
|
||||
? 'border-red-500'
|
||||
: 'border-bambu-dark-tertiary'
|
||||
}`}
|
||||
placeholder={t('settings.confirmNewPassword')}
|
||||
autoComplete="new-password"
|
||||
minLength={6}
|
||||
/>
|
||||
{userFormData.confirmPassword && userFormData.password !== userFormData.confirmPassword && (
|
||||
<p className="text-red-400 text-xs mt-1">{t('settings.passwordsDoNotMatch')}</p>
|
||||
|
||||
{/* Password Fields - only show when Advanced Auth is disabled */}
|
||||
{!advancedAuthStatus?.advanced_auth_enabled && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white mb-2">
|
||||
{t('users.form.password') || 'Password'} <span className="text-bambu-gray font-normal">({t('users.form.leaveBlankToKeep') || 'leave blank to keep current'})</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={userFormData.password}
|
||||
onChange={(e) => setUserFormData({ ...userFormData, password: e.target.value, confirmPassword: '' })}
|
||||
className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors"
|
||||
placeholder={t('settings.enterNewPassword')}
|
||||
autoComplete="new-password"
|
||||
minLength={6}
|
||||
/>
|
||||
</div>
|
||||
{userFormData.password && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white mb-2">{t('settings.confirmPassword')}</label>
|
||||
<input
|
||||
type="password"
|
||||
value={userFormData.confirmPassword}
|
||||
onChange={(e) => setUserFormData({ ...userFormData, confirmPassword: e.target.value })}
|
||||
className={`w-full px-4 py-3 bg-bambu-dark-secondary border rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors ${
|
||||
userFormData.confirmPassword && userFormData.password !== userFormData.confirmPassword
|
||||
? 'border-red-500'
|
||||
: 'border-bambu-dark-tertiary'
|
||||
}`}
|
||||
placeholder={t('settings.confirmNewPassword')}
|
||||
autoComplete="new-password"
|
||||
minLength={6}
|
||||
/>
|
||||
{userFormData.confirmPassword && userFormData.password !== userFormData.confirmPassword && (
|
||||
<p className="text-red-400 text-xs mt-1">{t('settings.passwordsDoNotMatch')}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Info box about auto-generated password when Advanced Auth is enabled */}
|
||||
{advancedAuthStatus?.advanced_auth_enabled && (
|
||||
<div className="bg-bambu-dark-secondary/50 border border-bambu-green/20 rounded-lg p-3">
|
||||
<p className="text-sm text-bambu-gray">
|
||||
{t('users.form.passwordManagedByAdvancedAuth') || 'Password is managed by Advanced Authentication. Use "Reset Password" to send a new password to the user via email.'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Groups Field */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-white mb-2">Groups</label>
|
||||
<label className="block text-sm font-medium text-white mb-2">{t('users.form.groups') || 'Groups'}</label>
|
||||
<div className="space-y-2 max-h-40 overflow-y-auto p-2 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg">
|
||||
{groupsData.map(group => (
|
||||
<label
|
||||
@@ -3922,7 +3976,7 @@ export function SettingsPage() {
|
||||
/>
|
||||
<span className="text-sm text-white">{group.name}</span>
|
||||
{group.is_system && (
|
||||
<span className="text-xs text-yellow-400">(System)</span>
|
||||
<span className="text-xs text-yellow-400">({t('users.system') || 'System'})</span>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
@@ -3938,21 +3992,26 @@ export function SettingsPage() {
|
||||
setUserFormData({ username: '', password: '', email: '', confirmPassword: '', role: 'user', group_ids: [] });
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
{t('users.modal.cancel') || 'Cancel'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleUpdateUser(editingUserId)}
|
||||
disabled={updateUserMutation.isPending || !userFormData.username || !!(userFormData.password && (userFormData.password !== userFormData.confirmPassword || userFormData.password.length < 6))}
|
||||
disabled={
|
||||
updateUserMutation.isPending ||
|
||||
!userFormData.username ||
|
||||
(advancedAuthStatus?.advanced_auth_enabled && !userFormData.email) ||
|
||||
Boolean(!advancedAuthStatus?.advanced_auth_enabled && userFormData.password && (userFormData.password !== userFormData.confirmPassword || userFormData.password.length < 6))
|
||||
}
|
||||
>
|
||||
{updateUserMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Saving...
|
||||
{t('users.modal.saving') || 'Saving...'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="w-4 h-4" />
|
||||
Save Changes
|
||||
{t('users.modal.saveChanges') || 'Save Changes'}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -23,8 +23,8 @@
|
||||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-C0uTHy_F.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DbCAeYz5.css">
|
||||
<script type="module" crossorigin src="/assets/index-NgO9xbrN.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-RuMjImbZ.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user