feat: increase password max to 256, fix i18n, and force re-login #83

- Raise password maximum length from 32 to 256 characters
- fix profileSchema to accept `t` for proper internationalization
- Invalidate user's WebUI token on password change, requiring re-login
This commit is contained in:
rustmailer
2025-12-30 11:05:07 +08:00
parent c01872284e
commit 62d956c7d6
23 changed files with 66 additions and 54 deletions
+7 -1
View File
@@ -16,7 +16,6 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
use crate::modules::account::migration::{AccountModel, AccountType}; use crate::modules::account::migration::{AccountModel, AccountType};
use crate::modules::cache::imap::mailbox::{Attribute, AttributeEnum, MailBox}; use crate::modules::cache::imap::mailbox::{Attribute, AttributeEnum, MailBox};
use crate::modules::context::executors::MAIL_CONTEXT; use crate::modules::context::executors::MAIL_CONTEXT;
@@ -64,8 +63,15 @@ pub async fn convert_names_to_mailboxes(
for name in names.into_iter() { for name in names.into_iter() {
// Convert the name into a MailBox structure // Convert the name into a MailBox structure
let mailbox_name = name.name().to_string(); let mailbox_name = name.name().to_string();
let mut mailbox: MailBox = name.into(); let mut mailbox: MailBox = name.into();
tracing::debug!(
raw = &mailbox_name,
decoded = &mailbox.name,
"mailbox name comparison"
);
if contains_no_select(&mailbox.attributes) { if contains_no_select(&mailbox.attributes) {
continue; continue;
} }
+6
View File
@@ -444,6 +444,7 @@ impl BichonUser {
pub async fn update(id: u64, request: UserUpdateRequest) -> BichonResult<()> { pub async fn update(id: u64, request: UserUpdateRequest) -> BichonResult<()> {
let _ = &request.validate().await?; let _ = &request.validate().await?;
let password_changed = request.password.is_some();
if DEFAULT_ADMIN_USER_ID == id && request.global_roles.is_some() { if DEFAULT_ADMIN_USER_ID == id && request.global_roles.is_some() {
return Err(raise_error!( return Err(raise_error!(
@@ -537,6 +538,11 @@ impl BichonUser {
}, },
) )
.await?; .await?;
if password_changed {
AccessTokenModel::reset_webui_token(id).await?;
}
Ok(()) Ok(())
} }
+4 -4
View File
@@ -204,9 +204,9 @@ impl UserCreateRequest {
ErrorCode::InvalidParameter ErrorCode::InvalidParameter
)); ));
} }
if password_len > 32 { if password_len > 256 {
return Err(raise_error!( return Err(raise_error!(
"Password cannot exceed 32 characters.".into(), "Password cannot exceed 256 characters.".into(),
ErrorCode::InvalidParameter ErrorCode::InvalidParameter
)); ));
} }
@@ -317,9 +317,9 @@ impl UserUpdateRequest {
if let Some(password) = &self.password { if let Some(password) = &self.password {
let len = password.len(); let len = password.len();
if len < 8 || len > 32 { if len < 8 || len > 256 {
return Err(raise_error!( return Err(raise_error!(
"Password must be 8-32 characters.".into(), "Password must be 8-256 characters.".into(),
ErrorCode::InvalidParameter ErrorCode::InvalidParameter
)); ));
} }
@@ -47,40 +47,40 @@ import AvatarUpload from './avatar-upload'
import useMinimalAccountList from '@/hooks/use-minimal-account-list' import useMinimalAccountList from '@/hooks/use-minimal-account-list'
import { PermissionsDialog } from './permissions-dialog' import { PermissionsDialog } from './permissions-dialog'
const profileSchema = z.object({ const profileSchema = (t: (key: string) => string) => z.object({
username: z username: z
.string({ .string({
required_error: 'settings.profile.validation.username.required', required_error: t('settings.profile.validation.username.required'),
}) })
.min(5, { .min(5, {
message: 'settings.profile.validation.username.min', message: t('settings.profile.validation.username.min'),
}) })
.max(32, { .max(32, {
message: 'settings.profile.validation.username.max', message: t('settings.profile.validation.username.max'),
}), }),
email: z email: z
.string({ .string({
required_error: 'settings.profile.validation.email.required', required_error: t('settings.profile.validation.email.required'),
}) })
.email({ .email({
message: 'settings.profile.validation.email.invalid', message: t('settings.profile.validation.email.invalid'),
}), }),
password: z password: z
.string() .string()
.min(8, { .min(8, {
message: 'settings.profile.validation.password.min', message: t('settings.profile.validation.password.min'),
}) })
.max(32, { .max(256, {
message: 'settings.profile.validation.password.max', message: t('settings.profile.validation.password.max'),
}) })
.or(z.literal('')) .or(z.literal(''))
.optional() .optional()
.transform((v) => (v ? v : undefined)), .transform((v) => (v ? v : undefined)),
}) })
export type ProfileFormValues = z.infer<typeof profileSchema> export type ProfileFormValues = z.infer<ReturnType<typeof profileSchema>>
function fileToBase64(file: File): Promise<string> { function fileToBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -110,7 +110,7 @@ export function UserProfileForm({ user }: UserProfileFormProps) {
useState<number | undefined>(undefined) useState<number | undefined>(undefined)
const form = useForm<ProfileFormValues>({ const form = useForm<ProfileFormValues>({
resolver: zodResolver(profileSchema), resolver: zodResolver(profileSchema(t)),
mode: 'onChange', mode: 'onChange',
defaultValues: { defaultValues: {
username: user.username, username: user.username,
@@ -111,14 +111,14 @@ const createUserSchema = (t: any) => z.object({
password: z.string() password: z.string()
.min(1, t('users.actions.schema.password_required')) .min(1, t('users.actions.schema.password_required'))
.min(8, t('users.actions.schema.password_min')) .min(8, t('users.actions.schema.password_min'))
.max(32, t('users.actions.schema.password_max')), .max(256, t('users.actions.schema.password_max')),
}); });
const updateUserSchema = (t: any) => z.object({ const updateUserSchema = (t: any) => z.object({
...baseUserSchema(t), ...baseUserSchema(t),
password: z.string() password: z.string()
.min(8, t('users.actions.schema.password_min')) .min(8, t('users.actions.schema.password_min'))
.max(32, t('users.actions.schema.password_max')) .max(256, t('users.actions.schema.password_max'))
.or(z.literal('')) .or(z.literal(''))
.optional() .optional()
.transform(v => v || undefined), .transform(v => v || undefined),
+2 -2
View File
@@ -829,7 +829,7 @@
}, },
"password": { "password": {
"min": "يجب أن تكون كلمة المرور 8 أحرف على الأقل", "min": "يجب أن تكون كلمة المرور 8 أحرف على الأقل",
"max": "يجب ألا تزيد كلمة المرور عن 32 حرفًا" "max": "يجب ألا تزيد كلمة المرور عن 256 حرفًا"
} }
} }
} }
@@ -1238,7 +1238,7 @@
"ip_invalid": "تم اكتشاف عنوان IP غير صالح", "ip_invalid": "تم اكتشاف عنوان IP غير صالح",
"password_required": "كلمة المرور مطلوبة", "password_required": "كلمة المرور مطلوبة",
"password_min": "8 أحرف على الأقل", "password_min": "8 أحرف على الأقل",
"password_max": "32 حرفاً كحد أقصى" "password_max": "256 حرفاً كحد أقصى"
}, },
"toast": { "toast": {
"updated": "تم تحديث المستخدم", "updated": "تم تحديث المستخدم",
+2 -2
View File
@@ -829,7 +829,7 @@
}, },
"password": { "password": {
"min": "Adgangskoden skal være mindst 8 tegn", "min": "Adgangskoden skal være mindst 8 tegn",
"max": "Adgangskoden må højst være 32 tegn" "max": "Adgangskoden må højst være 256 tegn"
} }
} }
} }
@@ -1238,7 +1238,7 @@
"ip_invalid": "Ugyldig IP-adresse", "ip_invalid": "Ugyldig IP-adresse",
"password_required": "Adgangskode er påkrævet", "password_required": "Adgangskode er påkrævet",
"password_min": "Mindst 8 tegn", "password_min": "Mindst 8 tegn",
"password_max": "Højst 32 tegn" "password_max": "Højst 256 tegn"
}, },
"toast": { "toast": {
"updated": "Bruger opdateret", "updated": "Bruger opdateret",
+2 -2
View File
@@ -829,7 +829,7 @@
}, },
"password": { "password": {
"min": "Das Passwort muss mindestens 8 Zeichen lang sein", "min": "Das Passwort muss mindestens 8 Zeichen lang sein",
"max": "Das Passwort darf höchstens 32 Zeichen lang sein" "max": "Das Passwort darf höchstens 256 Zeichen lang sein"
} }
} }
} }
@@ -1238,7 +1238,7 @@
"ip_invalid": "Ungültige IP-Adresse erkannt", "ip_invalid": "Ungültige IP-Adresse erkannt",
"password_required": "Passwort ist erforderlich", "password_required": "Passwort ist erforderlich",
"password_min": "Passwort muss mindestens 8 Zeichen lang sein", "password_min": "Passwort muss mindestens 8 Zeichen lang sein",
"password_max": "Passwort darf maximal 32 Zeichen lang sein" "password_max": "Passwort darf maximal 256 Zeichen lang sein"
}, },
"toast": { "toast": {
"updated": "Benutzer aktualisiert", "updated": "Benutzer aktualisiert",
+2 -2
View File
@@ -829,7 +829,7 @@
}, },
"password": { "password": {
"min": "Password must be at least 8 characters", "min": "Password must be at least 8 characters",
"max": "Password must be at most 32 characters" "max": "Password must be at most 256 characters"
} }
} }
} }
@@ -1238,7 +1238,7 @@
"ip_invalid": "Invalid IP address detected", "ip_invalid": "Invalid IP address detected",
"password_required": "Password is required", "password_required": "Password is required",
"password_min": "Password must be at least 8 characters", "password_min": "Password must be at least 8 characters",
"password_max": "Password cannot exceed 32 characters" "password_max": "Password cannot exceed 256 characters"
}, },
"toast": { "toast": {
"updated": "User updated", "updated": "User updated",
+2 -2
View File
@@ -829,7 +829,7 @@
}, },
"password": { "password": {
"min": "La contraseña debe tener al menos 8 caracteres", "min": "La contraseña debe tener al menos 8 caracteres",
"max": "La contraseña debe tener como máximo 32 caracteres" "max": "La contraseña debe tener como máximo 256 caracteres"
} }
} }
} }
@@ -1238,7 +1238,7 @@
"ip_invalid": "Dirección IP inválida", "ip_invalid": "Dirección IP inválida",
"password_required": "La contraseña es obligatoria", "password_required": "La contraseña es obligatoria",
"password_min": "Mínimo 8 caracteres", "password_min": "Mínimo 8 caracteres",
"password_max": "Máximo 32 caracteres" "password_max": "Máximo 256 caracteres"
}, },
"toast": { "toast": {
"updated": "Usuario actualizado", "updated": "Usuario actualizado",
+2 -2
View File
@@ -829,7 +829,7 @@
}, },
"password": { "password": {
"min": "Salasanan on oltava vähintään 8 merkkiä", "min": "Salasanan on oltava vähintään 8 merkkiä",
"max": "Salasana voi olla enintään 32 merkkiä" "max": "Salasana voi olla enintään 256 merkkiä"
} }
} }
} }
@@ -1238,7 +1238,7 @@
"ip_invalid": "Virheellinen IP-osoite", "ip_invalid": "Virheellinen IP-osoite",
"password_required": "Salasana on pakollinen", "password_required": "Salasana on pakollinen",
"password_min": "Vähintään 8 merkkiä", "password_min": "Vähintään 8 merkkiä",
"password_max": "Enintään 32 merkkiä" "password_max": "Enintään 256 merkkiä"
}, },
"toast": { "toast": {
"updated": "Käyttäjä päivitetty", "updated": "Käyttäjä päivitetty",
+2 -2
View File
@@ -829,7 +829,7 @@
}, },
"password": { "password": {
"min": "Le mot de passe doit contenir au moins 8 caractères", "min": "Le mot de passe doit contenir au moins 8 caractères",
"max": "Le mot de passe doit contenir au maximum 32 caractères" "max": "Le mot de passe doit contenir au maximum 256 caractères"
} }
} }
} }
@@ -1238,7 +1238,7 @@
"ip_invalid": "Adresse IP invalide détectée", "ip_invalid": "Adresse IP invalide détectée",
"password_required": "Le mot de passe est requis", "password_required": "Le mot de passe est requis",
"password_min": "Le mot de passe doit contenir au moins 8 caractères", "password_min": "Le mot de passe doit contenir au moins 8 caractères",
"password_max": "Le mot de passe ne peut pas dépasser 32 caractères" "password_max": "Le mot de passe ne peut pas dépasser 256 caractères"
}, },
"toast": { "toast": {
"updated": "Utilisateur mis à jour", "updated": "Utilisateur mis à jour",
+2 -2
View File
@@ -829,7 +829,7 @@
}, },
"password": { "password": {
"min": "La password deve contenere almeno 8 caratteri", "min": "La password deve contenere almeno 8 caratteri",
"max": "La password deve contenere al massimo 32 caratteri" "max": "La password deve contenere al massimo 256 caratteri"
} }
} }
} }
@@ -1238,7 +1238,7 @@
"ip_invalid": "Indirizzo IP non valido", "ip_invalid": "Indirizzo IP non valido",
"password_required": "La password è obbligatoria", "password_required": "La password è obbligatoria",
"password_min": "Minimo 8 caratteri", "password_min": "Minimo 8 caratteri",
"password_max": "Massimo 32 caratteri" "password_max": "Massimo 256 caratteri"
}, },
"toast": { "toast": {
"updated": "Utente aggiornato", "updated": "Utente aggiornato",
+2 -2
View File
@@ -829,7 +829,7 @@
}, },
"password": { "password": {
"min": "パスワードは8文字以上である必要があります", "min": "パスワードは8文字以上である必要があります",
"max": "パスワードは32文字以内である必要があります" "max": "パスワードは256文字以内である必要があります"
} }
} }
} }
@@ -1238,7 +1238,7 @@
"ip_invalid": "無効な IP アドレスが検出されました", "ip_invalid": "無効な IP アドレスが検出されました",
"password_required": "パスワードは必須です", "password_required": "パスワードは必須です",
"password_min": "パスワードは 8 文字以上である必要があります", "password_min": "パスワードは 8 文字以上である必要があります",
"password_max": "パスワードは 32 文字以内である必要があります" "password_max": "パスワードは 256 文字以内である必要があります"
}, },
"toast": { "toast": {
"updated": "ユーザーを更新しました", "updated": "ユーザーを更新しました",
+2 -2
View File
@@ -829,7 +829,7 @@
}, },
"password": { "password": {
"min": "비밀번호는 최소 8자 이상이어야 합니다", "min": "비밀번호는 최소 8자 이상이어야 합니다",
"max": "비밀번호는 최대 32자까지 가능합니다" "max": "비밀번호는 최대 256자까지 가능합니다"
} }
} }
} }
@@ -1238,7 +1238,7 @@
"ip_invalid": "유효하지 않은 IP 주소가 감지되었습니다", "ip_invalid": "유효하지 않은 IP 주소가 감지되었습니다",
"password_required": "비밀번호는 필수입니다", "password_required": "비밀번호는 필수입니다",
"password_min": "비밀번호는 8자 이상이어야 합니다", "password_min": "비밀번호는 8자 이상이어야 합니다",
"password_max": "비밀번호는 32자를 초과할 수 없습니다" "password_max": "비밀번호는 256자를 초과할 수 없습니다"
}, },
"toast": { "toast": {
"updated": "사용자 업데이트됨", "updated": "사용자 업데이트됨",
+2 -2
View File
@@ -829,7 +829,7 @@
}, },
"password": { "password": {
"min": "Wachtwoord moet minimaal 8 tekens bevatten", "min": "Wachtwoord moet minimaal 8 tekens bevatten",
"max": "Wachtwoord mag maximaal 32 tekens bevatten" "max": "Wachtwoord mag maximaal 256 tekens bevatten"
} }
} }
} }
@@ -1238,7 +1238,7 @@
"ip_invalid": "Ongeldig IP-adres gedetecteerd", "ip_invalid": "Ongeldig IP-adres gedetecteerd",
"password_required": "Wachtwoord is verplicht", "password_required": "Wachtwoord is verplicht",
"password_min": "Minimaal 8 tekens", "password_min": "Minimaal 8 tekens",
"password_max": "Maximaal 32 tekens" "password_max": "Maximaal 256 tekens"
}, },
"toast": { "toast": {
"updated": "Gebruiker bijgewerkt", "updated": "Gebruiker bijgewerkt",
+2 -2
View File
@@ -829,7 +829,7 @@
}, },
"password": { "password": {
"min": "Passordet må være minst 8 tegn", "min": "Passordet må være minst 8 tegn",
"max": "Passordet kan være maks 32 tegn" "max": "Passordet kan være maks 256 tegn"
} }
} }
} }
@@ -1238,7 +1238,7 @@
"ip_invalid": "Ugyldig IP-adresse", "ip_invalid": "Ugyldig IP-adresse",
"password_required": "Passord er påkrevd", "password_required": "Passord er påkrevd",
"password_min": "Minst 8 tegn", "password_min": "Minst 8 tegn",
"password_max": "Maks 32 tegn" "password_max": "Maks 256 tegn"
}, },
"toast": { "toast": {
"updated": "Bruker oppdatert", "updated": "Bruker oppdatert",
+2 -2
View File
@@ -829,7 +829,7 @@
}, },
"password": { "password": {
"min": "Hasło musi mieć co najmniej 8 znaków", "min": "Hasło musi mieć co najmniej 8 znaków",
"max": "Hasło może mieć maksymalnie 32 znaki" "max": "Hasło może mieć maksymalnie 256 znaki"
} }
} }
} }
@@ -1238,7 +1238,7 @@
"ip_invalid": "Wykryto niepoprawny adres IP", "ip_invalid": "Wykryto niepoprawny adres IP",
"password_required": "Hasło jest wymagane", "password_required": "Hasło jest wymagane",
"password_min": "Min. 8 znaków", "password_min": "Min. 8 znaków",
"password_max": "Maks. 32 znaki" "password_max": "Maks. 256 znaki"
}, },
"toast": { "toast": {
"updated": "Użytkownik zaktualizowany", "updated": "Użytkownik zaktualizowany",
+2 -2
View File
@@ -829,7 +829,7 @@
}, },
"password": { "password": {
"min": "A senha deve ter pelo menos 8 caracteres", "min": "A senha deve ter pelo menos 8 caracteres",
"max": "A senha deve ter no máximo 32 caracteres" "max": "A senha deve ter no máximo 256 caracteres"
} }
} }
} }
@@ -1238,7 +1238,7 @@
"ip_invalid": "IP inválido detectado", "ip_invalid": "IP inválido detectado",
"password_required": "Senha é obrigatória", "password_required": "Senha é obrigatória",
"password_min": "Mínimo 8 caracteres", "password_min": "Mínimo 8 caracteres",
"password_max": "Máximo 32 caracteres" "password_max": "Máximo 256 caracteres"
}, },
"toast": { "toast": {
"updated": "Usuário atualizado", "updated": "Usuário atualizado",
+2 -2
View File
@@ -829,7 +829,7 @@
}, },
"password": { "password": {
"min": "Пароль должен содержать не менее 8 символов", "min": "Пароль должен содержать не менее 8 символов",
"max": "Пароль должен содержать не более 32 символов" "max": "Пароль должен содержать не более 256 символов"
} }
} }
} }
@@ -1238,7 +1238,7 @@
"ip_invalid": "Неверный IP-адрес", "ip_invalid": "Неверный IP-адрес",
"password_required": "Пароль обязателен", "password_required": "Пароль обязателен",
"password_min": "Минимум 8 символов", "password_min": "Минимум 8 символов",
"password_max": "Максимум 32 символа" "password_max": "Максимум 256 символа"
}, },
"toast": { "toast": {
"updated": "Пользователь обновлен", "updated": "Пользователь обновлен",
+2 -2
View File
@@ -829,7 +829,7 @@
}, },
"password": { "password": {
"min": "Lösenordet måste vara minst 8 tecken", "min": "Lösenordet måste vara minst 8 tecken",
"max": "Lösenordet får vara högst 32 tecken" "max": "Lösenordet får vara högst 256 tecken"
} }
} }
} }
@@ -1238,7 +1238,7 @@
"ip_invalid": "Ogiltig IP-adress upptäckt", "ip_invalid": "Ogiltig IP-adress upptäckt",
"password_required": "Lösenord krävs", "password_required": "Lösenord krävs",
"password_min": "Lösenordet måste vara minst 8 tecken", "password_min": "Lösenordet måste vara minst 8 tecken",
"password_max": "Lösenordet får vara max 32 tecken" "password_max": "Lösenordet får vara max 256 tecken"
}, },
"toast": { "toast": {
"updated": "Användaren uppdaterad", "updated": "Användaren uppdaterad",
+2 -2
View File
@@ -822,7 +822,7 @@
}, },
"password": { "password": {
"min": "密碼至少需要 8 個字元", "min": "密碼至少需要 8 個字元",
"max": "密碼最多 32 個字元" "max": "密碼最多 256 個字元"
} }
} }
} }
@@ -1231,7 +1231,7 @@
"ip_invalid": "發現無效的 IP 地址", "ip_invalid": "發現無效的 IP 地址",
"password_required": "密碼不能為空", "password_required": "密碼不能為空",
"password_min": "密碼長度至少為 8 個字元", "password_min": "密碼長度至少為 8 個字元",
"password_max": "密碼長度不能超過 32 個字元" "password_max": "密碼長度不能超過 256 個字元"
}, },
"toast": { "toast": {
"updated": "使用者已更新", "updated": "使用者已更新",
+2 -2
View File
@@ -829,7 +829,7 @@
}, },
"password": { "password": {
"min": "密码长度至少为 8 个字符", "min": "密码长度至少为 8 个字符",
"max": "密码长度不能超过 32 个字符" "max": "密码长度不能超过 256 个字符"
} }
} }
} }
@@ -1238,7 +1238,7 @@
"ip_invalid": "发现无效的 IP 地址", "ip_invalid": "发现无效的 IP 地址",
"password_required": "密码不能为空", "password_required": "密码不能为空",
"password_min": "密码长度至少为 8 个字符", "password_min": "密码长度至少为 8 个字符",
"password_max": "密码长度不能超过 32 个字符" "password_max": "密码长度不能超过 256 个字符"
}, },
"toast": { "toast": {
"updated": "用户已更新", "updated": "用户已更新",