Add server security hardening and draft quiz persistence
Security: - Add AES-256-GCM encryption for user PII (email, API keys, config) - Add rate limiting (helmet + express-rate-limit) - Require auth for file uploads UX: - Persist draft quizzes to sessionStorage (survives refresh) - Add URL-based edit routes (/edit/draft, /edit/:quizId) - Fix QuizEditor async defaultConfig race condition - Fix URL param accumulation in Landing
This commit is contained in:
parent
75c496e68f
commit
e480ad06df
18 changed files with 1775 additions and 94 deletions
|
|
@ -39,11 +39,13 @@ export const Landing: React.FC<LandingProps> = ({ onGenerate, onCreateManual, on
|
|||
|
||||
const setMode = (newMode: 'HOST' | 'JOIN') => {
|
||||
setModeState(newMode);
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
const newParams = new URLSearchParams();
|
||||
const modalParam = searchParams.get('modal');
|
||||
if (modalParam) {
|
||||
newParams.set('modal', modalParam);
|
||||
}
|
||||
if (newMode === 'HOST') {
|
||||
newParams.set('mode', 'host');
|
||||
} else {
|
||||
newParams.delete('mode');
|
||||
}
|
||||
setSearchParams(newParams, { replace: true });
|
||||
};
|
||||
|
|
@ -58,44 +60,33 @@ export const Landing: React.FC<LandingProps> = ({ onGenerate, onCreateManual, on
|
|||
const defaultConfigOpen = modalParam === 'settings';
|
||||
const accountSettingsOpen = modalParam === 'account';
|
||||
|
||||
const setLibraryOpen = (open: boolean) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
if (open) {
|
||||
newParams.set('modal', 'library');
|
||||
} else {
|
||||
newParams.delete('modal');
|
||||
const buildCleanParams = (updates: Record<string, string | null>) => {
|
||||
const newParams = new URLSearchParams();
|
||||
const modeParam = searchParams.get('mode');
|
||||
if (modeParam) newParams.set('mode', modeParam);
|
||||
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
if (value !== null) {
|
||||
newParams.set(key, value);
|
||||
}
|
||||
}
|
||||
setSearchParams(newParams);
|
||||
return newParams;
|
||||
};
|
||||
|
||||
const setLibraryOpen = (open: boolean) => {
|
||||
setSearchParams(buildCleanParams({ modal: open ? 'library' : null }));
|
||||
};
|
||||
|
||||
const setPreferencesOpen = (open: boolean) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
if (open) {
|
||||
newParams.set('modal', 'preferences');
|
||||
} else {
|
||||
newParams.delete('modal');
|
||||
}
|
||||
setSearchParams(newParams);
|
||||
setSearchParams(buildCleanParams({ modal: open ? 'preferences' : null }));
|
||||
};
|
||||
|
||||
const setDefaultConfigOpen = (open: boolean) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
if (open) {
|
||||
newParams.set('modal', 'settings');
|
||||
} else {
|
||||
newParams.delete('modal');
|
||||
}
|
||||
setSearchParams(newParams);
|
||||
setSearchParams(buildCleanParams({ modal: open ? 'settings' : null }));
|
||||
};
|
||||
|
||||
const setAccountSettingsOpen = (open: boolean) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
if (open) {
|
||||
newParams.set('modal', 'account');
|
||||
} else {
|
||||
newParams.delete('modal');
|
||||
}
|
||||
setSearchParams(newParams);
|
||||
setSearchParams(buildCleanParams({ modal: open ? 'account' : null }));
|
||||
};
|
||||
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||
|
|
|
|||
|
|
@ -39,11 +39,20 @@ export const QuizEditor: React.FC<QuizEditorProps> = ({
|
|||
const [config, setConfig] = useState<GameConfig>(
|
||||
initialQuiz.config || defaultConfig || DEFAULT_GAME_CONFIG
|
||||
);
|
||||
const [hasAppliedDefaultConfig, setHasAppliedDefaultConfig] = useState(!!initialQuiz.config);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasAppliedDefaultConfig && defaultConfig && defaultConfig !== DEFAULT_GAME_CONFIG) {
|
||||
setConfig(defaultConfig);
|
||||
setHasAppliedDefaultConfig(true);
|
||||
}
|
||||
}, [defaultConfig, hasAppliedDefaultConfig]);
|
||||
|
||||
useBodyScrollLock(!!showDeleteConfirm);
|
||||
|
||||
const handleConfigChange = useCallback((newConfig: GameConfig) => {
|
||||
setConfig(newConfig);
|
||||
setHasAppliedDefaultConfig(true);
|
||||
onConfigChange?.(newConfig);
|
||||
}, [onConfigChange]);
|
||||
|
||||
|
|
@ -191,7 +200,7 @@ export const QuizEditor: React.FC<QuizEditorProps> = ({
|
|||
|
||||
{showSaveButton && (
|
||||
<button
|
||||
onClick={() => onSave(quiz)}
|
||||
onClick={() => onSave({ ...quiz, config })}
|
||||
disabled={isSaving}
|
||||
className="bg-white/20 p-2 rounded-full hover:bg-white/30 transition disabled:opacity-50"
|
||||
title="Save to library"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue