feat(ADM-018): completed feature
This commit is contained in:
109
project/src/modules/store-settings/api/settings.routes.ts
Normal file
109
project/src/modules/store-settings/api/settings.routes.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
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'] ?? '',
|
||||
});
|
||||
});
|
||||
}
|
||||
17
project/src/modules/store-settings/domain/settings.ts
Normal file
17
project/src/modules/store-settings/domain/settings.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export interface StoreSetting {
|
||||
key: string;
|
||||
value: string;
|
||||
updatedAt: Date;
|
||||
updatedBy: string | null;
|
||||
}
|
||||
|
||||
export interface StoreSettings {
|
||||
storeName: string;
|
||||
storeTagline: string;
|
||||
contactEmail: string;
|
||||
contactPhone: string;
|
||||
contactAddress: string;
|
||||
footerText: string;
|
||||
facebookUrl: string;
|
||||
instagramUrl: string;
|
||||
}
|
||||
2
project/src/modules/store-settings/index.ts
Normal file
2
project/src/modules/store-settings/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { registerStoreSettingsRoutes } from './api/settings.routes.js';
|
||||
export type { StoreSettings, StoreSetting } from './domain/settings.js';
|
||||
Reference in New Issue
Block a user