@@ -1032,6 +1032,80 @@ function FieldOptionsBody({
10321032 onChange = { ( v ) => updateSelOptions ( { protected : v } ) }
10331033 />
10341034 </ div >
1035+
1036+ { /* ── Rule-based file protection ─────────────────────────────── */ }
1037+ < div style = { { marginTop : 4 , paddingTop : 12 , borderTop : "1px solid rgba(255,255,255,0.06)" } } >
1038+ < div style = { { fontSize : 12 , fontWeight : 600 , textTransform : "uppercase" , letterSpacing : 0.5 , color : "var(--vb-muted)" , marginBottom : 8 } } >
1039+ Download protection
1040+ </ div >
1041+ </ div >
1042+ < div >
1043+ < label className = "label" > Download rule</ label >
1044+ < textarea
1045+ className = "input mono"
1046+ rows = { 3 }
1047+ value = { ( sel . options ?. [ "viewRule" ] as string | undefined ) ?? "" }
1048+ onChange = { ( e ) => {
1049+ const v = e . target . value ;
1050+ updateSelOptions ( { viewRule : v === "" ? undefined : v } ) ;
1051+ } }
1052+ placeholder = "@auth.id != '' && @auth.id = record.owner"
1053+ style = { { width : "100%" , resize : "vertical" , fontSize : 12 } }
1054+ />
1055+ < div className = "muted" style = { { fontSize : 11 , marginTop : 4 } } >
1056+ Per-field rule, AND-combined with the collection's view rule.
1057+ Empty = inherit collection rule. Extra context: < span className = "mono" > @request.headers.x_vb_ip</ span > ,
1058+ { " " } < span className = "mono" > x_vb_file_size</ span > , < span className = "mono" > x_vb_file_mime</ span > .
1059+ </ div >
1060+ </ div >
1061+ < div className = "row" style = { { justifyContent : "space-between" , alignItems : "flex-start" , padding : "10px 12px" , background : "rgba(255,255,255,0.03)" , borderRadius : 7 , gap : 12 } } >
1062+ < div style = { { flex : 1 } } >
1063+ < div style = { { fontSize : 13 , fontWeight : 500 } } > Require authentication</ div >
1064+ < div className = "muted" style = { { fontSize : 11 , marginTop : 2 } } >
1065+ Reject anonymous downloads even when the collection's view rule is public.
1066+ </ div >
1067+ </ div >
1068+ < Toggle
1069+ on = { ! ! sel . options ?. [ "requireAuth" ] }
1070+ onChange = { ( v ) => updateSelOptions ( { requireAuth : v || undefined } ) }
1071+ />
1072+ </ div >
1073+ < div className = "row" style = { { justifyContent : "space-between" , alignItems : "flex-start" , padding : "10px 12px" , background : "rgba(255,255,255,0.03)" , borderRadius : 7 , gap : 12 } } >
1074+ < div style = { { flex : 1 } } >
1075+ < div style = { { fontSize : 13 , fontWeight : 500 } } > One-time download token</ div >
1076+ < div className = "muted" style = { { fontSize : 11 , marginTop : 2 } } >
1077+ Each token works for a single fetch. Replay returns < span className = "mono" > 410 Gone</ span > .
1078+ </ div >
1079+ </ div >
1080+ < Toggle
1081+ on = { ! ! sel . options ?. [ "oneTimeToken" ] }
1082+ onChange = { ( v ) => updateSelOptions ( { oneTimeToken : v || undefined } ) }
1083+ />
1084+ </ div >
1085+ < div className = "row" style = { { justifyContent : "space-between" , alignItems : "flex-start" , padding : "10px 12px" , background : "rgba(255,255,255,0.03)" , borderRadius : 7 , gap : 12 } } >
1086+ < div style = { { flex : 1 } } >
1087+ < div style = { { fontSize : 13 , fontWeight : 500 } } > Bind token to IP</ div >
1088+ < div className = "muted" style = { { fontSize : 11 , marginTop : 2 } } >
1089+ Token rejects requests from a different IP. Incompatible with mobile NAT — opt-in.
1090+ </ div >
1091+ </ div >
1092+ < Toggle
1093+ on = { ! ! sel . options ?. [ "bindTokenIp" ] }
1094+ onChange = { ( v ) => updateSelOptions ( { bindTokenIp : v || undefined } ) }
1095+ />
1096+ </ div >
1097+ < div className = "row" style = { { justifyContent : "space-between" , alignItems : "flex-start" , padding : "10px 12px" , background : "rgba(255,255,255,0.03)" , borderRadius : 7 , gap : 12 } } >
1098+ < div style = { { flex : 1 } } >
1099+ < div style = { { fontSize : 13 , fontWeight : 500 } } > Audit downloads</ div >
1100+ < div className = "muted" style = { { fontSize : 11 , marginTop : 2 } } >
1101+ Append a < span className = "mono" > files.download</ span > row to the audit log per fetch.
1102+ </ div >
1103+ </ div >
1104+ < Toggle
1105+ on = { ! ! sel . options ?. [ "auditDownloads" ] }
1106+ onChange = { ( v ) => updateSelOptions ( { auditDownloads : v || undefined } ) }
1107+ />
1108+ </ div >
10351109 </ >
10361110 ) }
10371111 </ >
0 commit comments