feat: add debug logging for SMTP configuration and detailed mail delivery status
Build and Push Multi-Platform Images / build-and-push (push) Failing after 34s
Build and Push Multi-Platform Images / build-and-push (push) Failing after 34s
This commit is contained in:
@@ -16,6 +16,7 @@ import { KontonummernModule } from './kontonummern/kontonummern.module';
|
|||||||
import { StatsModule } from './stats/stats.module';
|
import { StatsModule } from './stats/stats.module';
|
||||||
import { BarcodeModule } from './barcode/barcode.module';
|
import { BarcodeModule } from './barcode/barcode.module';
|
||||||
import { InboxPostprocessorModule } from './inbox-postprocessor/inbox-postprocessor.module';
|
import { InboxPostprocessorModule } from './inbox-postprocessor/inbox-postprocessor.module';
|
||||||
|
import { UserSettingsModule } from './user-settings/user-settings.module';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -43,6 +44,7 @@ import * as path from 'path';
|
|||||||
StatsModule,
|
StatsModule,
|
||||||
BarcodeModule,
|
BarcodeModule,
|
||||||
InboxPostprocessorModule,
|
InboxPostprocessorModule,
|
||||||
|
UserSettingsModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
InboxDocument,
|
InboxDocument,
|
||||||
InboxPostprocessingAction,
|
InboxPostprocessingAction,
|
||||||
CorrespondentEmailMapping,
|
CorrespondentEmailMapping,
|
||||||
|
UserSettings,
|
||||||
} from './entities';
|
} from './entities';
|
||||||
|
|
||||||
const entities = [
|
const entities = [
|
||||||
@@ -47,6 +48,7 @@ const entities = [
|
|||||||
InboxDocument,
|
InboxDocument,
|
||||||
InboxPostprocessingAction,
|
InboxPostprocessingAction,
|
||||||
CorrespondentEmailMapping,
|
CorrespondentEmailMapping,
|
||||||
|
UserSettings,
|
||||||
];
|
];
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
|||||||
@@ -19,3 +19,4 @@ export { BarcodeTemplate } from './barcode-template.entity';
|
|||||||
export { InboxDocument } from './inbox-document.entity';
|
export { InboxDocument } from './inbox-document.entity';
|
||||||
export { InboxPostprocessingAction } from './inbox-postprocessing-action.entity';
|
export { InboxPostprocessingAction } from './inbox-postprocessing-action.entity';
|
||||||
export { CorrespondentEmailMapping } from './correspondent-email-mapping.entity';
|
export { CorrespondentEmailMapping } from './correspondent-email-mapping.entity';
|
||||||
|
export { UserSettings } from './user-settings.entity';
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Entity, PrimaryColumn, Column } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('user_settings')
|
||||||
|
export class UserSettings {
|
||||||
|
@PrimaryColumn({ type: 'varchar', length: 255 })
|
||||||
|
UserId!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||||
|
SmtpHost!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'int', nullable: true })
|
||||||
|
SmtpPort!: number | null;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: false })
|
||||||
|
SmtpSecure!: boolean;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||||
|
SmtpUser!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||||
|
SmtpPass!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||||
|
SmtpFrom!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
MailSignatureHtml!: string | null;
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import { createReadStream } from 'fs';
|
|||||||
import { InboxService } from './inbox.service';
|
import { InboxService } from './inbox.service';
|
||||||
import { InboxPostprocessorService } from '../inbox-postprocessor/inbox-postprocessor.service';
|
import { InboxPostprocessorService } from '../inbox-postprocessor/inbox-postprocessor.service';
|
||||||
import { BarcodeScannerService } from '../barcode/barcode-scanner.service';
|
import { BarcodeScannerService } from '../barcode/barcode-scanner.service';
|
||||||
|
import { UserSettingsService } from '../user-settings/user-settings.service';
|
||||||
import { RequirePermissions } from '../auth/permissions.decorator';
|
import { RequirePermissions } from '../auth/permissions.decorator';
|
||||||
import { Permission } from '../auth/permissions.enum';
|
import { Permission } from '../auth/permissions.enum';
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ export class InboxController {
|
|||||||
private readonly inboxService: InboxService,
|
private readonly inboxService: InboxService,
|
||||||
private readonly postprocessor: InboxPostprocessorService,
|
private readonly postprocessor: InboxPostprocessorService,
|
||||||
private readonly barcodeScanner: BarcodeScannerService,
|
private readonly barcodeScanner: BarcodeScannerService,
|
||||||
|
private readonly userSettingsService: UserSettingsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@@ -205,6 +207,10 @@ export class InboxController {
|
|||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const preferredUsername: string | null = req.user?.preferredUsername ?? null;
|
const preferredUsername: string | null = req.user?.preferredUsername ?? null;
|
||||||
await this.inboxService.sendAsEmail(id, preferredUsername, body);
|
const smtpOverride = await this.userSettingsService.getSmtpConfig(req.user.userId);
|
||||||
|
await this.inboxService.sendAsEmail(id, preferredUsername, {
|
||||||
|
...body,
|
||||||
|
smtpOverride: smtpOverride ?? undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { InboxMigrationService } from './inbox-migration.service';
|
|||||||
import { BarcodeModule } from '../barcode/barcode.module';
|
import { BarcodeModule } from '../barcode/barcode.module';
|
||||||
import { InboxPostprocessorModule } from '../inbox-postprocessor/inbox-postprocessor.module';
|
import { InboxPostprocessorModule } from '../inbox-postprocessor/inbox-postprocessor.module';
|
||||||
import { PostprocessingModule } from '../postprocessing/postprocessing.module';
|
import { PostprocessingModule } from '../postprocessing/postprocessing.module';
|
||||||
|
import { UserSettingsModule } from '../user-settings/user-settings.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -17,6 +18,7 @@ import { PostprocessingModule } from '../postprocessing/postprocessing.module';
|
|||||||
BarcodeModule,
|
BarcodeModule,
|
||||||
InboxPostprocessorModule,
|
InboxPostprocessorModule,
|
||||||
PostprocessingModule,
|
PostprocessingModule,
|
||||||
|
UserSettingsModule,
|
||||||
],
|
],
|
||||||
controllers: [InboxController, ClientsController],
|
controllers: [InboxController, ClientsController],
|
||||||
providers: [InboxService, InboxMigrationService],
|
providers: [InboxService, InboxMigrationService],
|
||||||
|
|||||||
@@ -274,6 +274,7 @@ export class InboxService {
|
|||||||
body: string;
|
body: string;
|
||||||
html?: string;
|
html?: string;
|
||||||
segments: { pages: number[]; filename: string }[];
|
segments: { pages: number[]; filename: string }[];
|
||||||
|
smtpOverride?: { host: string; port: number; secure: boolean; user: string; pass: string; from: string };
|
||||||
},
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { doc, pdfPath } = await this.resolveDocument(id, preferredUsername);
|
const { doc, pdfPath } = await this.resolveDocument(id, preferredUsername);
|
||||||
@@ -294,6 +295,7 @@ export class InboxService {
|
|||||||
body: opts.body,
|
body: opts.body,
|
||||||
html: opts.html,
|
html: opts.html,
|
||||||
attachments,
|
attachments,
|
||||||
|
smtpOverride: opts.smtpOverride,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,27 @@ export class MailService {
|
|||||||
body: string;
|
body: string;
|
||||||
html?: string;
|
html?: string;
|
||||||
attachments?: { filename: string; content: Buffer }[];
|
attachments?: { filename: string; content: Buffer }[];
|
||||||
|
smtpOverride?: { host: string; port: number; secure: boolean; user: string; pass: string; from: string };
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const from = this.configService.get<string>('SMTP_FROM', 'paperless@localhost');
|
let transporter = this.transporter;
|
||||||
|
let from = this.configService.get<string>('SMTP_FROM', 'paperless@localhost');
|
||||||
|
|
||||||
await this.transporter.sendMail({
|
if (options.smtpOverride) {
|
||||||
|
const o = options.smtpOverride;
|
||||||
|
transporter = nodemailer.createTransport({
|
||||||
|
host: o.host,
|
||||||
|
port: o.port,
|
||||||
|
secure: o.secure,
|
||||||
|
auth: { user: o.user, pass: o.pass },
|
||||||
|
});
|
||||||
|
from = o.from || from;
|
||||||
|
} else {
|
||||||
|
this.logger.debug(
|
||||||
|
`SMTP config — host: ${this.configService.get('SMTP_HOST')} port: ${this.configService.get('SMTP_PORT')} secure: ${this.configService.get('SMTP_SECURE')} user: ${this.configService.get('SMTP_USER')} from: ${from}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = await transporter.sendMail({
|
||||||
from,
|
from,
|
||||||
to: options.to,
|
to: options.to,
|
||||||
subject: options.subject,
|
subject: options.subject,
|
||||||
@@ -40,6 +57,8 @@ export class MailService {
|
|||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.log(`Mail gesendet an ${options.to}: "${options.subject}"`);
|
this.logger.log(
|
||||||
|
`Mail gesendet an ${options.to}: "${options.subject}" — messageId: ${info.messageId} accepted: ${JSON.stringify(info.accepted)} rejected: ${JSON.stringify(info.rejected)} response: ${info.response}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Body, Controller, Get, HttpCode, Post, Put, Request } from '@nestjs/common';
|
||||||
|
import { UserSettingsService } from './user-settings.service';
|
||||||
|
|
||||||
|
@Controller('api/user-settings')
|
||||||
|
export class UserSettingsController {
|
||||||
|
constructor(private readonly userSettingsService: UserSettingsService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async getSettings(@Request() req: any) {
|
||||||
|
return this.userSettingsService.getSettings(req.user.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put()
|
||||||
|
async updateSettings(@Request() req: any, @Body() body: any) {
|
||||||
|
return this.userSettingsService.updateSettings(req.user.userId, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('test-smtp')
|
||||||
|
@HttpCode(200)
|
||||||
|
async testSmtp(@Body() body: { host: string; port: number; secure: boolean; user: string; pass: string }) {
|
||||||
|
return this.userSettingsService.testSmtp(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { UserSettings } from '../database/entities/user-settings.entity';
|
||||||
|
import { UserSettingsService } from './user-settings.service';
|
||||||
|
import { UserSettingsController } from './user-settings.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([UserSettings])],
|
||||||
|
providers: [UserSettingsService],
|
||||||
|
controllers: [UserSettingsController],
|
||||||
|
exports: [UserSettingsService],
|
||||||
|
})
|
||||||
|
export class UserSettingsModule {}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import * as nodemailer from 'nodemailer';
|
||||||
|
import { UserSettings } from '../database/entities/user-settings.entity';
|
||||||
|
|
||||||
|
export interface UserSettingsDto {
|
||||||
|
smtpHost: string | null;
|
||||||
|
smtpPort: number | null;
|
||||||
|
smtpSecure: boolean;
|
||||||
|
smtpUser: string | null;
|
||||||
|
smtpPassSet: boolean;
|
||||||
|
smtpFrom: string | null;
|
||||||
|
mailSignatureHtml: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserSettingsService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(UserSettings)
|
||||||
|
private readonly repo: Repository<UserSettings>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getSettings(userId: string): Promise<UserSettingsDto> {
|
||||||
|
const entity = await this.repo.findOne({ where: { UserId: userId } });
|
||||||
|
return this.toDto(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSettings(
|
||||||
|
userId: string,
|
||||||
|
data: {
|
||||||
|
smtpHost?: string | null;
|
||||||
|
smtpPort?: number | null;
|
||||||
|
smtpSecure?: boolean;
|
||||||
|
smtpUser?: string | null;
|
||||||
|
smtpPass?: string | null;
|
||||||
|
smtpFrom?: string | null;
|
||||||
|
mailSignatureHtml?: string | null;
|
||||||
|
},
|
||||||
|
): Promise<UserSettingsDto> {
|
||||||
|
let entity = await this.repo.findOne({ where: { UserId: userId } });
|
||||||
|
if (!entity) {
|
||||||
|
entity = this.repo.create({ UserId: userId });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.smtpHost !== undefined) entity.SmtpHost = data.smtpHost;
|
||||||
|
if (data.smtpPort !== undefined) entity.SmtpPort = data.smtpPort;
|
||||||
|
if (data.smtpSecure !== undefined) entity.SmtpSecure = data.smtpSecure;
|
||||||
|
if (data.smtpUser !== undefined) entity.SmtpUser = data.smtpUser;
|
||||||
|
if (data.smtpPass !== undefined && data.smtpPass !== null && data.smtpPass !== '') {
|
||||||
|
entity.SmtpPass = data.smtpPass;
|
||||||
|
}
|
||||||
|
if (data.smtpFrom !== undefined) entity.SmtpFrom = data.smtpFrom;
|
||||||
|
if (data.mailSignatureHtml !== undefined) entity.MailSignatureHtml = data.mailSignatureHtml;
|
||||||
|
|
||||||
|
await this.repo.save(entity);
|
||||||
|
return this.toDto(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
async testSmtp(config: {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
secure: boolean;
|
||||||
|
user: string;
|
||||||
|
pass: string;
|
||||||
|
}): Promise<{ ok: boolean; error?: string }> {
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: config.host,
|
||||||
|
port: config.port,
|
||||||
|
secure: config.secure,
|
||||||
|
auth: { user: config.user, pass: config.pass },
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await transporter.verify();
|
||||||
|
return { ok: true };
|
||||||
|
} catch (err: any) {
|
||||||
|
return { ok: false, error: err.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSmtpConfig(userId: string): Promise<{
|
||||||
|
host: string; port: number; secure: boolean; user: string; pass: string; from: string;
|
||||||
|
} | null> {
|
||||||
|
const entity = await this.repo.findOne({ where: { UserId: userId } });
|
||||||
|
if (!entity?.SmtpHost || !entity?.SmtpPass) return null;
|
||||||
|
return {
|
||||||
|
host: entity.SmtpHost,
|
||||||
|
port: entity.SmtpPort ?? 587,
|
||||||
|
secure: entity.SmtpSecure,
|
||||||
|
user: entity.SmtpUser ?? '',
|
||||||
|
pass: entity.SmtpPass,
|
||||||
|
from: entity.SmtpFrom ?? entity.SmtpUser ?? '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private toDto(entity: UserSettings | null): UserSettingsDto {
|
||||||
|
return {
|
||||||
|
smtpHost: entity?.SmtpHost ?? null,
|
||||||
|
smtpPort: entity?.SmtpPort ?? null,
|
||||||
|
smtpSecure: entity?.SmtpSecure ?? false,
|
||||||
|
smtpUser: entity?.SmtpUser ?? null,
|
||||||
|
smtpPassSet: !!(entity?.SmtpPass),
|
||||||
|
smtpFrom: entity?.SmtpFrom ?? null,
|
||||||
|
mailSignatureHtml: entity?.MailSignatureHtml ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import ManuellBearbeitenPage from './pages/ManuellBearbeitenPage';
|
|||||||
import MailpostfachPage from './pages/MailpostfachPage';
|
import MailpostfachPage from './pages/MailpostfachPage';
|
||||||
import MailDetailPage from './pages/MailDetailPage';
|
import MailDetailPage from './pages/MailDetailPage';
|
||||||
import SettingsPage from './pages/SettingsPage';
|
import SettingsPage from './pages/SettingsPage';
|
||||||
|
import UserSettingsPage from './pages/UserSettingsPage';
|
||||||
import LoginPage from './pages/LoginPage';
|
import LoginPage from './pages/LoginPage';
|
||||||
import DashboardPage from './pages/DashboardPage';
|
import DashboardPage from './pages/DashboardPage';
|
||||||
import { Spin, Result, Button } from 'antd';
|
import { Spin, Result, Button } from 'antd';
|
||||||
@@ -127,6 +128,7 @@ function ThemedApp() {
|
|||||||
<Route path="/mailpostfach" element={<PermissionRoute permission={Permission.VIEW_MAIL}><MailpostfachPage /></PermissionRoute>} />
|
<Route path="/mailpostfach" element={<PermissionRoute permission={Permission.VIEW_MAIL}><MailpostfachPage /></PermissionRoute>} />
|
||||||
<Route path="/mailpostfach/:id" element={<PermissionRoute permission={Permission.VIEW_MAIL}><MailDetailPage /></PermissionRoute>} />
|
<Route path="/mailpostfach/:id" element={<PermissionRoute permission={Permission.VIEW_MAIL}><MailDetailPage /></PermissionRoute>} />
|
||||||
<Route path="/settings" element={<PermissionRoute permission={Permission.MANAGE_SETTINGS}><SettingsPage /></PermissionRoute>} />
|
<Route path="/settings" element={<PermissionRoute permission={Permission.MANAGE_SETTINGS}><SettingsPage /></PermissionRoute>} />
|
||||||
|
<Route path="/user-settings" element={<UserSettingsPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import api from './client';
|
||||||
|
|
||||||
|
export interface UserSettingsData {
|
||||||
|
smtpHost: string | null;
|
||||||
|
smtpPort: number | null;
|
||||||
|
smtpSecure: boolean;
|
||||||
|
smtpUser: string | null;
|
||||||
|
smtpPassSet: boolean;
|
||||||
|
smtpFrom: string | null;
|
||||||
|
mailSignatureHtml: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userSettingsApi = {
|
||||||
|
get: () => api.get<UserSettingsData>('/api/user-settings').then((r) => r.data),
|
||||||
|
|
||||||
|
update: (data: Partial<UserSettingsData> & { smtpPass?: string }) =>
|
||||||
|
api.put<UserSettingsData>('/api/user-settings', data).then((r) => r.data),
|
||||||
|
|
||||||
|
testSmtp: (cfg: { host: string; port: number; secure: boolean; user: string; pass: string }) =>
|
||||||
|
api
|
||||||
|
.post<{ ok: boolean; error?: string }>('/api/user-settings/test-smtp', cfg)
|
||||||
|
.then((r) => r.data),
|
||||||
|
};
|
||||||
@@ -204,6 +204,12 @@ export default function AppLayout() {
|
|||||||
<Dropdown
|
<Dropdown
|
||||||
menu={{
|
menu={{
|
||||||
items: [
|
items: [
|
||||||
|
{
|
||||||
|
key: 'user-settings',
|
||||||
|
icon: <SettingOutlined />,
|
||||||
|
label: 'Benutzereinstellungen',
|
||||||
|
onClick: () => navigate('/user-settings'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'logout',
|
key: 'logout',
|
||||||
icon: <LogoutOutlined />,
|
icon: <LogoutOutlined />,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import StarterKit from '@tiptap/starter-kit';
|
|||||||
import Underline from '@tiptap/extension-underline';
|
import Underline from '@tiptap/extension-underline';
|
||||||
import { inboxApi, type InboxBarcode, type InboxFile, type PostprocessActionResult } from '../api/inbox';
|
import { inboxApi, type InboxBarcode, type InboxFile, type PostprocessActionResult } from '../api/inbox';
|
||||||
import { paperlessApi } from '../api/paperless';
|
import { paperlessApi } from '../api/paperless';
|
||||||
|
import { userSettingsApi } from '../api/userSettings';
|
||||||
|
|
||||||
const ZOOM_MIN = 0.5;
|
const ZOOM_MIN = 0.5;
|
||||||
const ZOOM_MAX = 3;
|
const ZOOM_MAX = 3;
|
||||||
@@ -678,13 +679,17 @@ function SendEmailDialog({ open, fileId, fileName, documents, onClose }: SendEma
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
editor?.commands.clearContent();
|
|
||||||
const base = fileName.replace(/\.pdf$/i, '');
|
const base = fileName.replace(/\.pdf$/i, '');
|
||||||
setFilenames(
|
setFilenames(
|
||||||
documents.map((doc) =>
|
documents.map((doc) =>
|
||||||
doc.belegname || (documents.length === 1 ? base : `${base}_${doc.index + 1}`),
|
doc.belegname || (documents.length === 1 ? base : `${base}_${doc.index + 1}`),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
userSettingsApi.get().then((settings) => {
|
||||||
|
editor?.commands.setContent(settings.mailSignatureHtml ?? '');
|
||||||
|
}).catch(() => {
|
||||||
|
editor?.commands.clearContent();
|
||||||
|
});
|
||||||
}, [open, documents, fileName, form, editor]);
|
}, [open, documents, fileName, form, editor]);
|
||||||
|
|
||||||
const handleOk = async () => {
|
const handleOk = async () => {
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Button, Card, Form, Input, InputNumber, Space, Switch, Tabs, Typography, message } from 'antd';
|
||||||
|
import { CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||||
|
import { useEditor, EditorContent } from '@tiptap/react';
|
||||||
|
import StarterKit from '@tiptap/starter-kit';
|
||||||
|
import Underline from '@tiptap/extension-underline';
|
||||||
|
import { userSettingsApi, type UserSettingsData } from '../api/userSettings';
|
||||||
|
|
||||||
|
const { Title } = Typography;
|
||||||
|
|
||||||
|
function TiptapToolbar({ editor }: { editor: ReturnType<typeof useEditor> }) {
|
||||||
|
if (!editor) return null;
|
||||||
|
const btn = (active: boolean): React.CSSProperties => ({
|
||||||
|
padding: '2px 8px',
|
||||||
|
border: '1px solid #d9d9d9',
|
||||||
|
borderRadius: 4,
|
||||||
|
cursor: 'pointer',
|
||||||
|
background: active ? '#e6f4ff' : '#fff',
|
||||||
|
fontWeight: active ? 600 : 400,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', gap: 4, padding: '4px 0', borderBottom: '1px solid #f0f0f0', marginBottom: 6, flexWrap: 'wrap' }}>
|
||||||
|
<button style={btn(editor.isActive('bold'))} onMouseDown={(e) => { e.preventDefault(); editor.chain().focus().toggleBold().run(); }}>F</button>
|
||||||
|
<button style={{ ...btn(editor.isActive('italic')), fontStyle: 'italic' }} onMouseDown={(e) => { e.preventDefault(); editor.chain().focus().toggleItalic().run(); }}>K</button>
|
||||||
|
<button style={{ ...btn(editor.isActive('underline')), textDecoration: 'underline' }} onMouseDown={(e) => { e.preventDefault(); editor.chain().focus().toggleUnderline().run(); }}>U</button>
|
||||||
|
<button style={btn(editor.isActive('bulletList'))} onMouseDown={(e) => { e.preventDefault(); editor.chain().focus().toggleBulletList().run(); }}>• Liste</button>
|
||||||
|
<button style={btn(editor.isActive('orderedList'))} onMouseDown={(e) => { e.preventDefault(); editor.chain().focus().toggleOrderedList().run(); }}>1. Liste</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MailSettingsTab() {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [testing, setTesting] = useState(false);
|
||||||
|
const [testResult, setTestResult] = useState<{ ok: boolean; error?: string } | null>(null);
|
||||||
|
const [passSet, setPassSet] = useState(false);
|
||||||
|
|
||||||
|
const editor = useEditor({
|
||||||
|
extensions: [StarterKit, Underline],
|
||||||
|
content: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
userSettingsApi.get().then((data) => {
|
||||||
|
form.setFieldsValue({
|
||||||
|
smtpHost: data.smtpHost ?? '',
|
||||||
|
smtpPort: data.smtpPort ?? 587,
|
||||||
|
smtpSecure: data.smtpSecure,
|
||||||
|
smtpUser: data.smtpUser ?? '',
|
||||||
|
smtpFrom: data.smtpFrom ?? '',
|
||||||
|
});
|
||||||
|
setPassSet(data.smtpPassSet);
|
||||||
|
if (data.mailSignatureHtml) {
|
||||||
|
editor?.commands.setContent(data.mailSignatureHtml);
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
message.error('Einstellungen konnten nicht geladen werden');
|
||||||
|
}).finally(() => setLoading(false));
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
const values = await form.validateFields();
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
const updated = await userSettingsApi.update({
|
||||||
|
smtpHost: values.smtpHost || null,
|
||||||
|
smtpPort: values.smtpPort || null,
|
||||||
|
smtpSecure: values.smtpSecure ?? false,
|
||||||
|
smtpUser: values.smtpUser || null,
|
||||||
|
smtpPass: values.smtpPass || undefined,
|
||||||
|
smtpFrom: values.smtpFrom || null,
|
||||||
|
mailSignatureHtml: editor?.getHTML() ?? null,
|
||||||
|
});
|
||||||
|
setPassSet(updated.smtpPassSet);
|
||||||
|
form.setFieldValue('smtpPass', '');
|
||||||
|
message.success('Einstellungen gespeichert');
|
||||||
|
} catch {
|
||||||
|
message.error('Speichern fehlgeschlagen');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTest = async () => {
|
||||||
|
const values = form.getFieldsValue();
|
||||||
|
if (!values.smtpHost) { message.warning('Bitte SMTP-Server angeben'); return; }
|
||||||
|
setTesting(true);
|
||||||
|
setTestResult(null);
|
||||||
|
try {
|
||||||
|
const result = await userSettingsApi.testSmtp({
|
||||||
|
host: values.smtpHost,
|
||||||
|
port: values.smtpPort ?? 587,
|
||||||
|
secure: values.smtpSecure ?? false,
|
||||||
|
user: values.smtpUser ?? '',
|
||||||
|
pass: values.smtpPass || '',
|
||||||
|
});
|
||||||
|
setTestResult(result);
|
||||||
|
} catch {
|
||||||
|
setTestResult({ ok: false, error: 'Verbindung fehlgeschlagen' });
|
||||||
|
} finally {
|
||||||
|
setTesting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form form={form} layout="vertical" style={{ maxWidth: 600 }}>
|
||||||
|
<Form.Item name="smtpHost" label="SMTP Server">
|
||||||
|
<Input placeholder="smtp.beispiel.de" />
|
||||||
|
</Form.Item>
|
||||||
|
<Space style={{ width: '100%' }} align="start">
|
||||||
|
<Form.Item name="smtpPort" label="Port" style={{ width: 120 }}>
|
||||||
|
<InputNumber min={1} max={65535} style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="smtpSecure" label="TLS/SSL" valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Space>
|
||||||
|
<Form.Item name="smtpUser" label="Benutzername">
|
||||||
|
<Input autoComplete="off" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="smtpPass" label="Passwort">
|
||||||
|
<Input.Password placeholder={passSet ? '(bereits gesetzt — leer lassen zum Beibehalten)' : ''} autoComplete="new-password" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="smtpFrom" label="Absender-Adresse (From)">
|
||||||
|
<Input placeholder="mein.name@beispiel.de" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="Signatur">
|
||||||
|
<div style={{ border: '1px solid #d9d9d9', borderRadius: 6, padding: '6px 10px', minHeight: 160 }}>
|
||||||
|
<TiptapToolbar editor={editor} />
|
||||||
|
<EditorContent editor={editor} style={{ minHeight: 120, outline: 'none' }} />
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" loading={saving} onClick={handleSave}>
|
||||||
|
Speichern
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
loading={testing}
|
||||||
|
icon={testing ? <LoadingOutlined /> : undefined}
|
||||||
|
onClick={handleTest}
|
||||||
|
>
|
||||||
|
Verbindung testen
|
||||||
|
</Button>
|
||||||
|
{testResult && (
|
||||||
|
<Space>
|
||||||
|
{testResult.ok
|
||||||
|
? <CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||||
|
: <CloseCircleOutlined style={{ color: '#ff4d4f' }} />}
|
||||||
|
<Typography.Text type={testResult.ok ? 'success' : 'danger'}>
|
||||||
|
{testResult.ok ? 'Verbindung erfolgreich' : (testResult.error ?? 'Fehler')}
|
||||||
|
</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UserSettingsPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Title level={3} style={{ marginBottom: 24 }}>Benutzereinstellungen</Title>
|
||||||
|
<Card>
|
||||||
|
<Tabs
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: 'mail',
|
||||||
|
label: 'Maileinstellungen',
|
||||||
|
children: <MailSettingsTab />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user