Синхронизация моделей, Docker настройки и исправление Angular шаблонов

- V1__init.sql: синхронизированы таблицы с Java моделями (users, folders, messages, contacts)
- docker-compose.yml: обновлён контекст сборки, переменные R2DBC/Flyway, порты (5433, 3026, 1443)
- backend/Dockerfile: обновлены пути COPY для нового контекста сборки
- settings.gradle: удалён frontend проект из Gradle
- contacts.component.html: исправлен ternary оператор с HTML на Angular @if/@else
- Фронтенд компоненты: обновления compose, inbox, layout, login, message-detail, register
- index.html, styles.css: обновления стилей и мета-данных

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
2026-11-05 03:08:36 +01:00
co-authored by Junie
parent 0d16342f9f
commit b6f6d41813
13 changed files with 88 additions and 48 deletions
+3 -2
View File
@@ -1,9 +1,10 @@
# Stage 1: Build
FROM eclipse-temurin:21-jdk-alpine AS build
WORKDIR /app
COPY build.gradle settings.gradle gradlew ./
COPY backend/build.gradle ./
COPY gradlew ./
COPY gradle gradle
COPY src src
COPY backend/src src
RUN chmod +x gradlew
RUN ./gradlew bootJar --no-daemon
@@ -2,8 +2,11 @@
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
display_name VARCHAR(255) NOT NULL,
quota_bytes BIGINT NOT NULL DEFAULT 1073741824,
used_bytes BIGINT NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
@@ -12,7 +15,8 @@ CREATE TABLE folders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(50) NOT NULL,
type VARCHAR(20) NOT NULL,
message_count INTEGER NOT NULL DEFAULT 0,
unread_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
@@ -25,10 +29,13 @@ CREATE TABLE messages (
recipient_email VARCHAR(255) NOT NULL,
subject VARCHAR(1000),
body TEXT,
is_html BOOLEAN NOT NULL DEFAULT FALSE,
is_read BOOLEAN NOT NULL DEFAULT FALSE,
is_flagged BOOLEAN NOT NULL DEFAULT FALSE,
is_spam BOOLEAN NOT NULL DEFAULT FALSE,
message_size BIGINT,
received_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
received_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
message_id VARCHAR(255)
);
-- Таблица вложений
@@ -58,6 +65,8 @@ CREATE TABLE contacts (
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
phone VARCHAR(50),
company VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (user_id, email)
);
+12 -7
View File
@@ -2,18 +2,23 @@ version: '3.8'
services:
mail-server:
build: ./backend
build:
context: .
dockerfile: ./backend/Dockerfile
ports:
- "8080:8080" # REST API
- "3025:3025" # SMTP (приём писем)
- "143:143" # IMAP (доступ к письмам)
- "3026:3025" # SMTP (приём писем)
- "1443:143" # IMAP (доступ к письмам)
volumes:
- attachments:/app/attachments
- dkim-keys:/app/dkim-keys
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/mailserver
- SPRING_DATASOURCE_USERNAME=mail
- SPRING_DATASOURCE_PASSWORD=secret
- SPRING_R2DBC_URL=r2dbc:postgresql://db:5432/mailserver
- SPRING_R2DBC_USERNAME=mail
- SPRING_R2DBC_PASSWORD=secret
- SPRING_FLYWAY_URL=jdbc:postgresql://db:5432/mailserver
- SPRING_FLYWAY_USER=mail
- SPRING_FLYWAY_PASSWORD=secret
- SPRING_MAIL_HOST=mailpit
- SPRING_MAIL_PORT=1025
depends_on:
@@ -29,7 +34,7 @@ services:
volumes:
- pg-data:/var/lib/postgresql/data
ports:
- "5432:5432"
- "5433:5432"
mailpit:
image: axllent/mailpit
@@ -12,7 +12,7 @@ import { Router, ActivatedRoute } from '@angular/router';
template: `
<div class="glass-panel p-6">
<h2 class="text-lg font-orbitron font-bold text-cyan-500 mb-6 flex items-center gap-2">
<span class="text-pink-500">✉️</span> COMPOSE MESSAGE
<i class="fa-solid fa-pen-to-square text-pink-500"></i> COMPOSE MESSAGE
</h2>
@if (success) {
@@ -53,14 +53,14 @@ import { Router, ActivatedRoute } from '@angular/router';
class="w-full bg-slate-950/80 border border-cyan-500/40 rounded-xl px-4 py-3 text-sm text-slate-200 font-mono focus:outline-none focus:ring-2 focus:ring-pink-500 focus:border-cyan-500 transition-all resize-none"
[placeholder]="isHtml ? 'Write HTML or Markdown...' : 'Write your message...'"></textarea>
@if (isHtml) {
<p class="text-xs text-slate-500 font-mono mt-1">💡 Supports Markdown: **bold**, *italic*, - lists, \`\`\`code\`\`\`</p>
<p class="text-xs text-slate-500 font-mono mt-1"><i class="fa-solid fa-lightbulb mr-1"></i>Supports Markdown: **bold**, *italic*, - lists, &nbsp;code&nbsp;</p>
}
</div>
<div class="flex gap-3 pt-4">
<button type="submit" [disabled]="sending"
class="cyber-btn px-8 py-3 rounded-xl text-sm font-bold">
{{ sending ? 'SENDING...' : 'SEND' }}
<i class="fa-solid fa-paper-plane mr-1"></i> {{ sending ? 'SENDING...' : 'SEND' }}
</button>
<a routerLink="/inbox" class="px-6 py-3 rounded-xl text-xs font-mono text-slate-400 border border-slate-600 hover:text-slate-200 hover:border-slate-400 transition-all text-center">
CANCEL
@@ -1,13 +1,13 @@
<div class="space-y-4">
<h2 class="text-lg font-orbitron font-bold text-cyan-300 flex items-center gap-2">
<span class="text-pink-500">👥</span> CONTACTS
<i class="fa-solid fa-users text-pink-500"></i> CONTACTS
<span class="text-sm font-mono text-slate-400 ml-auto">{{ contacts().length }} contacts</span>
</h2>
<!-- Форма добавления -->
<div class="glass-panel p-5">
<h3 class="text-sm font-orbitron font-bold text-yellow-400 mb-3 flex items-center gap-2">
<span class="text-pink-500">✏️</span> {{ editingContact() ? 'EDIT CONTACT' : 'ADD NEW CONTACT' }}
<i class="fa-solid fa-pen-to-square text-pink-500"></i> {{ editingContact() ? 'EDIT CONTACT' : 'ADD NEW CONTACT' }}
</h3>
<form (ngSubmit)="saveContact()" class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
@@ -37,12 +37,16 @@
<div class="md:col-span-2 flex gap-2">
<button type="submit"
class="cyber-btn px-6 py-2 rounded-xl text-xs font-bold">
{{ editingContact() ? '💾 UPDATE' : '➕ ADD' }}
@if (editingContact()) {
<i class="fa-solid fa-save mr-1"></i> UPDATE
} @else {
<i class="fa-solid fa-plus mr-1"></i> ADD
}
</button>
@if (editingContact()) {
<button type="button" (click)="cancelEdit()"
class="px-4 py-2 rounded-xl text-xs font-mono text-slate-400 border border-slate-600/40 hover:text-slate-200 hover:border-slate-400 transition-all">
✖ CANCEL
<i class="fa-solid fa-xmark mr-1"></i> CANCEL
</button>
}
</div>
@@ -53,7 +57,7 @@
<div class="glass-panel p-5">
@if (contacts().length === 0) {
<div class="text-center py-12 text-slate-400 font-mono">
<p class="text-4xl mb-4">📇</p>
<i class="fa-solid fa-address-card text-4xl mb-4 text-slate-600"></i>
<p>No contacts yet</p>
</div>
} @else {
@@ -83,11 +87,11 @@
<div class="flex gap-1">
<button (click)="editContact(contact)"
class="px-2 py-1 text-xs font-mono text-cyan-400 hover:bg-cyan-500/10 rounded-lg transition-all">
✏️
<i class="fa-solid fa-pen"></i>
</button>
<button (click)="deleteContact(contact.id!)"
class="px-2 py-1 text-xs font-mono text-pink-400 hover:bg-pink-500/10 rounded-lg transition-all">
🗑️
<i class="fa-solid fa-trash"></i>
</button>
</div>
</div>
@@ -19,9 +19,9 @@ import { of } from 'rxjs';
<div class="flex gap-3">
<input [(ngModel)]="searchQuery" (keyup.enter)="loadMessages()"
class="flex-1 bg-slate-950/80 border border-cyan-500/40 rounded-xl px-4 py-2 text-sm text-slate-200 font-mono focus:outline-none focus:ring-2 focus:ring-pink-500 focus:border-cyan-400 transition-all"
placeholder="🔍 Search messages...">
placeholder="Search messages...">
<button (click)="loadMessages()" class="cyber-btn px-6 py-2 rounded-xl text-xs font-bold">
SEARCH
<i class="fa-solid fa-magnifying-glass mr-1"></i> SEARCH
</button>
</div>
</div>
@@ -29,13 +29,13 @@ import { of } from 'rxjs';
<!-- Список писем -->
<div class="glass-panel p-4">
<h2 class="text-lg font-orbitron font-bold text-cyan-300 mb-4 flex items-center gap-2">
<span class="text-pink-500">📬</span> {{ folderName() }}
<i class="fa-solid fa-inbox text-pink-500"></i> {{ folderName() }}
<span class="text-sm font-mono text-slate-400 ml-auto">{{ messages()?.length || 0 }} messages</span>
</h2>
@if (!messages() || messages().length === 0) {
<div class="text-center py-12 text-slate-400 font-mono">
<p class="text-4xl mb-4">📭</p>
<i class="fa-solid fa-envelope-open-text text-4xl mb-4 text-slate-600"></i>
<p>No messages yet</p>
</div>
} @else {
@@ -64,7 +64,7 @@ import { of } from 'rxjs';
</span>
}
@if (msg.flagged) {
<span class="text-yellow-400 text-xs">★</span>
<i class="fa-solid fa-star text-yellow-400 text-xs"></i>
}
</div>
<div class="text-sm text-cyan-400 font-rajdhani truncate group-hover:text-pink-400 transition-colors">
@@ -80,7 +80,7 @@ import { of } from 'rxjs';
{{ msg.sentAt | date:'MMM d, HH:mm' }}
</div>
@if (msg.hasAttachments) {
<span class="text-xs text-purple-400 font-mono">📎 {{ msg.attachmentCount }}</span>
<span class="text-xs text-purple-400 font-mono"><i class="fa-solid fa-paperclip mr-1"></i>{{ msg.attachmentCount }}</span>
}
</div>
</div>
@@ -95,7 +95,7 @@ import { of } from 'rxjs';
<div class="glass-panel p-4 flex items-center justify-center gap-2">
<button (click)="goToPage(page() - 1)" [disabled]="page() === 0"
class="px-3 py-1 rounded-lg text-xs font-mono text-cyan-400 border border-cyan-500/30 hover:bg-cyan-500/10 disabled:opacity-30 disabled:cursor-not-allowed transition-all">
← Prev
<i class="fa-solid fa-chevron-left mr-1"></i> Prev
</button>
@for (p of pageNumbers; track p) {
<button (click)="goToPage(p)"
@@ -107,7 +107,7 @@ import { of } from 'rxjs';
}
<button (click)="goToPage(page() + 1)" [disabled]="page() >= totalPages() - 1"
class="px-3 py-1 rounded-lg text-xs font-mono text-cyan-400 border border-cyan-500/30 hover:bg-cyan-500/10 disabled:opacity-30 disabled:cursor-not-allowed transition-all">
Next →
Next <i class="fa-solid fa-chevron-right ml-1"></i>
</button>
<span class="text-xs font-mono text-slate-500 ml-2">
Page {{ page() + 1 }} of {{ totalPages() }}
@@ -76,7 +76,7 @@ const THEMES = [
[attr.aria-pressed]="isHighContrast()"
title="Toggle High Contrast Mode"
class="px-3 py-1.5 rounded-lg border border-cyan-500/40 bg-slate-900/80 text-xs font-mono hover:bg-cyan-500/20 focus:outline-none focus:ring-2 focus:ring-pink-500 transition-all flex items-center gap-1.5">
<span class="text-yellow-400">◐</span>
<i class="fa-solid fa-circle-half-stroke text-yellow-400"></i>
<span>Contrast</span>
</button>
@@ -87,7 +87,7 @@ const THEMES = [
<!-- Logout -->
<button (click)="onLogout()" class="cyber-btn px-4 py-1.5 rounded-lg text-xs font-bold">
⏻ Logout
<i class="fa-solid fa-power-off mr-1"></i> Logout
</button>
</div>
</div>
@@ -99,7 +99,7 @@ const THEMES = [
<aside class="w-64 flex-shrink-0">
<div class="glass-panel p-4">
<h2 class="text-sm font-orbitron font-bold text-cyan-300 mb-3 flex items-center gap-2">
<span class="text-pink-500">📁</span> FOLDERS
<i class="fa-solid fa-folder-tree text-pink-500"></i> FOLDERS
</h2>
<nav class="space-y-1">
@for (folder of folders(); track folder.id) {
@@ -123,14 +123,14 @@ const THEMES = [
<a routerLink="/contacts"
routerLinkActive="bg-cyan-500/20 text-cyan-300 border-cyan-500"
class="block px-3 py-2 rounded-lg text-sm font-rajdhani text-slate-300 hover:bg-cyan-500/10 hover:text-cyan-300 border border-transparent transition-all">
👥 Contacts
<i class="fa-solid fa-address-book mr-2"></i> Contacts
</a>
</div>
<!-- Кнопка написать -->
<div class="glass-panel p-4 mt-4">
<a routerLink="/compose" class="cyber-btn w-full py-3 rounded-xl text-xs font-bold block text-center">
✉️ NEW MESSAGE
<i class="fa-solid fa-pen-to-square mr-2"></i> NEW MESSAGE
</a>
</div>
</aside>
@@ -146,8 +146,8 @@ const THEMES = [
<div class="max-w-7xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-2">
<div>MAILSERVER SYNTHWAVE &copy; 2026</div>
<div class="flex items-center space-x-4">
<span class="text-cyan-400">◉ A11Y READY</span>
<span class="text-pink-400">⚡ HIGH PERF CSS3D</span>
<span class="text-cyan-400"><i class="fa-solid fa-universal-access mr-1"></i> A11Y READY</span>
<span class="text-pink-400"><i class="fa-solid fa-bolt mr-1"></i> HIGH PERF CSS3D</span>
</div>
</div>
</footer>
@@ -55,7 +55,11 @@ import { Router } from '@angular/router';
<button type="submit" [disabled]="loading"
class="cyber-btn w-full py-3 rounded-xl text-sm font-bold mt-6">
{{ loading ? '⏳ CONNECTING...' : '⚡ LOGIN' }}
@if (loading) {
<i class="fa-solid fa-spinner fa-spin mr-1"></i> CONNECTING...
} @else {
<i class="fa-solid fa-bolt mr-1"></i> LOGIN
}
</button>
</form>
@@ -31,7 +31,7 @@ import { Message } from '../../models/mail.models';
@if (message.spam) {
<div class="mt-3 px-3 py-1.5 inline-block rounded-full bg-yellow-500/20 border border-yellow-500/40 text-yellow-500 text-xs font-mono">
⚠️ MARKED AS SPAM
<i class="fa-solid fa-triangle-exclamation mr-1"></i> MARKED AS SPAM
</div>
}
</div>
@@ -47,7 +47,7 @@ import { Message } from '../../models/mail.models';
@if (message.hasAttachments) {
<div class="mb-6 p-4 rounded-xl bg-slate-950/60 border border-purple-500/30">
<h3 class="text-sm font-orbitron font-bold text-purple-500 mb-3">
📎 ATTACHMENTS ({{ message.attachmentCount }})
<i class="fa-solid fa-paperclip mr-1"></i> ATTACHMENTS ({{ message.attachmentCount }})
</h3>
<p class="text-xs text-slate-400 font-mono">
Attachments are available via the API
@@ -58,24 +58,24 @@ import { Message } from '../../models/mail.models';
<!-- Действия -->
<div class="flex gap-3 pt-4 border-t border-cyan-500/20">
<a routerLink="/inbox" class="cyber-btn px-6 py-2 rounded-xl text-xs font-bold">
← BACK TO INBOX
<i class="fa-solid fa-chevron-left mr-1"></i> BACK TO INBOX
</a>
<a [routerLink]="['/compose']" [queryParams]="{replyTo: message.sender, subject: 'Re: ' + (message.subject || '')}"
class="px-6 py-2 rounded-xl text-xs font-mono text-cyan-500 border border-cyan-500/40 hover:bg-cyan-500/20 transition-all">
↩️ REPLY
<i class="fa-solid fa-reply mr-1"></i> REPLY
</a>
<a [routerLink]="['/compose']" [queryParams]="{forwardTo: '', forwardSubject: 'Fwd: ' + (message.subject || ''), forwardBody: message.body}"
class="px-6 py-2 rounded-xl text-xs font-mono text-purple-500 border border-purple-500/40 hover:bg-purple-500/20 transition-all">
➡️ FORWARD
<i class="fa-solid fa-share mr-1"></i> FORWARD
</a>
<button (click)="onDelete()" class="px-6 py-2 rounded-xl text-xs font-mono text-pink-500 border border-pink-500/40 hover:bg-pink-500/20 transition-all">
🗑️ DELETE
<i class="fa-solid fa-trash mr-1"></i> DELETE
</button>
</div>
</div>
} @else {
<div class="glass-panel p-6 text-center py-12">
<p class="text-4xl mb-4">📭</p>
<i class="fa-solid fa-envelope-open-text text-4xl mb-4 text-slate-600"></i>
<p class="text-slate-400 font-mono">Message not found</p>
<a routerLink="/inbox" class="cyber-btn px-6 py-2 rounded-xl text-xs font-bold mt-4 inline-block">
← BACK TO INBOX
@@ -67,7 +67,11 @@ import { Router } from '@angular/router';
<button type="submit" [disabled]="loading"
class="cyber-btn w-full py-3 rounded-xl text-sm font-bold mt-6">
{{ loading ? '⏳ CREATING...' : '⚡ REGISTER' }}
@if (loading) {
<i class="fa-solid fa-spinner fa-spin mr-1"></i> CREATING...
} @else {
<i class="fa-solid fa-bolt mr-1"></i> REGISTER
}
</button>
</form>
+3
View File
@@ -6,9 +6,12 @@
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;800;900&family=Rajdhani:wght@400;500;600;700&family=Fira+Code:wght@400;500;600&display=swap" rel="stylesheet">
<!-- FontAwesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body class="bg-synth-dark">
<app-root></app-root>
+12
View File
@@ -253,6 +253,18 @@ body {
outline-offset: 3px;
}
/* ARIA Inspector Focus Highlight */
.aria-inspect-active {
outline: 2px dashed var(--highlight-color) !important;
outline-offset: 4px;
animation: pulseAria 1.5s infinite;
}
@keyframes pulseAria {
0%, 100% { outline-color: var(--highlight-color); }
50% { outline-color: var(--primary-color); }
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
-2
View File
@@ -1,5 +1,3 @@
include 'backend'
include 'frontend'
project(':backend').projectDir = file('backend')
project(':frontend').projectDir = file('frontend')