feat: implement public configuration retrieval (#218)

This commit is contained in:
Daniel Luiz Alves
2025-08-18 20:35:02 -03:00
committed by GitHub
6 changed files with 105 additions and 15 deletions

View File

@@ -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" });

View File

@@ -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),

View File

@@ -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");

View File

@@ -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;
}

View File

@@ -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<Config[]>([]);
@@ -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<string | null>(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) {

View File

@@ -21,6 +21,14 @@ export const updateConfig = <TData = UpdateConfigResult>(
return apiInstance.patch(`/api/config/update/${key}`, updateConfigBody, options);
};
/**
* List public configurations (excludes sensitive data)
* @summary List public configurations
*/
export const getPublicConfigs = <TData = GetAllConfigsResult>(options?: AxiosRequestConfig): Promise<TData> => {
return apiInstance.get(`/api/app/configs/public`, options);
};
/**
* List all configurations (admin only)
* @summary List all configurations