Skip to content

Commit e985f3b

Browse files
committed
feat: add macOS preferences editor to dashboard config form
- Add pref input (domain.key=value format) with type selector to config modal - Display added prefs as a list with remove buttons - Show prefs count in config stats bar - Give macOS prefs its own section in ConfigDetail (separate from taps) - Validate macos_prefs on POST and PUT API routes (max 100, required fields, type allowlist)
1 parent 3d7d37d commit e985f3b

File tree

5 files changed

+293
-43
lines changed

5 files changed

+293
-43
lines changed

src/lib/components/ConfigDetail.svelte

Lines changed: 37 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,12 @@
233233
<div class="stat-lbl">Dev</div>
234234
</div>
235235
{/if}
236+
{#if macosPrefs.length > 0}
237+
<div class="stat">
238+
<div class="stat-val">{macosPrefs.length}</div>
239+
<div class="stat-lbl">Prefs</div>
240+
</div>
241+
{/if}
236242
<div class="stat">
237243
<div class="stat-val">{config.install_count || 0}</div>
238244
<div class="stat-lbl">Installs</div>
@@ -408,50 +414,42 @@
408414
{/if}
409415
</section>
410416

411-
{#if taps.length > 0 || macosPrefs.length > 0}
417+
{#if macosPrefs.length > 0}
412418
<section class="section">
413-
<h2 class="section-title">📋 Additional</h2>
414-
415-
{#if taps.length > 0}
416-
<details class="detail-card">
417-
<summary>
418-
<span class="detail-icon">🚰</span>
419-
<span class="detail-title">Homebrew Taps ({taps.length})</span>
420-
</summary>
421-
<div class="detail-content">
422-
<div class="tap-list">
423-
{#each taps as tap}
424-
<div class="tap-item">{tap}</div>
425-
{/each}
419+
<h2 class="section-title">🍎 macOS Preferences</h2>
420+
<div class="prefs">
421+
{#each macosPrefs as pref}
422+
<div class="pref">
423+
<div class="pref-header">
424+
<span class="pref-key">{pref.key}</span>
425+
<span class="pref-domain">{pref.domain}</span>
426426
</div>
427+
{#if pref.desc}
428+
<p class="pref-desc">{pref.desc}</p>
429+
{/if}
430+
<div class="pref-val">{pref.value}</div>
427431
</div>
428-
</details>
429-
{/if}
432+
{/each}
433+
</div>
434+
</section>
435+
{/if}
430436

431-
{#if macosPrefs.length > 0}
432-
<details class="detail-card">
433-
<summary>
434-
<span class="detail-icon">🍎</span>
435-
<span class="detail-title">macOS Preferences ({macosPrefs.length})</span>
436-
</summary>
437-
<div class="detail-content">
438-
<div class="prefs">
439-
{#each macosPrefs as pref}
440-
<div class="pref">
441-
<div class="pref-header">
442-
<span class="pref-key">{pref.key}</span>
443-
<span class="pref-domain">{pref.domain}</span>
444-
</div>
445-
{#if pref.desc}
446-
<p class="pref-desc">{pref.desc}</p>
447-
{/if}
448-
<div class="pref-val">{pref.value}</div>
449-
</div>
450-
{/each}
451-
</div>
437+
{#if taps.length > 0}
438+
<section class="section">
439+
<h2 class="section-title">📋 Additional</h2>
440+
<details class="detail-card">
441+
<summary>
442+
<span class="detail-icon">🚰</span>
443+
<span class="detail-title">Homebrew Taps ({taps.length})</span>
444+
</summary>
445+
<div class="detail-content">
446+
<div class="tap-list">
447+
{#each taps as tap}
448+
<div class="tap-item">{tap}</div>
449+
{/each}
452450
</div>
453-
</details>
454-
{/if}
451+
</div>
452+
</details>
455453
</section>
456454
{/if}
457455

src/lib/server/validation.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,38 @@ export function validateReturnTo(path: string | null | undefined): boolean {
100100
return /^\/[a-zA-Z0-9\-_/]*(\?[a-zA-Z0-9\-_=&]*)?$/.test(decoded);
101101
}
102102

103+
/** Validates snapshot.macos_prefs array: required fields, type allowlist, max count.
104+
* Mirrors the Go config.RemoteConfig.Validate() check in the CLI. */
105+
export function validateMacOSPrefs(prefs: unknown): ValidationResult {
106+
if (prefs === undefined || prefs === null) return { valid: true };
107+
if (!Array.isArray(prefs)) return { valid: false, error: 'macos_prefs must be an array' };
108+
if (prefs.length > 100) return { valid: false, error: 'Maximum 100 macOS preferences allowed' };
109+
110+
const validTypes = new Set(['', 'string', 'int', 'bool', 'float']);
111+
112+
for (let i = 0; i < prefs.length; i++) {
113+
const p = prefs[i];
114+
if (typeof p !== 'object' || p === null) {
115+
return { valid: false, error: `macos_prefs[${i}] must be an object` };
116+
}
117+
const { domain, key, value, type } = p as Record<string, unknown>;
118+
if (!domain || typeof domain !== 'string') {
119+
return { valid: false, error: `macos_prefs[${i}] missing required field: domain` };
120+
}
121+
if (!key || typeof key !== 'string') {
122+
return { valid: false, error: `macos_prefs[${i}] missing required field: key` };
123+
}
124+
if (value === undefined || value === null || typeof value !== 'string') {
125+
return { valid: false, error: `macos_prefs[${i}] missing required field: value` };
126+
}
127+
if (type !== undefined && (typeof type !== 'string' || !validTypes.has(type as string))) {
128+
return { valid: false, error: `macos_prefs[${i}] invalid type "${type}" (allowed: string, int, bool, float)` };
129+
}
130+
}
131+
132+
return { valid: true };
133+
}
134+
103135
interface Package {
104136
name: string;
105137
type: string;

src/routes/api/configs/+server.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { json } from '@sveltejs/kit';
22
import type { RequestHandler } from './$types';
33
import { getCurrentUser, slugify, generateId } from '$lib/server/auth';
4-
import { validateCustomScript, validateDotfilesRepo, validatePackages, RESERVED_ALIASES } from '$lib/server/validation';
4+
import { validateCustomScript, validateDotfilesRepo, validatePackages, validateMacOSPrefs, RESERVED_ALIASES } from '$lib/server/validation';
55
import { checkRateLimit, getRateLimitKey, RATE_LIMITS } from '$lib/server/rate-limit';
66

77
export const GET: RequestHandler = async ({ platform, cookies, request }) => {
@@ -77,6 +77,10 @@ export const POST: RequestHandler = async ({ platform, cookies, request }) => {
7777
const rv = validateDotfilesRepo(dotfiles_repo);
7878
if (!rv.valid) return json({ error: rv.error }, { status: 400 });
7979
}
80+
if (snapshot?.macos_prefs !== undefined) {
81+
const pv = validateMacOSPrefs(snapshot.macos_prefs);
82+
if (!pv.valid) return json({ error: pv.error }, { status: 400 });
83+
}
8084

8185
if (!name) return json({ error: 'Name is required' }, { status: 400 });
8286
if (typeof name !== 'string' || name.length > 100) return json({ error: 'Name must be a string of 100 characters or less' }, { status: 400 });

src/routes/api/configs/[slug]/+server.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { json } from '@sveltejs/kit';
22
import type { RequestHandler } from './$types';
33
import { getCurrentUser, slugify } from '$lib/server/auth';
4-
import { validateCustomScript, validateDotfilesRepo, validatePackages, RESERVED_ALIASES } from '$lib/server/validation';
4+
import { validateCustomScript, validateDotfilesRepo, validatePackages, validateMacOSPrefs, RESERVED_ALIASES } from '$lib/server/validation';
55
import { checkRateLimit, getRateLimitKey, RATE_LIMITS } from '$lib/server/rate-limit';
66

77
export const GET: RequestHandler = async ({ platform, cookies, params, request }) => {
@@ -104,6 +104,10 @@ export const PUT: RequestHandler = async ({ platform, cookies, params, request }
104104
const rv = validateDotfilesRepo(dotfiles_repo);
105105
if (!rv.valid) return json({ error: rv.error }, { status: 400 });
106106
}
107+
if (snapshot !== undefined && snapshot !== null && snapshot.macos_prefs !== undefined) {
108+
const pv = validateMacOSPrefs(snapshot.macos_prefs);
109+
if (!pv.valid) return json({ error: pv.error }, { status: 400 });
110+
}
107111

108112
const slug = params.slug;
109113

0 commit comments

Comments
 (0)