Бэкенд: регистрация со входом сразу и /api/config, профиль и смена пароля, свои папки, PATCH письма (прочитано, флажок, перенос; Спам ставит отметку), корзина и очистка, черновики, несколько адресатов и копия, вложения черновика уходят с письмом, вложения и заголовки To/Cc входящих сохраняются, письма заблокированных — в Спам. Исправлено: загрузка файла (415), колонка content_id (500 на любых вложениях), SPA-маршрут с точкой. Фронтенд переписан: все таблицы — грид, оформление — шаблон synthwave-design-showcase.html. α Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
81 lines
3.2 KiB
TypeScript
81 lines
3.2 KiB
TypeScript
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
|
|
import { FormsModule } from '@angular/forms';
|
|
import { Router, RouterLink } from '@angular/router';
|
|
import { MatIconModule } from '@angular/material/icon';
|
|
import { ApiService } from '../../core/api.service';
|
|
import { AuthService } from '../../core/auth.service';
|
|
import { errorText } from '../../core/models';
|
|
import { AuthFrameComponent } from './auth-frame.component';
|
|
|
|
@Component({
|
|
selector: 'app-login',
|
|
imports: [FormsModule, RouterLink, MatIconModule, AuthFrameComponent],
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
template: `
|
|
<app-auth-frame title="LOG IN" subtitle="Your mailbox address and password">
|
|
<form (ngSubmit)="submit()" class="space-y-4">
|
|
<div>
|
|
<label class="synth-label" for="email">Email</label>
|
|
<input id="email" name="email" type="email" class="synth-input" [(ngModel)]="email" required
|
|
autocomplete="username" placeholder="you@domain">
|
|
</div>
|
|
<div>
|
|
<label class="synth-label" for="password">Password</label>
|
|
<div class="relative">
|
|
<input id="password" name="password" [type]="show() ? 'text' : 'password'" class="synth-input pr-10"
|
|
[(ngModel)]="password" required autocomplete="current-password">
|
|
<button type="button" class="absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-cyan-300"
|
|
[attr.aria-label]="show() ? 'Hide password' : 'Show password'" (click)="show.set(!show())">
|
|
<mat-icon>{{ show() ? 'visibility_off' : 'visibility' }}</mat-icon>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
@if (error()) {
|
|
<p class="text-sm text-pink-400 font-rajdhani flex items-center gap-2" role="alert">
|
|
<mat-icon class="!text-lg">error</mat-icon>{{ error() }}
|
|
</p>
|
|
}
|
|
<button type="submit" class="cyber-btn w-full py-2.5 rounded-xl font-bold text-sm" [disabled]="busy()">
|
|
<mat-icon>login</mat-icon> {{ busy() ? 'LOGGING IN…' : 'LOG IN' }}
|
|
</button>
|
|
</form>
|
|
@if (registration()) {
|
|
<p class="mt-6 text-center text-sm text-slate-400 font-rajdhani">
|
|
No mailbox yet? <a routerLink="/register" class="text-cyan-300 hover:text-pink-400 font-semibold">Create one</a>
|
|
</p>
|
|
}
|
|
</app-auth-frame>
|
|
`
|
|
})
|
|
export class LoginPage {
|
|
private auth = inject(AuthService);
|
|
private router = inject(Router);
|
|
|
|
email = '';
|
|
password = '';
|
|
readonly show = signal(false);
|
|
readonly busy = signal(false);
|
|
readonly error = signal('');
|
|
readonly registration = signal(false);
|
|
|
|
constructor() {
|
|
inject(ApiService).config().subscribe({ next: c => this.registration.set(c.registrationEnabled), error: () => {} });
|
|
}
|
|
|
|
submit(): void {
|
|
if (!this.email.trim() || !this.password) {
|
|
this.error.set('Enter the email and the password');
|
|
return;
|
|
}
|
|
this.busy.set(true);
|
|
this.error.set('');
|
|
this.auth.login(this.email.trim(), this.password).subscribe({
|
|
next: () => this.router.navigate(['/']),
|
|
error: e => {
|
|
this.busy.set(false);
|
|
this.error.set(errorText(e) === 'Invalid credentials' ? 'Wrong email or password' : errorText(e));
|
|
}
|
|
});
|
|
}
|
|
}
|