diff --git a/apps/server/src/modules/app/controller.ts b/apps/server/src/modules/app/controller.ts index 131c6aa..2308e61 100644 --- a/apps/server/src/modules/app/controller.ts +++ b/apps/server/src/modules/app/controller.ts @@ -9,7 +9,7 @@ export class AppController { private logoService = new LogoService(); private emailService = new EmailService(); - async getAppInfo(request: FastifyRequest, reply: FastifyReply) { + async getAppInfo(_request: FastifyRequest, reply: FastifyReply) { try { const appInfo = await this.appService.getAppInfo(); return reply.send(appInfo); @@ -18,7 +18,7 @@ export class AppController { } } - async getSystemInfo(request: FastifyRequest, reply: FastifyReply) { + async getSystemInfo(_request: FastifyRequest, reply: FastifyReply) { try { const systemInfo = await this.appService.getSystemInfo(); return reply.send(systemInfo); @@ -27,7 +27,7 @@ export class AppController { } } - async getAllConfigs(request: FastifyRequest, reply: FastifyReply) { + async getAllConfigs(_request: FastifyRequest, reply: FastifyReply) { try { const configs = await this.appService.getAllConfigs(); return reply.send({ configs }); @@ -36,6 +36,15 @@ export class AppController { } } + async getPublicConfigs(_request: FastifyRequest, reply: FastifyReply) { + try { + const configs = await this.appService.getPublicConfigs(); + return reply.send({ configs }); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } + } + async updateConfig(request: FastifyRequest, reply: FastifyReply) { try { const { key } = request.params as { key: string }; @@ -90,9 +99,8 @@ export class AppController { return reply.status(400).send({ error: "Only images are allowed" }); } - // Logo files should be small (max 5MB), so we can safely use streaming to buffer const chunks: Buffer[] = []; - const maxLogoSize = 5 * 1024 * 1024; // 5MB + const maxLogoSize = 5 * 1024 * 1024; let totalSize = 0; for await (const chunk of file.file) { @@ -114,7 +122,7 @@ export class AppController { } } - async removeLogo(request: FastifyRequest, reply: FastifyReply) { + async removeLogo(_request: FastifyRequest, reply: FastifyReply) { try { await this.logoService.deleteLogo(); return reply.send({ message: "Logo removed successfully" }); diff --git a/apps/server/src/modules/app/routes.ts b/apps/server/src/modules/app/routes.ts index 9cb7474..246b215 100644 --- a/apps/server/src/modules/app/routes.ts +++ b/apps/server/src/modules/app/routes.ts @@ -102,15 +102,34 @@ export async function appRoutes(app: FastifyInstance) { appController.updateConfig.bind(appController) ); + app.get( + "/app/configs/public", + { + schema: { + tags: ["App"], + operationId: "getPublicConfigs", + summary: "List public configurations", + description: "List public configurations (excludes sensitive data like SMTP credentials)", + response: { + 200: z.object({ + configs: z.array(ConfigResponseSchema), + }), + 400: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + appController.getPublicConfigs.bind(appController) + ); + app.get( "/app/configs", { - // preValidation: adminPreValidation, + preValidation: adminPreValidation, schema: { tags: ["App"], operationId: "getAllConfigs", summary: "List all configurations", - description: "List all configurations (admin only)", + description: "List all configurations including sensitive data (admin only)", response: { 200: z.object({ configs: z.array(ConfigResponseSchema), diff --git a/apps/server/src/modules/app/service.ts b/apps/server/src/modules/app/service.ts index 66ce7e8..4dee8f0 100644 --- a/apps/server/src/modules/app/service.ts +++ b/apps/server/src/modules/app/service.ts @@ -41,6 +41,30 @@ export class AppService { }); } + async getPublicConfigs() { + const sensitiveKeys = [ + "smtpHost", + "smtpPort", + "smtpUser", + "smtpPass", + "smtpSecure", + "smtpNoAuth", + "smtpTrustSelfSigned", + "jwtSecret" + ]; + + return prisma.appConfig.findMany({ + where: { + key: { + notIn: sensitiveKeys, + }, + }, + orderBy: { + group: "asc", + }, + }); + } + async updateConfig(key: string, value: string) { if (key === "jwtSecret") { throw new Error("JWT Secret cannot be updated through this endpoint"); diff --git a/apps/web/src/app/api/(proxy)/app/configs/public/route.ts b/apps/web/src/app/api/(proxy)/app/configs/public/route.ts new file mode 100644 index 0000000..3910108 --- /dev/null +++ b/apps/web/src/app/api/(proxy)/app/configs/public/route.ts @@ -0,0 +1,31 @@ +import { NextRequest, NextResponse } from "next/server"; + +const API_BASE_URL = process.env.API_BASE_URL || "http://localhost:3333"; + +export async function GET(req: NextRequest) { + const cookieHeader = req.headers.get("cookie"); + const url = `${API_BASE_URL}/app/configs/public`; + + const apiRes = await fetch(url, { + method: "GET", + headers: { + cookie: cookieHeader || "", + }, + redirect: "manual", + }); + + const resBody = await apiRes.text(); + const res = new NextResponse(resBody, { + status: apiRes.status, + headers: { + "Content-Type": "application/json", + }, + }); + + const setCookie = apiRes.headers.getSetCookie?.() || []; + if (setCookie.length > 0) { + res.headers.set("Set-Cookie", setCookie.join(",")); + } + + return res; +} \ No newline at end of file diff --git a/apps/web/src/hooks/use-secure-configs.ts b/apps/web/src/hooks/use-secure-configs.ts index 4d957de..2169241 100644 --- a/apps/web/src/hooks/use-secure-configs.ts +++ b/apps/web/src/hooks/use-secure-configs.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from "react"; -import { getAllConfigs } from "@/http/endpoints"; +import { getAllConfigs, getPublicConfigs } from "@/http/endpoints"; interface Config { key: string; @@ -13,8 +13,8 @@ interface Config { } /** - * Hook to fetch configurations securely - * Replaces direct use of getAllConfigs which exposed sensitive data + * Hook to fetch public configurations (excludes sensitive SMTP data) + * Safe to use without authentication */ export function useSecureConfigs() { const [configs, setConfigs] = useState([]); @@ -25,7 +25,7 @@ export function useSecureConfigs() { try { setIsLoading(true); setError(null); - const response = await getAllConfigs(); + const response = await getPublicConfigs(); setConfigs(response.data.configs); } catch (err) { setError(err instanceof Error ? err.message : "Unknown error"); @@ -95,8 +95,8 @@ export function useAdminConfigs() { } /** - * Hook to fetch a specific configuration value - * Useful when you only need a specific value (e.g. smtpEnabled) + * Hook to fetch a specific public configuration value + * Only returns non-sensitive config values (excludes SMTP credentials) */ export function useSecureConfigValue(key: string) { const [value, setValue] = useState(null); @@ -107,7 +107,7 @@ export function useSecureConfigValue(key: string) { try { setIsLoading(true); setError(null); - const response = await getAllConfigs(); + const response = await getPublicConfigs(); const config = response.data.configs.find((c) => c.key === key); setValue(config?.value || null); } catch (err) { diff --git a/apps/web/src/http/endpoints/config/index.ts b/apps/web/src/http/endpoints/config/index.ts index b23ce23..6bf7b15 100644 --- a/apps/web/src/http/endpoints/config/index.ts +++ b/apps/web/src/http/endpoints/config/index.ts @@ -21,6 +21,14 @@ export const updateConfig = ( return apiInstance.patch(`/api/config/update/${key}`, updateConfigBody, options); }; +/** + * List public configurations (excludes sensitive data) + * @summary List public configurations + */ +export const getPublicConfigs = (options?: AxiosRequestConfig): Promise => { + return apiInstance.get(`/api/app/configs/public`, options); +}; + /** * List all configurations (admin only) * @summary List all configurations