dad0136365
Build and Push Multi-Platform Images / build-and-push (push) Successful in 41s
Reformats code style (line breaks, indentation, type annotations) without changing logic. Also includes minor feature additions bundled in the same lint run (stats service, user-settings groups, agrarmonitor polling improvements). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
243 lines
7.9 KiB
TypeScript
243 lines
7.9 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import { Repository } from 'typeorm';
|
|
import * as nodemailer from 'nodemailer';
|
|
import * as crypto from 'crypto';
|
|
import { UserSettings } from '../database/entities/user-settings.entity';
|
|
|
|
const ALGORITHM = 'aes-256-gcm';
|
|
|
|
export interface UserSettingsDto {
|
|
smtpHost: string | null;
|
|
smtpPort: number | null;
|
|
smtpSecure: boolean;
|
|
smtpUser: string | null;
|
|
smtpPassSet: boolean;
|
|
smtpFrom: string | null;
|
|
smtpFromName: string | null;
|
|
mailSignatureHtml: string | null;
|
|
defaultLabelTemplateId: number | null;
|
|
emailRecipientHistory: string[] | null;
|
|
dailyDigestEnabled: boolean;
|
|
}
|
|
|
|
@Injectable()
|
|
export class UserSettingsService {
|
|
private readonly logger = new Logger(UserSettingsService.name);
|
|
private readonly encKey: Buffer | null;
|
|
|
|
constructor(
|
|
@InjectRepository(UserSettings)
|
|
private readonly repo: Repository<UserSettings>,
|
|
private readonly configService: ConfigService,
|
|
) {
|
|
const raw = this.configService.get<string>('SMTP_ENCRYPTION_KEY', '');
|
|
if (raw && raw.length === 64) {
|
|
this.encKey = Buffer.from(raw, 'hex');
|
|
} else {
|
|
this.encKey = null;
|
|
this.logger.warn(
|
|
'SMTP_ENCRYPTION_KEY fehlt oder hat falsche Länge (64 Hex-Zeichen = 32 Bytes erforderlich). Passwörter werden im Klartext gespeichert.',
|
|
);
|
|
}
|
|
}
|
|
|
|
private encrypt(plaintext: string): string {
|
|
if (!this.encKey) return plaintext;
|
|
const iv = crypto.randomBytes(16);
|
|
const cipher = crypto.createCipheriv(ALGORITHM, this.encKey, iv);
|
|
const encrypted = Buffer.concat([
|
|
cipher.update(plaintext, 'utf8'),
|
|
cipher.final(),
|
|
]);
|
|
const authTag = cipher.getAuthTag();
|
|
return `enc:${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted.toString('hex')}`;
|
|
}
|
|
|
|
private decrypt(stored: string): string {
|
|
if (!this.encKey || !stored.startsWith('enc:')) return stored;
|
|
const parts = stored.split(':');
|
|
if (parts.length !== 4) return stored;
|
|
const [, ivHex, authTagHex, encryptedHex] = parts;
|
|
const iv = Buffer.from(ivHex, 'hex');
|
|
const authTag = Buffer.from(authTagHex, 'hex');
|
|
const encrypted = Buffer.from(encryptedHex, 'hex');
|
|
const decipher = crypto.createDecipheriv(ALGORITHM, this.encKey, iv);
|
|
decipher.setAuthTag(authTag);
|
|
return Buffer.concat([
|
|
decipher.update(encrypted),
|
|
decipher.final(),
|
|
]).toString('utf8');
|
|
}
|
|
|
|
async getSettings(
|
|
userId: string,
|
|
email?: string,
|
|
preferredUsername?: string,
|
|
groups?: string[],
|
|
): Promise<UserSettingsDto> {
|
|
let entity = await this.repo.findOne({ where: { UserId: userId } });
|
|
if (email || preferredUsername || groups) {
|
|
if (!entity) {
|
|
entity = this.repo.create({ UserId: userId });
|
|
}
|
|
if (email) entity.UserEmail = email;
|
|
if (preferredUsername) entity.UserPreferredUsername = preferredUsername;
|
|
if (groups) entity.UserGroups = groups;
|
|
await this.repo.save(entity);
|
|
}
|
|
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;
|
|
smtpFromName?: string | null;
|
|
mailSignatureHtml?: string | null;
|
|
defaultLabelTemplateId?: number | null;
|
|
emailRecipientHistory?: string[] | null;
|
|
dailyDigestEnabled?: boolean;
|
|
},
|
|
email?: string,
|
|
preferredUsername?: string,
|
|
groups?: string[],
|
|
): 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 = this.encrypt(data.smtpPass);
|
|
}
|
|
if (data.smtpFrom !== undefined) entity.SmtpFrom = data.smtpFrom;
|
|
if (data.smtpFromName !== undefined)
|
|
entity.SmtpFromName = data.smtpFromName;
|
|
if (data.mailSignatureHtml !== undefined)
|
|
entity.MailSignatureHtml = data.mailSignatureHtml;
|
|
if (data.defaultLabelTemplateId !== undefined)
|
|
entity.DefaultLabelTemplateId = data.defaultLabelTemplateId;
|
|
if (data.emailRecipientHistory !== undefined)
|
|
entity.EmailRecipientHistory = data.emailRecipientHistory;
|
|
if (data.dailyDigestEnabled !== undefined)
|
|
entity.DailyDigestEnabled = data.dailyDigestEnabled;
|
|
if (email) entity.UserEmail = email;
|
|
if (preferredUsername) entity.UserPreferredUsername = preferredUsername;
|
|
if (groups) entity.UserGroups = groups;
|
|
|
|
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;
|
|
const fromEmail = entity.SmtpFrom ?? entity.SmtpUser ?? '';
|
|
const from = entity.SmtpFromName
|
|
? `"${entity.SmtpFromName}" <${fromEmail}>`
|
|
: fromEmail;
|
|
return {
|
|
host: entity.SmtpHost,
|
|
port: entity.SmtpPort ?? 587,
|
|
secure: entity.SmtpSecure,
|
|
user: entity.SmtpUser ?? '',
|
|
pass: this.decrypt(entity.SmtpPass),
|
|
from,
|
|
};
|
|
}
|
|
|
|
async findAllDigestSubscribers(): Promise<UserSettings[]> {
|
|
return this.repo
|
|
.find({
|
|
where: { DailyDigestEnabled: true },
|
|
})
|
|
.then((rows) => rows.filter((r) => !!r.UserEmail));
|
|
}
|
|
|
|
async getAvailableSenders(
|
|
userId: string,
|
|
): Promise<{ id: string; label: string }[]> {
|
|
const defaultEmail = this.configService.get<string>(
|
|
'SMTP_FROM',
|
|
'paperless@localhost',
|
|
);
|
|
const defaultName = this.configService.get<string>('SMTP_FROM_NAME', '');
|
|
const defaultLabel = defaultName
|
|
? `${defaultName} <${defaultEmail}>`
|
|
: defaultEmail;
|
|
const senders: { id: string; label: string }[] = [
|
|
{ id: 'default', label: defaultLabel },
|
|
];
|
|
|
|
const entity = await this.repo.findOne({ where: { UserId: userId } });
|
|
if (entity?.SmtpHost && entity?.SmtpPass) {
|
|
const userEmail = entity.SmtpFrom ?? entity.SmtpUser ?? '';
|
|
const userLabel = entity.SmtpFromName
|
|
? `${entity.SmtpFromName} <${userEmail}>`
|
|
: userEmail;
|
|
senders.push({ id: 'user', label: userLabel });
|
|
}
|
|
|
|
return senders;
|
|
}
|
|
|
|
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,
|
|
smtpFromName: entity?.SmtpFromName ?? null,
|
|
mailSignatureHtml: entity?.MailSignatureHtml ?? null,
|
|
defaultLabelTemplateId: entity?.DefaultLabelTemplateId ?? null,
|
|
emailRecipientHistory: entity?.EmailRecipientHistory ?? null,
|
|
dailyDigestEnabled: entity?.DailyDigestEnabled ?? false,
|
|
};
|
|
}
|
|
}
|