@@ -4,15 +4,105 @@ import { dashboardQuery, dashboardClient } from "@/lib/sanity/dashboard";
44
55export const dynamic = "force-dynamic" ;
66
7- export async function GET ( ) {
8- // Auth check (skip if Supabase not configured)
9- const hasSupabase = process . env . NEXT_PUBLIC_SUPABASE_URL && process . env . NEXT_PUBLIC_SUPABASE_ANON_KEY ;
10- if ( hasSupabase ) {
11- const supabase = await createClient ( ) ;
12- const { data : { user } } = await supabase . auth . getUser ( ) ;
13- if ( ! user ) return NextResponse . json ( { error : "Unauthorized" } , { status : 401 } ) ;
7+ const SETTINGS_DOC_ID = "dashboardSettings" ;
8+
9+ const DEFAULT_SETTINGS = {
10+ videosPerWeek : 3 ,
11+ publishDays : [ "Mon" , "Wed" , "Fri" ] ,
12+ contentCategories : [
13+ "JavaScript" , "TypeScript" , "React" , "Next.js" , "Angular" ,
14+ "Svelte" , "Node.js" , "CSS" , "DevOps" , "AI / ML" ,
15+ "Web Performance" , "Tooling" ,
16+ ] ,
17+ rateCardTiers : [
18+ { name : "Pre-roll Mention" , description : "15-second sponsor mention" , price : 200 } ,
19+ { name : "Mid-roll Segment" , description : "60-second dedicated segment" , price : 500 } ,
20+ { name : "Dedicated Video" , description : "Full sponsored video" , price : 1500 } ,
21+ ] ,
22+ } ;
23+
24+ async function requireAuth ( ) {
25+ const hasSupabase =
26+ process . env . NEXT_PUBLIC_SUPABASE_URL &&
27+ process . env . NEXT_PUBLIC_SUPABASE_ANON_KEY ;
28+
29+ if ( ! hasSupabase ) {
30+ return { error : NextResponse . json ( { error : "Auth not configured" } , { status : 503 } ) } ;
31+ }
32+
33+ const supabase = await createClient ( ) ;
34+ const {
35+ data : { user } ,
36+ } = await supabase . auth . getUser ( ) ;
37+
38+ if ( ! user ) {
39+ return { error : NextResponse . json ( { error : "Unauthorized" } , { status : 401 } ) } ;
40+ }
41+
42+ return { user } ;
43+ }
44+
45+ const VALID_DAYS = [ "Mon" , "Tue" , "Wed" , "Thu" , "Fri" , "Sat" , "Sun" ] ;
46+
47+ function validateSettings ( body : unknown ) : { valid : boolean ; data ?: Record < string , unknown > ; error ?: string } {
48+ if ( ! body || typeof body !== "object" ) {
49+ return { valid : false , error : "Invalid request body" } ;
50+ }
51+
52+ const input = body as Record < string , unknown > ;
53+ const sanitized : Record < string , unknown > = { } ;
54+
55+ if ( "videosPerWeek" in input ) {
56+ const v = Number ( input . videosPerWeek ) ;
57+ if ( ! Number . isInteger ( v ) || v < 1 || v > 14 ) {
58+ return { valid : false , error : "videosPerWeek must be an integer between 1 and 14" } ;
59+ }
60+ sanitized . videosPerWeek = v ;
61+ }
62+
63+ if ( "publishDays" in input ) {
64+ if ( ! Array . isArray ( input . publishDays ) || ! input . publishDays . every ( ( d : unknown ) => typeof d === "string" && VALID_DAYS . includes ( d as string ) ) ) {
65+ return { valid : false , error : "publishDays must be an array of valid day abbreviations" } ;
66+ }
67+ sanitized . publishDays = input . publishDays ;
1468 }
1569
70+ if ( "contentCategories" in input ) {
71+ if ( ! Array . isArray ( input . contentCategories ) || ! input . contentCategories . every ( ( c : unknown ) => typeof c === "string" && ( c as string ) . length <= 50 ) ) {
72+ return { valid : false , error : "contentCategories must be an array of strings (max 50 chars each)" } ;
73+ }
74+ sanitized . contentCategories = input . contentCategories ;
75+ }
76+
77+ if ( "rateCardTiers" in input ) {
78+ if ( ! Array . isArray ( input . rateCardTiers ) ) {
79+ return { valid : false , error : "rateCardTiers must be an array" } ;
80+ }
81+ for ( const tier of input . rateCardTiers as Record < string , unknown > [ ] ) {
82+ if ( typeof tier . name !== "string" || typeof tier . description !== "string" || typeof tier . price !== "number" ) {
83+ return { valid : false , error : "Each rate card tier must have name (string), description (string), and price (number)" } ;
84+ }
85+ }
86+ sanitized . rateCardTiers = ( input . rateCardTiers as Record < string , unknown > [ ] ) . map ( ( t ) => ( {
87+ _type : "object" ,
88+ _key : crypto . randomUUID ( ) . slice ( 0 , 8 ) ,
89+ name : t . name ,
90+ description : t . description ,
91+ price : t . price ,
92+ } ) ) ;
93+ }
94+
95+ if ( Object . keys ( sanitized ) . length === 0 ) {
96+ return { valid : false , error : "No valid fields provided" } ;
97+ }
98+
99+ return { valid : true , data : sanitized } ;
100+ }
101+
102+ export async function GET ( ) {
103+ const auth = await requireAuth ( ) ;
104+ if ( auth . error ) return auth . error ;
105+
16106 try {
17107 const settings = await dashboardQuery (
18108 `*[_type == "dashboardSettings"][0] {
@@ -22,49 +112,38 @@ export async function GET() {
22112 rateCardTiers[] { name, description, price }
23113 }`
24114 ) ;
25- return NextResponse . json ( settings ?? {
26- videosPerWeek : 3 ,
27- publishDays : [ "Mon" , "Wed" , "Fri" ] ,
28- contentCategories : [ "JavaScript" , "TypeScript" , "React" , "Next.js" , "Angular" , "Svelte" , "Node.js" , "CSS" , "DevOps" , "AI / ML" , "Web Performance" , "Tooling" ] ,
29- rateCardTiers : [
30- { name : "Pre-roll Mention" , description : "15-second sponsor mention" , price : 200 } ,
31- { name : "Mid-roll Segment" , description : "60-second dedicated segment" , price : 500 } ,
32- { name : "Dedicated Video" , description : "Full sponsored video" , price : 1500 } ,
33- ] ,
34- } ) ;
115+ return NextResponse . json ( settings ?? DEFAULT_SETTINGS ) ;
35116 } catch ( error ) {
36117 console . error ( "Failed to fetch settings:" , error ) ;
37118 return NextResponse . json ( { error : "Failed to fetch settings" } , { status : 500 } ) ;
38119 }
39120}
40121
41122export async function PUT ( request : Request ) {
42- const hasSupabase = process . env . NEXT_PUBLIC_SUPABASE_URL && process . env . NEXT_PUBLIC_SUPABASE_ANON_KEY ;
43- if ( hasSupabase ) {
44- const supabase = await createClient ( ) ;
45- const { data : { user } } = await supabase . auth . getUser ( ) ;
46- if ( ! user ) return NextResponse . json ( { error : "Unauthorized" } , { status : 401 } ) ;
47- }
123+ const auth = await requireAuth ( ) ;
124+ if ( auth . error ) return auth . error ;
48125
49126 if ( ! dashboardClient ) {
50127 return NextResponse . json ( { error : "Sanity client not available" } , { status : 503 } ) ;
51128 }
52129
53130 try {
54131 const body = await request . json ( ) ;
132+ const validation = validateSettings ( body ) ;
55133
56- // Find or create the settings document
57- const existing = await dashboardQuery ( `*[_type == "dashboardSettings"][0]{ _id }` ) ;
58-
59- if ( existing ?. _id ) {
60- await dashboardClient . patch ( existing . _id ) . set ( body ) . commit ( ) ;
61- } else {
62- await dashboardClient . create ( {
63- _type : "dashboardSettings" ,
64- ...body ,
65- } ) ;
134+ if ( ! validation . valid ) {
135+ return NextResponse . json ( { error : validation . error } , { status : 400 } ) ;
66136 }
67137
138+ // Use createIfNotExists with deterministic ID to prevent race conditions
139+ await dashboardClient . createIfNotExists ( {
140+ _id : SETTINGS_DOC_ID ,
141+ _type : "dashboardSettings" ,
142+ ...DEFAULT_SETTINGS ,
143+ } ) ;
144+
145+ await dashboardClient . patch ( SETTINGS_DOC_ID ) . set ( validation . data ! ) . commit ( ) ;
146+
68147 return NextResponse . json ( { success : true } ) ;
69148 } catch ( error ) {
70149 console . error ( "Failed to update settings:" , error ) ;
0 commit comments