Почтовый кабинет: регистрация, папки, черновики, вложения, грид @jambotron/aria-grid, интерфейс по synthwave-шаблону

Бэкенд: регистрация со входом сразу и /api/config, профиль и смена пароля, свои папки, PATCH письма
(прочитано, флажок, перенос; Спам ставит отметку), корзина и очистка, черновики, несколько адресатов и копия,
вложения черновика уходят с письмом, вложения и заголовки To/Cc входящих сохраняются, письма заблокированных —
в Спам. Исправлено: загрузка файла (415), колонка content_id (500 на любых вложениях), SPA-маршрут с точкой.
Фронтенд переписан: все таблицы — грид, оформление — шаблон synthwave-design-showcase.html.

α
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 16:41:29 +01:00
parent f605fbfd85
commit 29b1beabe0
70 changed files with 3468 additions and 1737 deletions
@@ -2,7 +2,6 @@ package com.mailserver.service;
import com.mailserver.domain.Folder;
import com.mailserver.domain.User;
import com.mailserver.dto.FolderResponse;
import com.mailserver.repository.FolderRepository;
import com.mailserver.repository.UserRepository;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@@ -35,18 +34,18 @@ public class UserService {
User user = new User();
user.setEmail(email);
user.setPassword(passwordEncoder.encode(password));
user.setDisplayName(displayName);
user.setDisplayName(displayName == null || displayName.isBlank() ? email.substring(0, email.indexOf('@')) : displayName.trim());
user.setCreatedAt(Instant.now());
return userRepository.save(user);
})
.flatMap(user -> {
// Создаём стандартные папки
String[] defaultFolders = {"INBOX", "Sent", "Drafts", "Trash", "Spam"};
return Flux.fromArray(defaultFolders)
.flatMap(name -> {
return Flux.fromIterable(MailService.SYSTEM_FOLDERS)
.concatMap(name -> {
Folder folder = new Folder();
folder.setUserId(user.getId());
folder.setName(name);
folder.setSystem(true);
folder.setCreatedAt(Instant.now());
return folderRepository.save(folder);
})
@@ -66,15 +65,17 @@ public class UserService {
return passwordEncoder.matches(rawPassword, user.getPassword());
}
public Flux<FolderResponse> getFolders(Long userId) {
return folderRepository.findByUserId(userId)
.map(f -> {
FolderResponse resp = new FolderResponse();
resp.setId(f.getId());
resp.setName(f.getName());
resp.setMessageCount(f.getMessageCount());
resp.setUnreadCount(f.getUnreadCount());
return resp;
});
/** New password, only with the current one. */
public Mono<Void> changePassword(User user, String currentPassword, String newPassword) {
if (!checkPassword(user, currentPassword)) {
return Mono.error(MailService.badRequest("Current password is wrong"));
}
user.setPassword(passwordEncoder.encode(newPassword));
return userRepository.save(user).then();
}
public Mono<User> updateDisplayName(User user, String displayName) {
user.setDisplayName(displayName.trim());
return userRepository.save(user);
}
}