feat(backend): initial setup for cms backend service
This commit is contained in:
164
backend/src/routes/auth.ts
Normal file
164
backend/src/routes/auth.ts
Normal file
@ -0,0 +1,164 @@
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../config/database.js';
|
||||
import { users } from '../db/schema.js';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { GiteaService } from '../services/gitea.service.js';
|
||||
import { env } from '../config/env.js';
|
||||
|
||||
const callbackSchema = z.object({
|
||||
code: z.string(),
|
||||
state: z.string(),
|
||||
});
|
||||
|
||||
const authRoute: FastifyPluginAsync = async (fastify) => {
|
||||
const giteaService = new GiteaService();
|
||||
|
||||
/**
|
||||
* GET /auth/gitea
|
||||
* Initiate OAuth flow
|
||||
*/
|
||||
fastify.get('/auth/gitea', async (request, reply) => {
|
||||
// Generate CSRF state token
|
||||
const state = giteaService.generateState();
|
||||
|
||||
// Store state in session
|
||||
request.session.set('oauth_state', state);
|
||||
|
||||
// Generate authorization URL
|
||||
const authUrl = giteaService.getAuthorizationUrl(state);
|
||||
|
||||
// Redirect to Gitea
|
||||
return reply.redirect(authUrl);
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /auth/callback
|
||||
* OAuth callback endpoint
|
||||
*/
|
||||
fastify.get('/auth/callback', {
|
||||
schema: {
|
||||
querystring: callbackSchema,
|
||||
},
|
||||
}, async (request, reply) => {
|
||||
try {
|
||||
const { code, state } = request.query as z.infer<typeof callbackSchema>;
|
||||
|
||||
// Verify CSRF state
|
||||
const expectedState = request.session.get('oauth_state');
|
||||
if (!expectedState || state !== expectedState) {
|
||||
return reply.code(400).send({ error: 'Invalid state parameter' });
|
||||
}
|
||||
|
||||
// Clear state from session
|
||||
request.session.delete('oauth_state');
|
||||
|
||||
// Exchange code for access token
|
||||
const tokenResponse = await giteaService.exchangeCodeForToken(code);
|
||||
|
||||
// Fetch user info from Gitea
|
||||
const giteaUser = await giteaService.getUserInfo(tokenResponse.access_token);
|
||||
|
||||
// Check if user is allowed
|
||||
if (!giteaService.isUserAllowed(giteaUser.login)) {
|
||||
return reply.code(403).send({
|
||||
error: 'Access denied. You are not authorized to access this CMS.'
|
||||
});
|
||||
}
|
||||
|
||||
// Find or create user in database
|
||||
let [user] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.giteaId, giteaUser.id.toString()))
|
||||
.limit(1);
|
||||
|
||||
if (!user) {
|
||||
// Create new user
|
||||
[user] = await db.insert(users).values({
|
||||
giteaId: giteaUser.id.toString(),
|
||||
giteaUsername: giteaUser.login,
|
||||
giteaEmail: giteaUser.email,
|
||||
displayName: giteaUser.full_name,
|
||||
avatarUrl: giteaUser.avatar_url,
|
||||
lastLogin: new Date(),
|
||||
}).returning();
|
||||
} else {
|
||||
// Update existing user
|
||||
[user] = await db
|
||||
.update(users)
|
||||
.set({
|
||||
giteaUsername: giteaUser.login,
|
||||
giteaEmail: giteaUser.email,
|
||||
displayName: giteaUser.full_name,
|
||||
avatarUrl: giteaUser.avatar_url,
|
||||
lastLogin: new Date(),
|
||||
})
|
||||
.where(eq(users.id, user.id))
|
||||
.returning();
|
||||
}
|
||||
|
||||
// Generate JWT for session management
|
||||
const token = fastify.jwt.sign(
|
||||
{
|
||||
id: user.id,
|
||||
giteaId: user.giteaId,
|
||||
username: user.giteaUsername,
|
||||
role: user.role,
|
||||
},
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
// Redirect to frontend with token
|
||||
const frontendUrl = env.FRONTEND_URL;
|
||||
return reply.redirect(`${frontendUrl}/auth/callback?token=${token}`);
|
||||
|
||||
} catch (error) {
|
||||
fastify.log.error('OAuth callback error:', error);
|
||||
return reply.code(500).send({ error: 'Authentication failed' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /auth/me
|
||||
* Get current user info
|
||||
*/
|
||||
fastify.get('/auth/me', {
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const userId = request.user.id;
|
||||
|
||||
const [user] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, userId))
|
||||
.limit(1);
|
||||
|
||||
if (!user) {
|
||||
return reply.code(404).send({ error: 'User not found' });
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.giteaUsername,
|
||||
email: user.giteaEmail,
|
||||
displayName: user.displayName,
|
||||
avatarUrl: user.avatarUrl,
|
||||
role: user.role,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /auth/logout
|
||||
* Logout (client-side token deletion)
|
||||
*/
|
||||
fastify.post('/auth/logout', {
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
// For JWT, logout is primarily client-side (delete token)
|
||||
// You could maintain a token blacklist in Redis for production
|
||||
return { message: 'Logged out successfully' };
|
||||
});
|
||||
};
|
||||
|
||||
export default authRoute;
|
||||
99
backend/src/routes/content.ts
Normal file
99
backend/src/routes/content.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../config/database.js';
|
||||
import { contentSections } from '../db/schema.js';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
const contentSectionSchema = z.object({
|
||||
contentJson: z.record(z.any()),
|
||||
});
|
||||
|
||||
const contentRoute: FastifyPluginAsync = async (fastify) => {
|
||||
|
||||
// Get content section
|
||||
fastify.get('/content/:section', {
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const { section } = request.params as { section: string };
|
||||
|
||||
const [content] = await db
|
||||
.select()
|
||||
.from(contentSections)
|
||||
.where(eq(contentSections.sectionName, section))
|
||||
.limit(1);
|
||||
|
||||
if (!content) {
|
||||
return reply.code(404).send({ error: 'Content section not found' });
|
||||
}
|
||||
|
||||
return {
|
||||
section: content.sectionName,
|
||||
content: content.contentJson,
|
||||
updatedAt: content.updatedAt,
|
||||
};
|
||||
});
|
||||
|
||||
// Update content section
|
||||
fastify.put('/content/:section', {
|
||||
schema: {
|
||||
body: contentSectionSchema,
|
||||
},
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const { section } = request.params as { section: string };
|
||||
const { contentJson } = request.body as z.infer<typeof contentSectionSchema>;
|
||||
|
||||
// Check if section exists
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(contentSections)
|
||||
.where(eq(contentSections.sectionName, section))
|
||||
.limit(1);
|
||||
|
||||
let result;
|
||||
|
||||
if (existing) {
|
||||
// Update existing
|
||||
[result] = await db
|
||||
.update(contentSections)
|
||||
.set({
|
||||
contentJson,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(contentSections.sectionName, section))
|
||||
.returning();
|
||||
} else {
|
||||
// Create new
|
||||
[result] = await db
|
||||
.insert(contentSections)
|
||||
.values({
|
||||
sectionName: section,
|
||||
contentJson,
|
||||
})
|
||||
.returning();
|
||||
}
|
||||
|
||||
return {
|
||||
section: result.sectionName,
|
||||
content: result.contentJson,
|
||||
updatedAt: result.updatedAt,
|
||||
};
|
||||
});
|
||||
|
||||
// List all content sections
|
||||
fastify.get('/content', {
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const sections = await db.select().from(contentSections);
|
||||
|
||||
return {
|
||||
sections: sections.map(s => ({
|
||||
section: s.sectionName,
|
||||
content: s.contentJson,
|
||||
updatedAt: s.updatedAt,
|
||||
})),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export default contentRoute;
|
||||
123
backend/src/routes/events.ts
Normal file
123
backend/src/routes/events.ts
Normal file
@ -0,0 +1,123 @@
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../config/database.js';
|
||||
import { events } from '../db/schema.js';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
const eventSchema = z.object({
|
||||
title: z.string().min(1).max(200),
|
||||
date: z.string().min(1).max(100),
|
||||
description: z.string().min(1),
|
||||
imageUrl: z.string().url(),
|
||||
displayOrder: z.number().int().min(0),
|
||||
isPublished: z.boolean().optional().default(true),
|
||||
});
|
||||
|
||||
const eventsRoute: FastifyPluginAsync = async (fastify) => {
|
||||
|
||||
// List all events
|
||||
fastify.get('/events', {
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const allEvents = await db.select().from(events).orderBy(events.displayOrder);
|
||||
return { events: allEvents };
|
||||
});
|
||||
|
||||
// Get single event
|
||||
fastify.get('/events/:id', {
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const event = await db.select().from(events).where(eq(events.id, id)).limit(1);
|
||||
|
||||
if (event.length === 0) {
|
||||
return reply.code(404).send({ error: 'Event not found' });
|
||||
}
|
||||
|
||||
return { event: event[0] };
|
||||
});
|
||||
|
||||
// Create event
|
||||
fastify.post('/events', {
|
||||
schema: {
|
||||
body: eventSchema,
|
||||
},
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const data = request.body as z.infer<typeof eventSchema>;
|
||||
|
||||
const [newEvent] = await db.insert(events).values(data).returning();
|
||||
|
||||
return reply.code(201).send({ event: newEvent });
|
||||
});
|
||||
|
||||
// Update event
|
||||
fastify.put('/events/:id', {
|
||||
schema: {
|
||||
body: eventSchema,
|
||||
},
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const data = request.body as z.infer<typeof eventSchema>;
|
||||
|
||||
const [updated] = await db
|
||||
.update(events)
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
.where(eq(events.id, id))
|
||||
.returning();
|
||||
|
||||
if (!updated) {
|
||||
return reply.code(404).send({ error: 'Event not found' });
|
||||
}
|
||||
|
||||
return { event: updated };
|
||||
});
|
||||
|
||||
// Delete event
|
||||
fastify.delete('/events/:id', {
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
|
||||
const [deleted] = await db
|
||||
.delete(events)
|
||||
.where(eq(events.id, id))
|
||||
.returning();
|
||||
|
||||
if (!deleted) {
|
||||
return reply.code(404).send({ error: 'Event not found' });
|
||||
}
|
||||
|
||||
return { message: 'Event deleted successfully' };
|
||||
});
|
||||
|
||||
// Reorder events
|
||||
fastify.put('/events/reorder', {
|
||||
schema: {
|
||||
body: z.object({
|
||||
orders: z.array(z.object({
|
||||
id: z.string().uuid(),
|
||||
displayOrder: z.number().int().min(0),
|
||||
})),
|
||||
}),
|
||||
},
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const { orders } = request.body as { orders: Array<{ id: string; displayOrder: number }> };
|
||||
|
||||
// Update all in transaction
|
||||
await db.transaction(async (tx) => {
|
||||
for (const { id, displayOrder } of orders) {
|
||||
await tx
|
||||
.update(events)
|
||||
.set({ displayOrder })
|
||||
.where(eq(events.id, id));
|
||||
}
|
||||
});
|
||||
|
||||
return { message: 'Events reordered successfully' };
|
||||
});
|
||||
};
|
||||
|
||||
export default eventsRoute;
|
||||
121
backend/src/routes/gallery.ts
Normal file
121
backend/src/routes/gallery.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../config/database.js';
|
||||
import { galleryImages } from '../db/schema.js';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
const galleryImageSchema = z.object({
|
||||
imageUrl: z.string().url(),
|
||||
altText: z.string().min(1).max(200),
|
||||
displayOrder: z.number().int().min(0),
|
||||
isPublished: z.boolean().optional().default(true),
|
||||
});
|
||||
|
||||
const galleryRoute: FastifyPluginAsync = async (fastify) => {
|
||||
|
||||
// List all gallery images
|
||||
fastify.get('/gallery', {
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const images = await db.select().from(galleryImages).orderBy(galleryImages.displayOrder);
|
||||
return { images };
|
||||
});
|
||||
|
||||
// Get single gallery image
|
||||
fastify.get('/gallery/:id', {
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const image = await db.select().from(galleryImages).where(eq(galleryImages.id, id)).limit(1);
|
||||
|
||||
if (image.length === 0) {
|
||||
return reply.code(404).send({ error: 'Image not found' });
|
||||
}
|
||||
|
||||
return { image: image[0] };
|
||||
});
|
||||
|
||||
// Create gallery image
|
||||
fastify.post('/gallery', {
|
||||
schema: {
|
||||
body: galleryImageSchema,
|
||||
},
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const data = request.body as z.infer<typeof galleryImageSchema>;
|
||||
|
||||
const [newImage] = await db.insert(galleryImages).values(data).returning();
|
||||
|
||||
return reply.code(201).send({ image: newImage });
|
||||
});
|
||||
|
||||
// Update gallery image
|
||||
fastify.put('/gallery/:id', {
|
||||
schema: {
|
||||
body: galleryImageSchema,
|
||||
},
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const data = request.body as z.infer<typeof galleryImageSchema>;
|
||||
|
||||
const [updated] = await db
|
||||
.update(galleryImages)
|
||||
.set(data)
|
||||
.where(eq(galleryImages.id, id))
|
||||
.returning();
|
||||
|
||||
if (!updated) {
|
||||
return reply.code(404).send({ error: 'Image not found' });
|
||||
}
|
||||
|
||||
return { image: updated };
|
||||
});
|
||||
|
||||
// Delete gallery image
|
||||
fastify.delete('/gallery/:id', {
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
|
||||
const [deleted] = await db
|
||||
.delete(galleryImages)
|
||||
.where(eq(galleryImages.id, id))
|
||||
.returning();
|
||||
|
||||
if (!deleted) {
|
||||
return reply.code(404).send({ error: 'Image not found' });
|
||||
}
|
||||
|
||||
return { message: 'Image deleted successfully' };
|
||||
});
|
||||
|
||||
// Reorder gallery images
|
||||
fastify.put('/gallery/reorder', {
|
||||
schema: {
|
||||
body: z.object({
|
||||
orders: z.array(z.object({
|
||||
id: z.string().uuid(),
|
||||
displayOrder: z.number().int().min(0),
|
||||
})),
|
||||
}),
|
||||
},
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const { orders } = request.body as { orders: Array<{ id: string; displayOrder: number }> };
|
||||
|
||||
// Update all in transaction
|
||||
await db.transaction(async (tx) => {
|
||||
for (const { id, displayOrder } of orders) {
|
||||
await tx
|
||||
.update(galleryImages)
|
||||
.set({ displayOrder })
|
||||
.where(eq(galleryImages.id, id));
|
||||
}
|
||||
});
|
||||
|
||||
return { message: 'Gallery images reordered successfully' };
|
||||
});
|
||||
};
|
||||
|
||||
export default galleryRoute;
|
||||
122
backend/src/routes/publish.ts
Normal file
122
backend/src/routes/publish.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { GitService } from '../services/git.service.js';
|
||||
import { FileGeneratorService } from '../services/file-generator.service.js';
|
||||
import { db } from '../config/database.js';
|
||||
import { events, galleryImages, contentSections, publishHistory } from '../db/schema.js';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
const publishSchema = z.object({
|
||||
commitMessage: z.string().min(1).max(200),
|
||||
});
|
||||
|
||||
const publishRoute: FastifyPluginAsync = async (fastify) => {
|
||||
fastify.post('/publish', {
|
||||
schema: {
|
||||
body: publishSchema,
|
||||
},
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
try {
|
||||
const { commitMessage } = request.body as z.infer<typeof publishSchema>;
|
||||
const userId = request.user.id;
|
||||
|
||||
fastify.log.info('Starting publish process...');
|
||||
|
||||
// Initialize git service
|
||||
const gitService = new GitService();
|
||||
await gitService.initialize();
|
||||
|
||||
fastify.log.info('Git repository initialized');
|
||||
|
||||
// Fetch all content from database
|
||||
const eventsData = await db
|
||||
.select()
|
||||
.from(events)
|
||||
.where(eq(events.isPublished, true))
|
||||
.orderBy(events.displayOrder);
|
||||
|
||||
const galleryData = await db
|
||||
.select()
|
||||
.from(galleryImages)
|
||||
.where(eq(galleryImages.isPublished, true))
|
||||
.orderBy(galleryImages.displayOrder);
|
||||
|
||||
const sectionsData = await db.select().from(contentSections);
|
||||
const sectionsMap = new Map(
|
||||
sectionsData.map(s => [s.sectionName, s.contentJson as any])
|
||||
);
|
||||
|
||||
fastify.log.info(`Fetched ${eventsData.length} events, ${galleryData.length} images, ${sectionsData.length} sections`);
|
||||
|
||||
// Generate and write files
|
||||
const fileGenerator = new FileGeneratorService();
|
||||
await fileGenerator.writeFiles(
|
||||
gitService.getWorkspacePath(''),
|
||||
eventsData.map(e => ({
|
||||
title: e.title,
|
||||
date: e.date,
|
||||
description: e.description,
|
||||
imageUrl: e.imageUrl,
|
||||
})),
|
||||
galleryData.map(g => ({
|
||||
imageUrl: g.imageUrl,
|
||||
altText: g.altText,
|
||||
})),
|
||||
sectionsMap
|
||||
);
|
||||
|
||||
fastify.log.info('Files generated successfully');
|
||||
|
||||
// Commit and push
|
||||
const commitHash = await gitService.commitAndPush(commitMessage);
|
||||
|
||||
fastify.log.info(`Changes committed: ${commitHash}`);
|
||||
|
||||
// Record in history
|
||||
await db.insert(publishHistory).values({
|
||||
userId,
|
||||
commitHash,
|
||||
commitMessage,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
commitHash,
|
||||
message: 'Changes published successfully',
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
fastify.log.error('Publish error:', error);
|
||||
|
||||
// Attempt to reset git state on error
|
||||
try {
|
||||
const gitService = new GitService();
|
||||
await gitService.reset();
|
||||
} catch (resetError) {
|
||||
fastify.log.error('Failed to reset git state:', resetError);
|
||||
}
|
||||
|
||||
return reply.code(500).send({
|
||||
success: false,
|
||||
error: 'Failed to publish changes',
|
||||
details: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get publish history
|
||||
fastify.get('/publish/history', {
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const history = await db
|
||||
.select()
|
||||
.from(publishHistory)
|
||||
.orderBy(publishHistory.publishedAt)
|
||||
.limit(20);
|
||||
|
||||
return { history };
|
||||
});
|
||||
};
|
||||
|
||||
export default publishRoute;
|
||||
116
backend/src/routes/settings.ts
Normal file
116
backend/src/routes/settings.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../config/database.js';
|
||||
import { siteSettings } from '../db/schema.js';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
const settingSchema = z.object({
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
const settingsRoute: FastifyPluginAsync = async (fastify) => {
|
||||
|
||||
// Get all settings
|
||||
fastify.get('/settings', {
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const settings = await db.select().from(siteSettings);
|
||||
|
||||
return {
|
||||
settings: settings.reduce((acc, setting) => {
|
||||
acc[setting.key] = setting.value;
|
||||
return acc;
|
||||
}, {} as Record<string, string>),
|
||||
};
|
||||
});
|
||||
|
||||
// Get single setting
|
||||
fastify.get('/settings/:key', {
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const { key } = request.params as { key: string };
|
||||
|
||||
const [setting] = await db
|
||||
.select()
|
||||
.from(siteSettings)
|
||||
.where(eq(siteSettings.key, key))
|
||||
.limit(1);
|
||||
|
||||
if (!setting) {
|
||||
return reply.code(404).send({ error: 'Setting not found' });
|
||||
}
|
||||
|
||||
return {
|
||||
key: setting.key,
|
||||
value: setting.value,
|
||||
updatedAt: setting.updatedAt,
|
||||
};
|
||||
});
|
||||
|
||||
// Update setting
|
||||
fastify.put('/settings/:key', {
|
||||
schema: {
|
||||
body: settingSchema,
|
||||
},
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const { key } = request.params as { key: string };
|
||||
const { value } = request.body as z.infer<typeof settingSchema>;
|
||||
|
||||
// Check if setting exists
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(siteSettings)
|
||||
.where(eq(siteSettings.key, key))
|
||||
.limit(1);
|
||||
|
||||
let result;
|
||||
|
||||
if (existing) {
|
||||
// Update existing
|
||||
[result] = await db
|
||||
.update(siteSettings)
|
||||
.set({
|
||||
value,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(siteSettings.key, key))
|
||||
.returning();
|
||||
} else {
|
||||
// Create new
|
||||
[result] = await db
|
||||
.insert(siteSettings)
|
||||
.values({
|
||||
key,
|
||||
value,
|
||||
})
|
||||
.returning();
|
||||
}
|
||||
|
||||
return {
|
||||
key: result.key,
|
||||
value: result.value,
|
||||
updatedAt: result.updatedAt,
|
||||
};
|
||||
});
|
||||
|
||||
// Delete setting
|
||||
fastify.delete('/settings/:key', {
|
||||
preHandler: [fastify.authenticate],
|
||||
}, async (request, reply) => {
|
||||
const { key } = request.params as { key: string };
|
||||
|
||||
const [deleted] = await db
|
||||
.delete(siteSettings)
|
||||
.where(eq(siteSettings.key, key))
|
||||
.returning();
|
||||
|
||||
if (!deleted) {
|
||||
return reply.code(404).send({ error: 'Setting not found' });
|
||||
}
|
||||
|
||||
return { message: 'Setting deleted successfully' };
|
||||
});
|
||||
};
|
||||
|
||||
export default settingsRoute;
|
||||
Reference in New Issue
Block a user