Веб-почта: при создании ящика — только имя, @домен подставляется (выбор, если доменов несколько); глазок «показать пароль» у всех полей пароля
continuous-integration/drone/pr Build is passing
continuous-integration/drone/pr Build is passing
Регистрация, смена пароля, создание ящика (вход уже был). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -49,13 +49,21 @@ const NAME = /^[a-z0-9]([a-z0-9._-]{0,62}[a-z0-9])?$/;
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="synth-label" for="password">Password (8+ characters)</label>
|
<label class="synth-label" for="password">Password (8+ characters)</label>
|
||||||
<input id="password" name="password" type="password" class="synth-input" [ngModel]="password()"
|
<div class="relative"><input id="password" name="password" [type]="show() ? 'text' : 'password'" class="synth-input pr-10" [ngModel]="password()"
|
||||||
(ngModelChange)="password.set($event)" required minlength="8" autocomplete="new-password">
|
(ngModelChange)="password.set($event)" required minlength="8" autocomplete="new-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>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="synth-label" for="password2">Password again</label>
|
<label class="synth-label" for="password2">Password again</label>
|
||||||
<input id="password2" name="password2" type="password" class="synth-input" [ngModel]="password2()"
|
<div class="relative"><input id="password2" name="password2" [type]="show() ? 'text' : 'password'" class="synth-input pr-10" [ngModel]="password2()"
|
||||||
(ngModelChange)="password2.set($event)" required autocomplete="new-password">
|
(ngModelChange)="password2.set($event)" required autocomplete="new-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>
|
</div>
|
||||||
@if (error()) {
|
@if (error()) {
|
||||||
<p class="text-sm text-pink-400 font-rajdhani flex items-center gap-2" role="alert">
|
<p class="text-sm text-pink-400 font-rajdhani flex items-center gap-2" role="alert">
|
||||||
@@ -79,6 +87,7 @@ export class RegisterPage {
|
|||||||
|
|
||||||
readonly loaded = signal(false);
|
readonly loaded = signal(false);
|
||||||
readonly enabled = signal(false);
|
readonly enabled = signal(false);
|
||||||
|
readonly show = signal(false);
|
||||||
readonly domains = signal<string[]>([]);
|
readonly domains = signal<string[]>([]);
|
||||||
readonly domain = signal('');
|
readonly domain = signal('');
|
||||||
readonly local = signal('');
|
readonly local = signal('');
|
||||||
|
|||||||
@@ -20,11 +20,26 @@ import { AuthService } from '../../core/auth.service';
|
|||||||
|
|
||||||
<form (ngSubmit)="create()" class="grid grid-cols-1 md:grid-cols-[1fr_1fr_1fr_auto] gap-3 items-end mb-4">
|
<form (ngSubmit)="create()" class="grid grid-cols-1 md:grid-cols-[1fr_1fr_1fr_auto] gap-3 items-end mb-4">
|
||||||
<div><label class="synth-label" for="mb-email">Address</label>
|
<div><label class="synth-label" for="mb-email">Address</label>
|
||||||
<input id="mb-email" name="email" class="synth-input" [(ngModel)]="email" [placeholder]="'no-reply@' + domain()" autocomplete="off" required></div>
|
<div class="flex items-stretch gap-2">
|
||||||
|
<input id="mb-email" name="email" class="synth-input" [ngModel]="local()" (ngModelChange)="local.set($event.toLowerCase().trim())"
|
||||||
|
placeholder="no-reply" autocomplete="off" required>
|
||||||
|
<span class="flex items-center text-cyan-300 font-mono">@</span>
|
||||||
|
@if (domains().length > 1) {
|
||||||
|
<select name="domain" class="synth-input !w-auto" [ngModel]="domain()" (ngModelChange)="domain.set($event)" aria-label="Domain">
|
||||||
|
@for (d of domains(); track d) { <option [value]="d">{{ d }}</option> }
|
||||||
|
</select>
|
||||||
|
} @else {
|
||||||
|
<span class="flex items-center px-3 rounded-[10px] bg-slate-900/70 border border-cyan-500/30 font-mono text-sm text-slate-200 whitespace-nowrap">{{ domain() }}</span>
|
||||||
|
}
|
||||||
|
</div></div>
|
||||||
<div><label class="synth-label" for="mb-name">Name</label>
|
<div><label class="synth-label" for="mb-name">Name</label>
|
||||||
<input id="mb-name" name="name" class="synth-input" [(ngModel)]="name" maxlength="100" autocomplete="off"></div>
|
<input id="mb-name" name="name" class="synth-input" [(ngModel)]="name" maxlength="100" autocomplete="off"></div>
|
||||||
<div><label class="synth-label" for="mb-password">Password (empty — generate)</label>
|
<div><label class="synth-label" for="mb-password">Password (empty — generate)</label>
|
||||||
<input id="mb-password" name="password" type="password" class="synth-input" [(ngModel)]="password" autocomplete="new-password"></div>
|
<div class="relative"><input id="mb-password" name="password" [type]="showPw() ? 'text' : 'password'" class="synth-input pr-10" [(ngModel)]="password" autocomplete="new-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]="showPw() ? 'Hide password' : 'Show password'" (click)="showPw.set(!showPw())">
|
||||||
|
<mat-icon>{{ showPw() ? 'visibility_off' : 'visibility' }}</mat-icon>
|
||||||
|
</button></div></div>
|
||||||
<button type="submit" class="cyber-btn px-4 py-2 rounded-lg text-xs font-bold" [disabled]="busy()"><mat-icon>add</mat-icon> CREATE</button>
|
<button type="submit" class="cyber-btn px-4 py-2 rounded-lg text-xs font-bold" [disabled]="busy()"><mat-icon>add</mat-icon> CREATE</button>
|
||||||
</form>
|
</form>
|
||||||
@if (error()) { <p class="text-sm text-pink-400 mb-3" role="alert">{{ error() }}</p> }
|
@if (error()) { <p class="text-sm text-pink-400 mb-3" role="alert">{{ error() }}</p> }
|
||||||
@@ -74,13 +89,20 @@ export class MailboxesSection {
|
|||||||
readonly error = signal('');
|
readonly error = signal('');
|
||||||
readonly busy = signal(false);
|
readonly busy = signal(false);
|
||||||
readonly me = computed(() => this.state.profile()?.email ?? '');
|
readonly me = computed(() => this.state.profile()?.email ?? '');
|
||||||
readonly domain = computed(() => this.me().split('@')[1] ?? 'example.com');
|
readonly showPw = signal(false);
|
||||||
|
readonly local = signal('');
|
||||||
email = '';
|
// Домены сервера (/api/config); пока не пришли — домен своего ящика
|
||||||
|
readonly domains = signal<string[]>([]);
|
||||||
|
readonly domain = signal('');
|
||||||
name = '';
|
name = '';
|
||||||
password = '';
|
password = '';
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
this.domain.set(this.me().split('@')[1] ?? '');
|
||||||
|
this.api.config().subscribe({
|
||||||
|
next: c => { this.domains.set(c.domains); if (!c.domains.includes(this.domain())) { this.domain.set(c.domains[0] ?? ''); } },
|
||||||
|
error: () => {}
|
||||||
|
});
|
||||||
this.load();
|
this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,14 +115,16 @@ export class MailboxesSection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
create(): void {
|
create(): void {
|
||||||
const email = this.email.trim();
|
const local = this.local().replace(/@.*$/, '');
|
||||||
if (!email) { this.error.set('Enter an address'); return; }
|
if (!local) { this.error.set('Enter an address'); return; }
|
||||||
|
const email = local + '@' + this.domain();
|
||||||
this.error.set('');
|
this.error.set('');
|
||||||
this.busy.set(true);
|
this.busy.set(true);
|
||||||
this.api.createMailbox(email, this.name.trim(), this.password).subscribe({
|
this.api.createMailbox(email, this.name.trim(), this.password).subscribe({
|
||||||
next: created => {
|
next: created => {
|
||||||
this.busy.set(false);
|
this.busy.set(false);
|
||||||
this.email = this.name = this.password = '';
|
this.local.set('');
|
||||||
|
this.name = this.password = '';
|
||||||
if (created.password) { this.secret.set({ email: created.mailbox.email, password: created.password }); }
|
if (created.password) { this.secret.set({ email: created.mailbox.email, password: created.password }); }
|
||||||
this.toasts.ok('Mailbox ' + created.mailbox.email + ' created');
|
this.toasts.ok('Mailbox ' + created.mailbox.email + ' created');
|
||||||
this.load();
|
this.load();
|
||||||
|
|||||||
@@ -42,11 +42,23 @@ import { MailboxesSection } from './mailboxes.section';
|
|||||||
<form (ngSubmit)="changePassword()" class="space-y-3">
|
<form (ngSubmit)="changePassword()" class="space-y-3">
|
||||||
<input type="email" name="username" class="sr-only" [value]="state.profile()?.email ?? ''" autocomplete="username" tabindex="-1" aria-hidden="true">
|
<input type="email" name="username" class="sr-only" [value]="state.profile()?.email ?? ''" autocomplete="username" tabindex="-1" aria-hidden="true">
|
||||||
<div><label class="synth-label" for="pw-current">Current password</label>
|
<div><label class="synth-label" for="pw-current">Current password</label>
|
||||||
<input id="pw-current" name="current" type="password" class="synth-input" [(ngModel)]="current" autocomplete="current-password"></div>
|
<div class="relative"><input id="pw-current" name="current" [type]="showPw() ? 'text' : 'password'" class="synth-input pr-10" [(ngModel)]="current" 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]="showPw() ? 'Hide password' : 'Show password'" (click)="showPw.set(!showPw())">
|
||||||
|
<mat-icon>{{ showPw() ? 'visibility_off' : 'visibility' }}</mat-icon>
|
||||||
|
</button></div></div>
|
||||||
<div><label class="synth-label" for="pw-new">New password (8+ characters)</label>
|
<div><label class="synth-label" for="pw-new">New password (8+ characters)</label>
|
||||||
<input id="pw-new" name="next" type="password" class="synth-input" [(ngModel)]="next" autocomplete="new-password"></div>
|
<div class="relative"><input id="pw-new" name="next" [type]="showPw() ? 'text' : 'password'" class="synth-input pr-10" [(ngModel)]="next" autocomplete="new-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]="showPw() ? 'Hide password' : 'Show password'" (click)="showPw.set(!showPw())">
|
||||||
|
<mat-icon>{{ showPw() ? 'visibility_off' : 'visibility' }}</mat-icon>
|
||||||
|
</button></div></div>
|
||||||
<div><label class="synth-label" for="pw-again">New password again</label>
|
<div><label class="synth-label" for="pw-again">New password again</label>
|
||||||
<input id="pw-again" name="again" type="password" class="synth-input" [(ngModel)]="again" autocomplete="new-password"></div>
|
<div class="relative"><input id="pw-again" name="again" [type]="showPw() ? 'text' : 'password'" class="synth-input pr-10" [(ngModel)]="again" autocomplete="new-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]="showPw() ? 'Hide password' : 'Show password'" (click)="showPw.set(!showPw())">
|
||||||
|
<mat-icon>{{ showPw() ? 'visibility_off' : 'visibility' }}</mat-icon>
|
||||||
|
</button></div></div>
|
||||||
@if (passwordError()) { <p class="text-sm text-pink-400" role="alert">{{ passwordError() }}</p> }
|
@if (passwordError()) { <p class="text-sm text-pink-400" role="alert">{{ passwordError() }}</p> }
|
||||||
<button type="submit" class="cyber-btn px-4 py-2 rounded-lg text-xs font-bold"><mat-icon>lock_reset</mat-icon> CHANGE PASSWORD</button>
|
<button type="submit" class="cyber-btn px-4 py-2 rounded-lg text-xs font-bold"><mat-icon>lock_reset</mat-icon> CHANGE PASSWORD</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -117,6 +129,7 @@ export class SettingsPage {
|
|||||||
next = '';
|
next = '';
|
||||||
again = '';
|
again = '';
|
||||||
readonly passwordError = signal('');
|
readonly passwordError = signal('');
|
||||||
|
readonly showPw = signal(false);
|
||||||
|
|
||||||
readonly used = computed(() => formatBytes(this.state.profile()?.usedBytes));
|
readonly used = computed(() => formatBytes(this.state.profile()?.usedBytes));
|
||||||
readonly quota = computed(() => formatBytes(this.state.profile()?.quotaBytes));
|
readonly quota = computed(() => formatBytes(this.state.profile()?.quotaBytes));
|
||||||
|
|||||||
Reference in New Issue
Block a user