Skip to content
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
2 changes: 2 additions & 0 deletions client/src/components/settings/AccountSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { useAuth } from '@/hooks/use-auth';
import { useTranslation } from 'react-i18next';
import { apiFetch } from '@/lib/api';
import { SUPPORTED_LANGUAGES } from '@/lib/languages';
import { PRETTY_DATE_FORMAT, formatPrettyDateTime } from '@/utils/date-utils';

interface AccountSettingsProps {
profileUsername: string;
Expand Down Expand Up @@ -418,6 +419,7 @@ const AccountSettings = ({
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value={PRETTY_DATE_FORMAT}>{formatPrettyDateTime(new Date(2026, 0, 15, 12, 0))}</SelectItem>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Pretty date format is rejected

When a user selects the new pretty-date option, profile autosave submits MMM D, YYYY, but the backend allowlist accepts only the three legacy formats, so the displayed selection is not persisted and reverts after the profile is refreshed.

Context Used: Ensure all code meets SOLID, DRY, and KISS softwar... (source)

Artifacts

Repro: executable focused client-to-backend persistence contract harness

  • Evidence file captured while the check ran.

Repro: before-change legacy selection PATCH and profile reload responses

  • The full command output behind this check.

Repro: after-change pretty selection PATCH and reverted profile reload responses

  • The full command output behind this check.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: client/src/components/settings/AccountSettings.tsx
Line: 422

Comment:
**Pretty date format is rejected**

When a user selects the new pretty-date option, profile autosave submits `MMM D, YYYY`, but the backend allowlist accepts only the three legacy formats, so the displayed selection is not persisted and reverts after the profile is refreshed.

**Context Used:** Ensure all code meets SOLID, DRY, and KISS softwar... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

<SelectItem value="MM/DD/YYYY">MM/DD/YYYY HH:mm</SelectItem>
<SelectItem value="DD/MM/YYYY">DD/MM/YYYY HH:mm</SelectItem>
<SelectItem value="YYYY-MM-DD">YYYY-MM-DD HH:mm</SelectItem>
Expand Down
49 changes: 28 additions & 21 deletions client/src/components/settings/DomainSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,24 +56,40 @@ const DomainSettings: React.FC = () => {
const loadDomainConfig = async () => {
try {
const response = await apiFetch('/v1/panel/settings/domain');
if (response.ok) {
const data = await response.json();
if (data.customDomain) {
setCustomDomain(data.customDomain);
setDomainStatus(data.status);
}
setAccessingFromCustomDomain(data.accessingFromCustomDomain || false);
setModlSubdomainUrl(data.modlSubdomainUrl || '');
setCanManageCustomDomain(Boolean(data.canManageCustomDomain));
if (!response.ok) {
toast({
title: t('toast.error'),
description: t('settings.domain.loadFailed'),
variant: "destructive",
});
return;
}
const data = await response.json();
if (data.customDomain) {
setCustomDomain(data.customDomain);
setDomainStatus(data.status);
}
setAccessingFromCustomDomain(data.accessingFromCustomDomain || false);
setModlSubdomainUrl(data.modlSubdomainUrl || '');
setCanManageCustomDomain(Boolean(data.canManageCustomDomain));
} catch (error) {
// Domain configuration may not exist yet
toast({
title: t('toast.error'),
description: t('settings.domain.loadFailed'),
variant: "destructive",
});
}
};

const validateDomain = (domain: string): boolean => {
const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9](?:\.[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])*$/;
return domainRegex.test(domain) && domain.length <= 253;
const normalized = domain.trim();
if (normalized.length === 0 || normalized.length > 253) {
return false;
}
if (!normalized.includes('.')) {
return false;
}
return !/\s/.test(normalized);
};
Comment on lines 84 to 93

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Domain validation accepts malformed hosts

The new validation accepts any dotted, whitespace-free string, including invalid hostnames such as a..b, a_.example.com, and -panel.example.com, so these values produce unnecessary configuration requests and defer basic validation to the API or DNS provider.

Rule Used: This is a React frontend project on React 19 with ... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: client/src/components/settings/DomainSettings.tsx
Line: 84-93

Comment:
**Domain validation accepts malformed hosts**

The new validation accepts any dotted, whitespace-free string, including invalid hostnames such as `a..b`, `a_.example.com`, and `-panel.example.com`, so these values produce unnecessary configuration requests and defer basic validation to the API or DNS provider.

**Rule Used:** This is a React frontend project on React 19 with ... ([source](https://app.greptile.com/modl-gg/-/custom-context?memory=b7532101-0c9e-4ab6-b168-353a105ba593))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.


const handleDomainSubmit = async () => {
Expand Down Expand Up @@ -104,15 +120,6 @@ const DomainSettings: React.FC = () => {
return;
}

if (domainStatus?.domain && domainStatus.domain.toLowerCase() === customDomain.trim().toLowerCase()) {
toast({
title: t('settings.domain.alreadyConfigured'),
description: t('settings.domain.alreadyConfiguredDesc'),
variant: "destructive",
});
return;
}

setIsLoading(true);
try {
const response = await apiFetch('/v1/panel/settings/domain', {
Expand Down
11 changes: 10 additions & 1 deletion client/src/components/settings/UsageSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { HardDrive, Search, Trash2, Download, FolderOpen, Calendar, AlertCircle, Settings, CreditCard, Brain, Play } from 'lucide-react';
import { HardDrive, Search, Trash2, Download, FolderOpen, Calendar, AlertCircle, Settings, CreditCard, Brain, Play, Info } from 'lucide-react';
import { getApiUrl, getCurrentDomain, apiFetch } from '@/lib/api';
import { Button } from '@modl-gg/shared-web/components/ui/button';
import { Input } from '@modl-gg/shared-web/components/ui/input';
Expand Down Expand Up @@ -889,6 +889,15 @@ const fetchStorageData = async () => {
</p>
</div>

{replayRetentionEnabled && (
<div className="flex items-start gap-2 rounded-lg border border-border bg-surface-1 p-3">
<Info className="h-4 w-4 shrink-0 text-muted-foreground mt-0.5" />
<p className="text-xs text-muted-foreground">
{t('settings.usage.replayRetentionExemptionNote')}
</p>
</div>
)}

<div className="flex items-center justify-between gap-3">
<Badge variant="outline">
{replayRetentionEnabled
Expand Down
6 changes: 3 additions & 3 deletions client/src/hooks/use-auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createContext, type ReactNode, useCallback, useContext, useEffect, useM
import { useLocation } from "wouter";
import { useToast } from "@modl-gg/shared-web/hooks/use-toast";
import { getApiUrl, getCurrentDomain } from "@/lib/api";
import { setDateLocale, setDateFormat } from "@/utils/date-utils";
import { DEFAULT_DATE_FORMAT, setDateLocale, setDateFormat } from "@/utils/date-utils";
import { startAuthentication, type PublicKeyCredentialRequestOptionsJSON } from "@simplewebauthn/browser";
import { isWebAuthnCancellation, unwrapPublicKeyOptions, type MaybePublicKeyWrapped } from "@/utils/webauthn";
import i18n from "@/lib/i18n";
Expand Down Expand Up @@ -70,7 +70,7 @@ function mapUserFromMeResponse(userData: MeResponse): User {
role: userData.role,
minecraftUsername: userData.minecraftUsername,
language: userData.language || undefined,
dateFormat: userData.dateFormat || 'MM/DD/YYYY',
dateFormat: userData.dateFormat || DEFAULT_DATE_FORMAT,
};
}

Expand Down Expand Up @@ -114,7 +114,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {

useEffect(() => {
const lang = user?.language || 'en';
const dateFormat = user?.dateFormat || 'MM/DD/YYYY';
const dateFormat = user?.dateFormat || DEFAULT_DATE_FORMAT;
setDateLocale(lang);
setDateFormat(dateFormat);
}, [user?.language, user?.dateFormat]);
Expand Down
4 changes: 2 additions & 2 deletions client/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,7 @@
"enterDomain": "Bitte gib einen Domainnamen ein.",
"invalidDomain": "Ungültige Domain",
"invalidDomainDesc": "Bitte gib einen gültigen Domainnamen ein (z. B. panel.beispiel.de).",
"alreadyConfigured": "Bereits konfiguriert",
"alreadyConfiguredDesc": "Diese Domain ist bereits konfiguriert.",
"loadFailed": "Domain-Einstellungen konnten nicht geladen werden.",
"configurationError": "Konfigurationsfehler",
"cloudflareConfigFailed": "Domain konnte nicht mit Cloudflare konfiguriert werden.",
"configurationStarted": "Konfiguration gestartet",
Expand Down Expand Up @@ -796,6 +795,7 @@
"replayRetentionEnabledDesc": "Gespeicherte Replays nach der konfigurierten Aufbewahrungszeit automatisch löschen.",
"replayRetentionDays": "Aufbewahrungszeitraum (Tage)",
"replayRetentionDaysDesc": "Gespeicherte Replays, die älter als diese Anzahl von Tagen sind, können bereinigt werden.",
"replayRetentionExemptionNote": "An offene Tickets oder Berufungen angehängte Replays werden nie gelöscht, auch nicht nach Ablauf der Aufbewahrungszeit.",
"replayRetentionDisabledDesc": "Automatische Replay-Bereinigung ist deaktiviert. Gespeicherte Replays bleiben erhalten, bis sie manuell gelöscht werden.",
"replayRetentionActive": "Replays werden nach {{days}} Tagen gelöscht",
"replayRetentionOff": "Automatische Bereinigung deaktiviert",
Expand Down
4 changes: 2 additions & 2 deletions client/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,7 @@
"enterDomain": "Please enter a domain name.",
"invalidDomain": "Invalid Domain",
"invalidDomainDesc": "Please enter a valid domain name (e.g. panel.example.com).",
"alreadyConfigured": "Already Configured",
"alreadyConfiguredDesc": "This domain is already configured.",
"loadFailed": "Failed to load domain settings.",
"configurationError": "Configuration Error",
"cloudflareConfigFailed": "Failed to configure domain with Cloudflare.",
"configurationStarted": "Configuration Started",
Expand Down Expand Up @@ -796,6 +795,7 @@
"replayRetentionEnabledDesc": "Automatically delete stored replays after the configured retention period.",
"replayRetentionDays": "Retention period (days)",
"replayRetentionDaysDesc": "Stored replays older than this many days are eligible for cleanup.",
"replayRetentionExemptionNote": "Replays attached to open tickets or appeals are never deleted, even after the retention period.",
"replayRetentionDisabledDesc": "Automatic replay cleanup is disabled. Stored replays will be retained until manually deleted.",
"replayRetentionActive": "Deleting replays after {{days}} days",
"replayRetentionOff": "Automatic cleanup disabled",
Expand Down
4 changes: 2 additions & 2 deletions client/src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,7 @@
"enterDomain": "Por favor introduce un nombre de dominio.",
"invalidDomain": "Dominio inválido",
"invalidDomainDesc": "Por favor introduce un nombre de dominio válido (ej. panel.ejemplo.com).",
"alreadyConfigured": "Ya configurado",
"alreadyConfiguredDesc": "Este dominio ya está configurado.",
"loadFailed": "No se pudo cargar la configuración del dominio.",
"configurationError": "Error de configuración",
"cloudflareConfigFailed": "No se pudo configurar el dominio con Cloudflare.",
"configurationStarted": "Configuración iniciada",
Expand Down Expand Up @@ -796,6 +795,7 @@
"replayRetentionEnabledDesc": "Eliminar automáticamente las repeticiones almacenadas después del periodo de retención configurado.",
"replayRetentionDays": "Periodo de retención (días)",
"replayRetentionDaysDesc": "Las repeticiones almacenadas con más de esta cantidad de días podrán limpiarse.",
"replayRetentionExemptionNote": "Las repeticiones adjuntas a tickets o apelaciones abiertos nunca se eliminan, incluso después del periodo de retención.",
"replayRetentionDisabledDesc": "La limpieza automática de repeticiones está deshabilitada. Las repeticiones almacenadas se conservarán hasta que se eliminen manualmente.",
"replayRetentionActive": "Eliminando repeticiones después de {{days}} días",
"replayRetentionOff": "Limpieza automática deshabilitada",
Expand Down
4 changes: 2 additions & 2 deletions client/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,7 @@
"enterDomain": "Veuillez entrer un nom de domaine.",
"invalidDomain": "Domaine invalide",
"invalidDomainDesc": "Veuillez entrer un nom de domaine valide (ex: panel.exemple.com).",
"alreadyConfigured": "Déjà configuré",
"alreadyConfiguredDesc": "Ce domaine est déjà configuré.",
"loadFailed": "Échec du chargement des paramètres du domaine.",
"configurationError": "Erreur de configuration",
"cloudflareConfigFailed": "Échec de la configuration du domaine avec Cloudflare.",
"configurationStarted": "Configuration démarrée",
Expand Down Expand Up @@ -796,6 +795,7 @@
"replayRetentionEnabledDesc": "Supprimer automatiquement les replays stockés après la période de rétention configurée.",
"replayRetentionDays": "Période de rétention (jours)",
"replayRetentionDaysDesc": "Les replays stockés plus anciens que ce nombre de jours peuvent être nettoyés.",
"replayRetentionExemptionNote": "Les replays associés à des tickets ou appels ouverts ne sont jamais supprimés, même après la période de rétention.",
"replayRetentionDisabledDesc": "Le nettoyage automatique des replays est désactivé. Les replays stockés seront conservés jusqu'à leur suppression manuelle.",
"replayRetentionActive": "Suppression des replays après {{days}} jours",
"replayRetentionOff": "Nettoyage automatique désactivé",
Expand Down
4 changes: 2 additions & 2 deletions client/src/locales/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,7 @@
"enterDomain": "कृपया एक डोमेन नाम दर्ज करें।",
"invalidDomain": "अमान्य डोमेन",
"invalidDomainDesc": "कृपया एक मान्य डोमेन नाम दर्ज करें (जैसे panel.example.com)।",
"alreadyConfigured": "पहले से कॉन्फ़िगर है",
"alreadyConfiguredDesc": "यह डोमेन पहले से ही कॉन्फ़िगर है।",
"loadFailed": "डोमेन सेटिंग्स लोड करने में विफल।",
"configurationError": "कॉन्फ़िगरेशन त्रुटि",
"cloudflareConfigFailed": "Cloudflare के साथ डोमेन कॉन्फ़िगर करने में विफल।",
"configurationStarted": "कॉन्फ़िगरेशन प्रारंभ",
Expand Down Expand Up @@ -796,6 +795,7 @@
"replayRetentionEnabledDesc": "कॉन्फ़िगर की गई प्रतिधारण अवधि के बाद संग्रहीत रिप्ले को स्वचालित रूप से हटाएं।",
"replayRetentionDays": "प्रतिधारण अवधि (दिन)",
"replayRetentionDaysDesc": "इस संख्या से अधिक पुराने संग्रहीत रिप्ले सफाई के पात्र होंगे।",
"replayRetentionExemptionNote": "खुले टिकट या अपील से जुड़े रिप्ले कभी नहीं हटाए जाते, भले ही प्रतिधारण अवधि समाप्त हो गई हो।",
"replayRetentionDisabledDesc": "स्वचालित रिप्ले सफाई अक्षम है। संग्रहीत रिप्ले तब तक रहेंगे जब तक उन्हें मैन्युअल रूप से हटाया नहीं जाता।",
"replayRetentionActive": "{{days}} दिनों के बाद रिप्ले हटाया जा रहा है",
"replayRetentionOff": "स्वचालित सफाई अक्षम",
Expand Down
4 changes: 2 additions & 2 deletions client/src/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,7 @@
"enterDomain": "Inserisci un nome di dominio.",
"invalidDomain": "Dominio non valido",
"invalidDomainDesc": "Inserisci un nome di dominio valido (es. panel.example.com).",
"alreadyConfigured": "Già configurato",
"alreadyConfiguredDesc": "Questo dominio è già configurato.",
"loadFailed": "Impossibile caricare le impostazioni del dominio.",
"configurationError": "Errore di configurazione",
"cloudflareConfigFailed": "Impossibile configurare il dominio con Cloudflare.",
"configurationStarted": "Configurazione avviata",
Expand Down Expand Up @@ -796,6 +795,7 @@
"replayRetentionEnabledDesc": "Elimina automaticamente i replay archiviati dopo il periodo di conservazione configurato.",
"replayRetentionDays": "Periodo di conservazione (giorni)",
"replayRetentionDaysDesc": "I replay archiviati più vecchi di questo numero di giorni sono soggetti a pulizia.",
"replayRetentionExemptionNote": "I replay allegati a ticket o appelli aperti non vengono mai eliminati, anche dopo il periodo di conservazione.",
"replayRetentionDisabledDesc": "La pulizia automatica dei replay è disabilitata. I replay archiviati verranno conservati fino all'eliminazione manuale.",
"replayRetentionActive": "Eliminazione dei replay dopo {{days}} giorni",
"replayRetentionOff": "Pulizia automatica disabilitata",
Expand Down
4 changes: 2 additions & 2 deletions client/src/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,7 @@
"enterDomain": "ドメイン名を入力してください。",
"invalidDomain": "無効なドメイン",
"invalidDomainDesc": "有効なドメイン名を入力してください(例:panel.example.com)。",
"alreadyConfigured": "構成済み",
"alreadyConfiguredDesc": "このドメインは既に構成されています。",
"loadFailed": "ドメイン設定の読み込みに失敗しました。",
"configurationError": "構成エラー",
"cloudflareConfigFailed": "Cloudflareでのドメインの構成に失敗しました。",
"configurationStarted": "構成の開始",
Expand Down Expand Up @@ -796,6 +795,7 @@
"replayRetentionEnabledDesc": "設定された保持期間を過ぎた保存済みリプレイを自動的に削除します。",
"replayRetentionDays": "保持期間 (日)",
"replayRetentionDaysDesc": "この日数より古い保存済みリプレイは削除の対象となります。",
"replayRetentionExemptionNote": "未解決のチケットや不服申し立てに添付されたリプレイは、保持期間を過ぎても削除されません。",
"replayRetentionDisabledDesc": "自動リプレイ削除は無効になっています。保存されたリプレイは手動で削除されるまで保持されます。",
"replayRetentionActive": "{{days}} 日後にリプレイを削除中",
"replayRetentionOff": "自動削除は無効です",
Expand Down
4 changes: 2 additions & 2 deletions client/src/locales/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,7 @@
"enterDomain": "Voer een domeinnaam in.",
"invalidDomain": "Ongeldig domein",
"invalidDomainDesc": "Voer een geldige domeinnaam in (bijv. panel.voorbeeld.nl).",
"alreadyConfigured": "Al geconfigureerd",
"alreadyConfiguredDesc": "Dit domein is al geconfigureerd.",
"loadFailed": "Domeininstellingen laden mislukt.",
"configurationError": "Configuratiefout",
"cloudflareConfigFailed": "Domein configureren met Cloudflare mislukt.",
"configurationStarted": "Configuratie gestart",
Expand Down Expand Up @@ -796,6 +795,7 @@
"replayRetentionEnabledDesc": "Verwijder opgeslagen replays automatisch na de ingestelde bewaartermijn.",
"replayRetentionDays": "Bewaartermijn (dagen)",
"replayRetentionDaysDesc": "Opgeslagen replays ouder dan dit aantal dagen komen in aanmerking voor opruiming.",
"replayRetentionExemptionNote": "Replays die aan open tickets of beroepen zijn gekoppeld, worden nooit verwijderd, zelfs niet na de bewaartermijn.",
"replayRetentionDisabledDesc": "Automatische replay-opruiming is uitgeschakeld. Opgeslagen replays blijven bewaard totdat ze handmatig worden verwijderd.",
"replayRetentionActive": "Replays worden na {{days}} dagen verwijderd",
"replayRetentionOff": "Automatische opruiming uitgeschakeld",
Expand Down
4 changes: 2 additions & 2 deletions client/src/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,7 @@
"enterDomain": "Por favor, insira um nome de domínio.",
"invalidDomain": "Domínio Inválido",
"invalidDomainDesc": "Por favor, insira um nome de domínio válido (ex: painel.exemplo.com).",
"alreadyConfigured": "Já Configurado",
"alreadyConfiguredDesc": "Este domínio já está configurado.",
"loadFailed": "Falha ao carregar as configurações do domínio.",
"configurationError": "Erro de Configuração",
"cloudflareConfigFailed": "Falha ao configurar o domínio com a Cloudflare.",
"configurationStarted": "Configuração Iniciada",
Expand Down Expand Up @@ -796,6 +795,7 @@
"replayRetentionEnabledDesc": "Excluir automaticamente os replays armazenados após o período de retenção configurado.",
"replayRetentionDays": "Período de retenção (dias)",
"replayRetentionDaysDesc": "Replays armazenados mais antigos que este número de dias serão qualificados para limpeza.",
"replayRetentionExemptionNote": "Os replays anexados a tickets ou apelações abertos nunca são excluídos, mesmo após o período de retenção.",
"replayRetentionDisabledDesc": "A limpeza automática de replays está desativada. Os replays armazenados serão retidos até serem excluídos manualmente.",
"replayRetentionActive": "Excluindo replays após {{days}} dias",
"replayRetentionOff": "Limpeza automática desativada",
Expand Down
Loading
Loading