Роли ADMIN и USER, User panel и Admin panel
continuous-integration/drone/pr Build is passing

- Роль ADMIN = users.is_admin; встроенный администратор (mailserver.admins, по
  умолчанию admin@) всегда ADMIN и получает роль в базе при старте.
- PUT /api/admin/mailboxes/{id}/role {ADMIN|USER}: только ADMIN; не свою, не
  встроенного, общий ящик не может быть ADMIN.
- Admin panel (/admin, только ADMIN): USERS (логины и роли, MAKE ADMIN / MAKE USER),
  MAILBOXES, LISTS. User panel (/settings): профиль, пароль, хранилище, вид, мои ящики.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 20:18:18 +01:00
co-authored by Claude Opus 5.5
parent 402d2c403b
commit cf87b60b38
13 changed files with 215 additions and 18 deletions
@@ -45,8 +45,13 @@ public class MailboxAdminController {
private static final Logger log = LoggerFactory.getLogger(MailboxAdminController.class);
private static final int MIN_PASSWORD = MailDirectory.MIN_PASSWORD;
/** @param role ADMIN or USER; @param builtIn the administrator from the configuration — the role stays */
public record MailboxView(Long id, String email, String displayName, boolean active, boolean admin, boolean trap,
boolean shared, List<String> members, long usedBytes, long quotaBytes, Instant createdAt) {
boolean shared, List<String> members, long usedBytes, long quotaBytes, Instant createdAt,
String role, boolean builtIn) {
}
public record RoleRequest(String role) {
}
/** @param password empty — the server generates one and returns it once */
@@ -171,6 +176,30 @@ public class MailboxAdminController {
}));
}
/** Role ADMIN (manages mailboxes, lists, roles — Admin panel) or USER. Not your own, not the built-in administrator. */
@PutMapping("/{id}/role")
public Mono<MailboxView> role(Mono<Authentication> auth, @PathVariable Long id, @RequestBody Mono<RoleRequest> body) {
return admin(auth).flatMap(me -> body.flatMap(request -> mailbox(id).flatMap(user -> {
if (!"ADMIN".equals(request.role()) && !"USER".equals(request.role())) {
return Mono.error(bad("Role: ADMIN or USER"));
}
boolean toAdmin = "ADMIN".equals(request.role());
if (user.getEmail().equalsIgnoreCase(me)) {
return Mono.error(bad("You can not change your own role"));
}
if (!toAdmin && admins.isBuiltIn(user.getEmail())) {
return Mono.error(bad("The built-in administrator stays ADMIN"));
}
if (toAdmin && user.isShared()) {
return Mono.error(bad("A shared mailbox has no login of its own — it can not be ADMIN"));
}
return users.setAdmin(user, toAdmin).doOnNext(u -> {
admins.grant(u.getEmail(), toAdmin);
log.warn("Mailbox {} role {} given by administrator {}", u.getEmail(), request.role(), me);
});
}))).flatMap(this::view);
}
/** Trap: what the mailbox sends is stored in its own INBOX, nothing leaves the server (staging@). */
@PutMapping("/{id}/trap")
public Mono<MailboxView> trap(Mono<Authentication> auth, @PathVariable Long id, @RequestBody Mono<TrapRequest> body) {
@@ -237,7 +266,7 @@ public class MailboxAdminController {
return mail.usedBytes(user.getId()).defaultIfEmpty(0L).zipWith(access.members(user.getId()).collectList())
.map(pair -> new MailboxView(user.getId(), user.getEmail(), user.getDisplayName(), user.isActive(),
admins.isAdmin(user.getEmail()), user.isTrap(), user.isShared(), pair.getT2(), pair.getT1(), user.getQuotaBytes() == null ? 0 : user.getQuotaBytes(),
user.getCreatedAt()));
user.getCreatedAt(), admins.isAdmin(user.getEmail()) ? "ADMIN" : "USER", admins.isBuiltIn(user.getEmail())));
}
private static ResponseStatusException bad(String message) {