Почтовый кабинет: регистрация, папки, черновики, вложения, грид @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:
@@ -1,36 +1,68 @@
|
||||
package com.mailserver.controller;
|
||||
|
||||
import com.mailserver.config.MailDomains;
|
||||
import com.mailserver.domain.User;
|
||||
import com.mailserver.dto.LoginRequest;
|
||||
import com.mailserver.dto.LoginResponse;
|
||||
import com.mailserver.domain.User;
|
||||
import com.mailserver.dto.PasswordChangeRequest;
|
||||
import com.mailserver.dto.ProfileResponse;
|
||||
import com.mailserver.dto.ProfileUpdateRequest;
|
||||
import com.mailserver.dto.RegisterRequest;
|
||||
import com.mailserver.service.MailService;
|
||||
import com.mailserver.service.UserService;
|
||||
import com.mailserver.util.JwtUtils;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** Login, registration, the logged-in user's profile and password; what the login page needs to know. */
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class AuthenticationController {
|
||||
|
||||
private final UserService userService;
|
||||
private final JwtUtils jwtUtils;
|
||||
/** Service addresses nobody can register (RFC 2142 and ours). */
|
||||
static final Set<String> RESERVED = Set.of("postmaster", "abuse", "mailer-daemon", "hostmaster", "webmaster",
|
||||
"root", "admin", "noreply", "no-reply", "security", "support");
|
||||
|
||||
public AuthenticationController(UserService userService, JwtUtils jwtUtils) {
|
||||
private final UserService userService;
|
||||
private final MailService mailService;
|
||||
private final JwtUtils jwtUtils;
|
||||
private final MailDomains domains;
|
||||
private final boolean registrationEnabled;
|
||||
|
||||
public AuthenticationController(UserService userService, MailService mailService, JwtUtils jwtUtils, MailDomains domains,
|
||||
@Value("${mailserver.registration.enabled:false}") boolean registrationEnabled) {
|
||||
this.userService = userService;
|
||||
this.mailService = mailService;
|
||||
this.jwtUtils = jwtUtils;
|
||||
this.domains = domains;
|
||||
this.registrationEnabled = registrationEnabled;
|
||||
}
|
||||
|
||||
/** Public: is registration open, and in which domains mailboxes can be created. */
|
||||
@GetMapping("/config")
|
||||
public Map<String, Object> config() {
|
||||
return Map.of("registrationEnabled", registrationEnabled, "domains", domains.domains().stream().sorted().toList());
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public Mono<ResponseEntity<?>> login(@Valid @RequestBody Mono<LoginRequest> requestMono) {
|
||||
return requestMono.flatMap(request ->
|
||||
userService.findByEmail(request.getEmail())
|
||||
userService.findByEmail(request.getEmail().trim().toLowerCase(Locale.ROOT))
|
||||
.switchIfEmpty(Mono.error(new IllegalArgumentException("Invalid credentials")))
|
||||
.flatMap(user -> {
|
||||
if (!user.isActive()) {
|
||||
@@ -39,15 +71,74 @@ public class AuthenticationController {
|
||||
if (!userService.checkPassword(user, request.getPassword())) {
|
||||
return Mono.just(ResponseEntity.badRequest().body(Map.of("error", "Invalid credentials")));
|
||||
}
|
||||
String token = jwtUtils.generateToken(user.getEmail());
|
||||
LoginResponse response = new LoginResponse();
|
||||
response.setToken(token);
|
||||
response.setEmail(user.getEmail());
|
||||
return Mono.just(ResponseEntity.ok(response));
|
||||
return Mono.just(ResponseEntity.ok(token(user)));
|
||||
})
|
||||
.onErrorResume(IllegalArgumentException.class, e ->
|
||||
Mono.just(ResponseEntity.badRequest().body(Map.of("error", "Invalid credentials")))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** New mailbox in one of our domains; logs in right away (the answer carries the token). */
|
||||
@PostMapping("/register")
|
||||
public Mono<ResponseEntity<?>> register(@Valid @RequestBody Mono<RegisterRequest> requestMono) {
|
||||
if (!registrationEnabled) {
|
||||
return Mono.just(ResponseEntity.status(HttpStatus.FORBIDDEN).body(Map.of("error", "Registration is closed")));
|
||||
}
|
||||
return requestMono.flatMap(request -> {
|
||||
String email = request.getEmail().trim().toLowerCase(Locale.ROOT);
|
||||
String local = email.substring(0, email.indexOf('@'));
|
||||
if (!domains.isLocal(email)) {
|
||||
return Mono.just(ResponseEntity.badRequest()
|
||||
.body(Map.of("error", "Mailboxes can only be created in: " + String.join(", ", domains.domains()))));
|
||||
}
|
||||
if (RESERVED.contains(local) || !local.matches("[a-z0-9]([a-z0-9._-]{0,62}[a-z0-9])?")) {
|
||||
return Mono.just(ResponseEntity.badRequest()
|
||||
.body(Map.of("error", "This name is not available: letters, digits, dot, dash, underscore")));
|
||||
}
|
||||
return userService.register(email, request.getPassword(), request.getDisplayName())
|
||||
.map(user -> ResponseEntity.status(HttpStatus.CREATED).body((Object) token(user)))
|
||||
.onErrorResume(IllegalArgumentException.class, e ->
|
||||
Mono.just(ResponseEntity.badRequest().body(Map.of("error", "This mailbox already exists"))));
|
||||
});
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
public Mono<ProfileResponse> me(Mono<Authentication> auth) {
|
||||
return user(auth).flatMap(this::profile);
|
||||
}
|
||||
|
||||
@PutMapping("/me")
|
||||
public Mono<ProfileResponse> updateMe(Mono<Authentication> auth, @Valid @RequestBody Mono<ProfileUpdateRequest> body) {
|
||||
return user(auth).zipWith(body)
|
||||
.flatMap(pair -> userService.updateDisplayName(pair.getT1(), pair.getT2().displayName()))
|
||||
.flatMap(this::profile);
|
||||
}
|
||||
|
||||
@PutMapping("/me/password")
|
||||
public Mono<Map<String, String>> changePassword(Mono<Authentication> auth, @Valid @RequestBody Mono<PasswordChangeRequest> body) {
|
||||
return user(auth).zipWith(body)
|
||||
.flatMap(pair -> userService.changePassword(pair.getT1(), pair.getT2().currentPassword(), pair.getT2().newPassword()))
|
||||
.thenReturn(Map.of("message", "Password changed"));
|
||||
}
|
||||
|
||||
private Mono<ProfileResponse> profile(User user) {
|
||||
return mailService.usedBytes(user.getId())
|
||||
.map(used -> new ProfileResponse(user.getEmail(), user.getDisplayName(), used,
|
||||
user.getQuotaBytes() == null ? 0 : user.getQuotaBytes(), user.getCreatedAt()));
|
||||
}
|
||||
|
||||
private Mono<User> user(Mono<Authentication> auth) {
|
||||
return auth.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.UNAUTHORIZED)))
|
||||
.flatMap(a -> userService.findByEmail(a.getName()))
|
||||
.onErrorMap(IllegalArgumentException.class, e -> new ResponseStatusException(HttpStatus.UNAUTHORIZED));
|
||||
}
|
||||
|
||||
private LoginResponse token(User user) {
|
||||
LoginResponse response = new LoginResponse();
|
||||
response.setToken(jwtUtils.generateToken(user.getEmail()));
|
||||
response.setEmail(user.getEmail());
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user