Skip to content
This repository was archived by the owner on Feb 27, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions apps/server/src/modules/app/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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 });
Expand All @@ -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 };
Expand Down Expand Up @@ -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) {
Expand All @@ -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" });
Expand Down
23 changes: 21 additions & 2 deletions apps/server/src/modules/app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
24 changes: 24 additions & 0 deletions apps/server/src/modules/app/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
31 changes: 31 additions & 0 deletions apps/web/src/app/api/(proxy)/app/configs/public/route.ts
Original file line number Diff line number Diff line change
@@ -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;
}
14 changes: 7 additions & 7 deletions apps/web/src/hooks/use-secure-configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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[]>([]);
Expand All @@ -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");
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/http/endpoints/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down