252 lines
8.5 KiB
TypeScript
252 lines
8.5 KiB
TypeScript
import { BadGatewayException, Injectable, Logger } from '@nestjs/common';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { Repository } from 'typeorm';
|
|
import { getShopBrandingPublicUrl, resolveShopBrandingPath } from '../../../config/uploadPaths';
|
|
import { Config } from '../../../types/Config';
|
|
import { removeFileFromDisk } from '../../../utils/removeFileFromDisk';
|
|
import { SimplexChatClient } from '../../simplex/services/SimplexChatClient';
|
|
import { UpdateNotificationsDto } from '../dto/UpdateNotificationsDto';
|
|
import { UpdateShippingNoteDto } from '../dto/UpdateShippingNoteDto';
|
|
import { UpdateSimplexLinkDto } from '../dto/UpdateSimplexLinkDto';
|
|
import { ShopSettings } from '../entities/ShopSettings';
|
|
import { SetupChecklist } from '../types/SetupChecklist';
|
|
import { ShopSettingsView } from '../types/ShopSettingsView';
|
|
import { StorefrontBranding } from '../types/StorefrontBranding';
|
|
|
|
@Injectable()
|
|
export class ShopSettingsService {
|
|
private readonly logger = new Logger(ShopSettingsService.name);
|
|
|
|
private connectingSimplexPromise: Promise<ShopSettingsView> | null = null;
|
|
|
|
constructor(
|
|
@InjectRepository(ShopSettings)
|
|
private readonly shopSettingsRepo: Repository<ShopSettings>,
|
|
private readonly configService: ConfigService,
|
|
private readonly simplexChatClient: SimplexChatClient
|
|
) {}
|
|
|
|
async getView(): Promise<ShopSettingsView> {
|
|
const settings = await this.findSettings();
|
|
|
|
return this.toView(settings);
|
|
}
|
|
|
|
async getStorefrontBranding(): Promise<StorefrontBranding> {
|
|
const settings = await this.findSettings();
|
|
|
|
const logoUrl = settings.logoStorageKey ? getShopBrandingPublicUrl(settings.logoStorageKey) : null;
|
|
const faviconUrl = settings.faviconStorageKey ? getShopBrandingPublicUrl(settings.faviconStorageKey) : null;
|
|
|
|
return {
|
|
logoUrl,
|
|
faviconUrl,
|
|
simplexLink: settings.simplexLink,
|
|
shippingNote: settings.shippingNote
|
|
};
|
|
}
|
|
|
|
async findSettings(): Promise<ShopSettings> {
|
|
const [settings] = await this.shopSettingsRepo.find({ take: 1 });
|
|
|
|
if (!settings) {
|
|
throw new Error('Shop settings have not been initialized');
|
|
}
|
|
|
|
return settings;
|
|
}
|
|
|
|
async updateSimplexLink({ simplexLink }: UpdateSimplexLinkDto): Promise<ShopSettingsView> {
|
|
const settings = await this.findSettings();
|
|
|
|
await this.shopSettingsRepo.update(settings.id, {
|
|
simplexLink: simplexLink.trim()
|
|
});
|
|
|
|
return this.getView();
|
|
}
|
|
|
|
async updateShippingNote({ shippingNote }: UpdateShippingNoteDto): Promise<ShopSettingsView> {
|
|
const settings = await this.findSettings();
|
|
|
|
await this.shopSettingsRepo.update(settings.id, {
|
|
shippingNote: shippingNote.trim()
|
|
});
|
|
|
|
return this.getView();
|
|
}
|
|
|
|
async updateNotifications({
|
|
notificationsEnabled,
|
|
notifyOnNewOrder,
|
|
notifyOnOrderMessage
|
|
}: UpdateNotificationsDto): Promise<ShopSettingsView> {
|
|
const settings = await this.findSettings();
|
|
|
|
await this.shopSettingsRepo.update(settings.id, {
|
|
notificationsEnabled,
|
|
notifyOnNewOrder,
|
|
notifyOnOrderMessage
|
|
});
|
|
|
|
return this.getView();
|
|
}
|
|
|
|
async uploadLogo(filename: string): Promise<ShopSettingsView> {
|
|
const settings = await this.findSettings();
|
|
|
|
const previousLogoStorageKey = settings.logoStorageKey;
|
|
|
|
await this.shopSettingsRepo.update(settings.id, { logoStorageKey: filename });
|
|
|
|
if (previousLogoStorageKey) {
|
|
const previousLogoPath = resolveShopBrandingPath(previousLogoStorageKey);
|
|
|
|
await removeFileFromDisk(previousLogoPath, ShopSettingsService.name);
|
|
}
|
|
|
|
return this.getView();
|
|
}
|
|
|
|
async uploadFavicon(filename: string): Promise<ShopSettingsView> {
|
|
const settings = await this.findSettings();
|
|
|
|
const previousFaviconStorageKey = settings.faviconStorageKey;
|
|
|
|
await this.shopSettingsRepo.update(settings.id, { faviconStorageKey: filename });
|
|
|
|
if (previousFaviconStorageKey) {
|
|
const previousFaviconPath = resolveShopBrandingPath(previousFaviconStorageKey);
|
|
|
|
await removeFileFromDisk(previousFaviconPath, ShopSettingsService.name);
|
|
}
|
|
|
|
return this.getView();
|
|
}
|
|
|
|
async connectSimplexNotifications(simplexNotificationLink: string): Promise<ShopSettingsView> {
|
|
if (this.connectingSimplexPromise) {
|
|
return this.connectingSimplexPromise;
|
|
}
|
|
|
|
this.connectingSimplexPromise = this.runConnectSimplexNotifications(simplexNotificationLink).finally(() => {
|
|
this.connectingSimplexPromise = null;
|
|
});
|
|
|
|
return this.connectingSimplexPromise;
|
|
}
|
|
|
|
private async runConnectSimplexNotifications(simplexNotificationLink: string): Promise<ShopSettingsView> {
|
|
const trimmedLink = simplexNotificationLink.trim();
|
|
|
|
try {
|
|
await this.updateSimplexNotificationLink(trimmedLink);
|
|
|
|
const contactId = await this.simplexChatClient.connect(trimmedLink);
|
|
|
|
await this.setSimplexNotificationContactId(contactId);
|
|
|
|
this.logger.log(`SimpleX notification contact ready (id=${contactId})`);
|
|
|
|
return this.getView();
|
|
} catch {
|
|
await this.setSimplexNotificationContactId(null);
|
|
|
|
this.logger.warn('Failed to connect SimpleX notification contact');
|
|
|
|
throw new BadGatewayException('Failed to connect to SimpleX');
|
|
}
|
|
}
|
|
|
|
async updateSimplexNotificationLink(simplexNotificationLink: string): Promise<void> {
|
|
const settings = await this.findSettings();
|
|
|
|
const trimmed = simplexNotificationLink.trim();
|
|
|
|
const linkChanged = settings.simplexNotificationLink !== trimmed;
|
|
|
|
await this.shopSettingsRepo.update(settings.id, {
|
|
simplexNotificationLink: trimmed,
|
|
...(linkChanged ? { simplexNotificationContactId: null } : {})
|
|
});
|
|
}
|
|
|
|
private async setSimplexNotificationContactId(contactId: number | null): Promise<void> {
|
|
const settings = await this.findSettings();
|
|
|
|
await this.shopSettingsRepo.update(settings.id, {
|
|
simplexNotificationContactId: contactId
|
|
});
|
|
}
|
|
|
|
private toView({
|
|
id,
|
|
logoStorageKey,
|
|
faviconStorageKey,
|
|
simplexLink,
|
|
simplexNotificationLink,
|
|
shippingNote,
|
|
notificationsEnabled,
|
|
notifyOnNewOrder,
|
|
notifyOnOrderMessage,
|
|
simplexNotificationContactId,
|
|
createdAt,
|
|
updatedAt
|
|
}: ShopSettings): ShopSettingsView {
|
|
const setupChecklist = this.buildSetupChecklist({ logoStorageKey, faviconStorageKey, simplexLink, shippingNote });
|
|
|
|
const { shopName, shopFiatCurrency, monero, bitcoin } = this.configService.get(
|
|
'shopSettings'
|
|
) as Config['shopSettings'];
|
|
|
|
const logoUrl = logoStorageKey ? getShopBrandingPublicUrl(logoStorageKey) : null;
|
|
const faviconUrl = faviconStorageKey ? getShopBrandingPublicUrl(faviconStorageKey) : null;
|
|
|
|
const isSetupComplete = this.isSetupComplete(setupChecklist);
|
|
|
|
return {
|
|
id,
|
|
shopName,
|
|
shopFiatCurrency,
|
|
monero,
|
|
bitcoin,
|
|
logoUrl,
|
|
faviconUrl,
|
|
simplexLink,
|
|
simplexNotificationLink,
|
|
shippingNote,
|
|
notificationsEnabled,
|
|
notifyOnNewOrder,
|
|
notifyOnOrderMessage,
|
|
simplexNotificationConnected: simplexNotificationContactId !== null,
|
|
isSetupComplete,
|
|
setupChecklist,
|
|
createdAt,
|
|
updatedAt
|
|
};
|
|
}
|
|
|
|
private buildSetupChecklist({
|
|
logoStorageKey,
|
|
faviconStorageKey,
|
|
simplexLink,
|
|
shippingNote
|
|
}: Pick<ShopSettings, 'logoStorageKey' | 'faviconStorageKey' | 'simplexLink' | 'shippingNote'>): SetupChecklist {
|
|
const {
|
|
validation: { shippingNoteMinLength }
|
|
} = this.configService.get('app') as Config['app'];
|
|
|
|
return {
|
|
logo: logoStorageKey !== null,
|
|
favicon: faviconStorageKey !== null,
|
|
simplexLink: (simplexLink?.length ?? 0) > 0,
|
|
shippingNote: (shippingNote?.length ?? 0) >= shippingNoteMinLength
|
|
};
|
|
}
|
|
|
|
private isSetupComplete(checklist: SetupChecklist): boolean {
|
|
return checklist.logo && checklist.favicon && checklist.simplexLink && checklist.shippingNote;
|
|
}
|
|
}
|