110 lines
3.9 KiB
TypeScript
110 lines
3.9 KiB
TypeScript
import type { FastifyInstance } from 'fastify';
|
|
import type pg from 'pg';
|
|
import { z } from 'zod';
|
|
import type { Authenticate } from '../../../shared/auth.js';
|
|
import { requireRole } from '../../../shared/auth.js';
|
|
import { parseJson } from '../../../shared/http-input.js';
|
|
import { AppError } from '../../../shared/errors.js';
|
|
|
|
export interface StoreSettingsRoutesDeps {
|
|
pool: pg.Pool;
|
|
authenticate: Authenticate;
|
|
}
|
|
|
|
const updateSettingsSchema = z.object({
|
|
storeName: z.string().min(1).max(120).optional(),
|
|
storeTagline: z.string().max(200).optional(),
|
|
contactEmail: z.string().email().optional(),
|
|
contactPhone: z.string().max(40).optional(),
|
|
contactAddress: z.string().max(400).optional(),
|
|
footerText: z.string().max(400).optional(),
|
|
facebookUrl: z.string().url().optional().or(z.literal('')),
|
|
instagramUrl: z.string().url().optional().or(z.literal('')),
|
|
});
|
|
|
|
const SETTING_KEYS: Record<string, string> = {
|
|
storeName: 'store_name',
|
|
storeTagline: 'store_tagline',
|
|
contactEmail: 'contact_email',
|
|
contactPhone: 'contact_phone',
|
|
contactAddress: 'contact_address',
|
|
footerText: 'footer_text',
|
|
facebookUrl: 'facebook_url',
|
|
instagramUrl: 'instagram_url',
|
|
};
|
|
|
|
export async function registerStoreSettingsRoutes(
|
|
app: FastifyInstance,
|
|
deps: StoreSettingsRoutesDeps,
|
|
): Promise<void> {
|
|
// GET /admin/settings — fetch all settings
|
|
app.get('/admin/settings', async (request, reply) => {
|
|
const user = await deps.authenticate(request);
|
|
requireRole(user, 'admin');
|
|
const result = await deps.pool.query<{ key: string; value: string }>(
|
|
`SELECT key, value FROM store_settings`,
|
|
);
|
|
const map: Record<string, string> = {};
|
|
for (const row of result.rows) {
|
|
map[row.key] = row.value;
|
|
}
|
|
return reply.send({
|
|
storeName: map['store_name'] ?? '',
|
|
storeTagline: map['store_tagline'] ?? '',
|
|
contactEmail: map['contact_email'] ?? '',
|
|
contactPhone: map['contact_phone'] ?? '',
|
|
contactAddress: map['contact_address'] ?? '',
|
|
footerText: map['footer_text'] ?? '',
|
|
facebookUrl: map['facebook_url'] ?? '',
|
|
instagramUrl: map['instagram_url'] ?? '',
|
|
});
|
|
});
|
|
|
|
// PATCH /admin/settings — update one or more settings
|
|
app.patch('/admin/settings', async (request, reply) => {
|
|
const user = await deps.authenticate(request);
|
|
requireRole(user, 'admin');
|
|
const input = parseJson(updateSettingsSchema, request.body);
|
|
const updates: string[] = [];
|
|
const values: unknown[] = [];
|
|
let i = 1;
|
|
for (const [field, dbKey] of Object.entries(SETTING_KEYS)) {
|
|
const val = input[field as keyof typeof input];
|
|
if (val !== undefined) {
|
|
updates.push(`key = CASE WHEN key = $${i} THEN $${i + 1} ELSE key END`);
|
|
values.push(dbKey, val);
|
|
i += 2;
|
|
}
|
|
}
|
|
if (updates.length > 0) {
|
|
// Use INSERT ... ON CONFLICT for each key
|
|
for (const [field, dbKey] of Object.entries(SETTING_KEYS)) {
|
|
const val = input[field as keyof typeof input];
|
|
if (val !== undefined) {
|
|
await deps.pool.query(
|
|
`INSERT INTO store_settings (key, value, updated_by) VALUES ($1, $2, $3)
|
|
ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW(), updated_by = $3`,
|
|
[dbKey, val, user.id],
|
|
);
|
|
}
|
|
}
|
|
}
|
|
// Return updated settings
|
|
const result = await deps.pool.query<{ key: string; value: string }>(
|
|
`SELECT key, value FROM store_settings`,
|
|
);
|
|
const map: Record<string, string> = {};
|
|
for (const row of result.rows) { map[row.key] = row.value; }
|
|
return reply.send({
|
|
storeName: map['store_name'] ?? '',
|
|
storeTagline: map['store_tagline'] ?? '',
|
|
contactEmail: map['contact_email'] ?? '',
|
|
contactPhone: map['contact_phone'] ?? '',
|
|
contactAddress: map['contact_address'] ?? '',
|
|
footerText: map['footer_text'] ?? '',
|
|
facebookUrl: map['facebook_url'] ?? '',
|
|
instagramUrl: map['instagram_url'] ?? '',
|
|
});
|
|
});
|
|
}
|