mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-23 06:11:58 +00:00
feat: implement public configuration retrieval (#218)
This commit is contained in:
@@ -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" });
|
||||
|
@@ -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),
|
||||
|
@@ -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");
|
||||
|
31
apps/web/src/app/api/(proxy)/app/configs/public/route.ts
Normal file
31
apps/web/src/app/api/(proxy)/app/configs/public/route.ts
Normal 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;
|
||||
}
|
@@ -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) {
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user