Files
reactive-mail-server/frontend/src/app/core/auth.service.ts
T
Liosha84andClaude Opus 5.5 c765c51cf0
continuous-integration/drone/pr Build is passing
Несколько ящиков у одного логина, общие ящики, кто ответил, вход ящик*логин
- V7: users.is_shared, mailbox_access (MEMBER от админа/Jambotron, ATTACHED по паролю ящика), messages.answered_by
- /api/mailboxes: мои ящики с непрочитанными, сессия другого ящика (JWT с claim by), подключить по паролю
  (LoginGuard), убрать из меню
- общий ящик: без своего входа (веб/SMTP/IMAP), участники открывают; «прочитано» общее; ответ из веба → answered_by
- SMTP/IMAP: ящик*логин + пароль логина (как master user в Dovecot)
- открытый чужой ящик (общий, подключённый, OPEN админа): профиль и пароль не меняются (403)
- админ: создать shared, участники ящика (PUT /{id}/members); служебное API: PUT /shared/{address};
  выключение аккаунта убирает и из общих ящиков
- UI: MAILBOXES в левом меню (переключение, ⊕ подключить, × убрать), shared/участники в настройках,
  «Answered by» у письма
- 88 тестов; живой прогон на Postgres 17 (апгрейд V6→V7 на данных), скриншоты UI

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 14:27:11 +01:00

95 lines
3.2 KiB
TypeScript

import { Injectable, computed, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { Observable, tap } from 'rxjs';
interface TokenResponse { token: string; email: string; }
/** JWT in localStorage; who is logged in. */
@Injectable({ providedIn: 'root' })
export class AuthService {
private http = inject(HttpClient);
private router = inject(Router);
readonly email = signal<string | null>(read('email'));
/** Administrator who opened this mailbox (Settings → MAILBOXES → Open), or null */
readonly openedBy = signal<string | null>(read('adminEmail'));
readonly loggedIn = computed(() => this.email() !== null && read('token') !== null);
login(email: string, password: string): Observable<TokenResponse> {
return this.http.post<TokenResponse>('/api/login', { email, password }).pipe(tap(r => this.store(r)));
}
register(email: string, password: string, displayName: string): Observable<TokenResponse> {
return this.http.post<TokenResponse>('/api/register', { email, password, displayName }).pipe(tap(r => this.store(r)));
}
token(): string | null { return read('token'); }
/** Administrator opens another mailbox: its session, the admin session kept to come back. */
openAs(response: TokenResponse): void {
const adminToken = read('token');
const adminEmail = read('email');
try {
if (adminToken && adminEmail && !read('adminToken')) {
localStorage.setItem('adminToken', adminToken);
localStorage.setItem('adminEmail', adminEmail);
}
} catch { /* storage blocked */ }
this.store(response);
this.openedBy.set(read('adminEmail'));
}
/**
* Switch to another mailbox of the same login (left menu). Going back to the mailbox an administrator came
* from ends the administrator's visit.
*/
switchTo(response: TokenResponse): void {
if (this.openedBy() && response.email === this.openedBy()) {
this.backToAdmin();
return;
}
this.store(response);
}
/** Back to the administrator's own mailbox. */
backToAdmin(): void {
const token = read('adminToken');
const email = read('adminEmail');
try {
localStorage.removeItem('adminToken');
localStorage.removeItem('adminEmail');
} catch { /* storage blocked */ }
this.openedBy.set(null);
if (token && email) { this.store({ token, email }); } else { this.clear(); }
}
logout(): void {
this.clear();
this.router.navigate(['/login']);
}
clear(): void {
try {
localStorage.removeItem('token');
localStorage.removeItem('email');
localStorage.removeItem('adminToken');
localStorage.removeItem('adminEmail');
} catch { /* storage blocked */ }
this.email.set(null);
this.openedBy.set(null);
}
private store(response: TokenResponse): void {
try {
localStorage.setItem('token', response.token);
localStorage.setItem('email', response.email);
} catch { /* storage blocked: logged in for this tab only */ }
this.email.set(response.email);
}
}
function read(key: string): string | null {
try { return localStorage.getItem(key); } catch { return null; }
}