Bernt Christian Egeland 5 miesięcy temu
rodzic
commit
204b138a00

+ 3 - 0
src/features/ai/Actions/aiSettingsActions.ts

@@ -5,6 +5,7 @@ import { withAuth } from "@/lib/with-auth";
 import { revalidatePath } from "next/cache";
 import { ALL_AI_KEYS, AI_KEYS, type AiProvider, type AiModel, getModelCost, formatModelLabel } from "../Schema/aiSettingsSchema";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+import { demoGuard } from "@/lib/demo";
 
 const API_KEY_MASK = "••••••••••••••••";
 
@@ -36,6 +37,7 @@ export async function getAiSettings() {
 export async function setAiSettings(entries: Record<string, string>) {
   return withAuth(
     async ({ userId, organizationId }) => {
+      demoGuard();
       // Filter out masked API key — only update if user provided a new one
       const filtered = Object.entries(entries).filter(
         ([key, value]) => !(key === AI_KEYS.AI_API_KEY && value === API_KEY_MASK),
@@ -118,6 +120,7 @@ function anthropicModelOrder(id: string): number {
 export async function fetchAiModels(provider: AiProvider) {
   return withAuth(
     async ({ organizationId }) => {
+      demoGuard();
       // Read API key from DB — never accept it from the client
       const setting = await db.appSetting.findUnique({
         where: { organizationId_key: { organizationId, key: AI_KEYS.AI_API_KEY } },

+ 3 - 0
src/features/email/Actions/emailSettingsActions.ts

@@ -6,6 +6,7 @@ import { revalidatePath } from "next/cache";
 import { ALL_ORG_EMAIL_KEYS } from "../Schema/emailSettingsSchema";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { sendOrgMail, getOrgFromAddress } from "@/lib/email";
+import { demoGuard } from "@/lib/demo";
 
 export async function getEmailSettings() {
   return withAuth(
@@ -30,6 +31,7 @@ export async function getEmailSettings() {
 export async function setEmailSettings(entries: Record<string, string>) {
   return withAuth(
     async ({ userId, organizationId }) => {
+      demoGuard();
       await db.$transaction(
         Object.entries(entries).map(([key, value]) =>
           db.appSetting.upsert({
@@ -56,6 +58,7 @@ export async function setEmailSettings(entries: Record<string, string>) {
 export async function testOrgEmailConnection() {
   return withAuth(
     async ({ userId, organizationId }) => {
+      demoGuard();
       const user = await db.user.findUnique({
         where: { id: userId },
         select: { email: true },

+ 3 - 0
src/features/settings/Actions/settingsActions.ts

@@ -5,6 +5,7 @@ import { withAuth } from "@/lib/with-auth";
 import { revalidatePath } from "next/cache";
 import type { SettingKey } from "../Schema/settingsSchema";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+import { demoGuardSettingKey } from "@/lib/demo";
 
 export async function getSetting(key: SettingKey) {
   return withAuth(async ({ userId, organizationId }) => {
@@ -33,6 +34,7 @@ export async function getSettings(keys?: SettingKey[]) {
 
 export async function setSetting(key: SettingKey, value: string) {
   return withAuth(async ({ userId, organizationId }) => {
+    demoGuardSettingKey(key);
     const setting = await db.appSetting.upsert({
       where: { organizationId_key: { organizationId, key } },
       update: { value },
@@ -45,6 +47,7 @@ export async function setSetting(key: SettingKey, value: string) {
 
 export async function setSettings(entries: Record<string, string>) {
   return withAuth(async ({ userId, organizationId }) => {
+    for (const key of Object.keys(entries)) demoGuardSettingKey(key);
     await db.$transaction(
       Object.entries(entries).map(([key, value]) =>
         db.appSetting.upsert({

+ 3 - 0
src/features/sms/Actions/smsSettingsActions.ts

@@ -7,6 +7,7 @@ import { revalidatePath } from "next/cache";
 import { ALL_ORG_SMS_KEYS, ORG_SMS_KEYS } from "../Schema/smsSettingsSchema";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { sendOrgSms } from "@/lib/sms";
+import { demoGuard } from "@/lib/demo";
 
 export async function getSmsSettings() {
   return withAuth(
@@ -31,6 +32,7 @@ export async function getSmsSettings() {
 export async function setSmsSettings(entries: Record<string, string>) {
   return withAuth(
     async ({ userId, organizationId }) => {
+      demoGuard();
       // Auto-generate webhook secret if not already set
       const existing = await db.appSetting.findUnique({
         where: {
@@ -71,6 +73,7 @@ export async function setSmsSettings(entries: Record<string, string>) {
 export async function testSmsSend(testPhone: string) {
   return withAuth(
     async ({ organizationId }) => {
+      demoGuard();
       if (!testPhone?.trim()) {
         throw new Error("Please enter a phone number to send the test SMS to");
       }

+ 3 - 0
src/features/telegram/Actions/telegramSettingsActions.ts

@@ -4,6 +4,7 @@ import crypto from "crypto";
 import { db } from "@/lib/db";
 import { withAuth } from "@/lib/with-auth";
 import { revalidatePath } from "next/cache";
+import { demoGuard } from "@/lib/demo";
 import {
   ALL_ORG_TELEGRAM_KEYS,
   ORG_TELEGRAM_KEYS,
@@ -40,6 +41,7 @@ export async function getTelegramSettings() {
 export async function setTelegramSettings(settings: { botToken: string }) {
   return withAuth(
     async ({ userId, organizationId }) => {
+      demoGuard();
       await requireFeature(organizationId, "telegram");
 
       // Validate bot token by calling getMe
@@ -94,6 +96,7 @@ export async function testTelegramSend(input: {
 }) {
   return withAuth(
     async ({ organizationId }) => {
+      demoGuard();
       await requireFeature(organizationId, "telegram");
 
       if (!input.chatId?.trim()) {

+ 23 - 0
src/lib/demo.ts

@@ -26,3 +26,26 @@ export function demoGuard(): void {
     throw new Error("This action is disabled on the demo. Install Torqvoice on your own server to use it.");
   }
 }
+
+/**
+ * Setting keys that store provider credentials / secrets. Demo visitors
+ * shouldn't be able to paste real API keys into a shared demo DB.
+ */
+const DEMO_BLOCKED_SETTING_KEY_PATTERNS: RegExp[] = [
+  /^payment\.(stripe|vipps|paypal)\./,
+  /^payment\.providersEnabled$/,
+];
+
+export function isDemoBlockedSettingKey(key: string): boolean {
+  return DEMO_BLOCKED_SETTING_KEY_PATTERNS.some((p) => p.test(key));
+}
+
+/**
+ * Guard for settings writes — throws if the key stores a credential/secret.
+ * Safe keys (theme, language, date format, ...) pass through.
+ */
+export function demoGuardSettingKey(key: string): void {
+  if (isDemoMode && isDemoBlockedSettingKey(key)) {
+    throw new Error("This setting can't be changed on the demo.");
+  }
+}