mirror of
https://github.com/pocketbase/pocketbase.git
synced 2026-10-31 09:20:25 +01:00
[#6410] added rate limit option to exclude IPs/CIDR subnets
This commit is contained in:
1 parent
24d72877e9
commit
5a144e1342
12 files changed
+453
-150
No files matched your search
@@ -23,6 +23,10 @@
|
||||
./pocketbase superuser ips 127.0.0.1 10.0.0.0 --dir=/custom/path/to/pb_data
|
||||
```
|
||||
|
||||
- Added rate limit option to exclude IPs/CIDR subnets ([#6410](https://github.com/pocketbase/pocketbase/issues/6410)).
|
||||
|
||||
- (@todo) Bumped min Go GitHub action version to 1.26.3 because it comes with some [minor bug and security fixes](https://github.com/golang/go/issues?q=milestone%3AGo1.26.3).
|
||||
|
||||
|
||||
## v0.37.5
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ func checkCollectionRateLimit(e *core.RequestEvent, collection *core.Collection,
|
||||
|
||||
// isIPInList checks if the specified IP is in a list of other individual IPs or subnets.
|
||||
func isIPInList(ipsOrSubnets []string, ip string) bool {
|
||||
if ip == "" || len(ipsOrSubnets) == 0 {
|
||||
if len(ipsOrSubnets) == 0 || ip == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -189,7 +189,9 @@ func checkRateLimit(e *core.RequestEvent, rtId string, rule core.RateLimitRule)
|
||||
}
|
||||
|
||||
func skipRateLimit(e *core.RequestEvent) bool {
|
||||
return !e.App.Settings().RateLimits.Enabled || e.HasSuperuserAuth()
|
||||
return !e.App.Settings().RateLimits.Enabled ||
|
||||
e.HasSuperuserAuth() ||
|
||||
isIPInList(e.App.Settings().RateLimits.ExcludedIPs, e.RealIP())
|
||||
}
|
||||
|
||||
var defaultAuthAudience = []string{core.RateLimitRuleAudienceAll, core.RateLimitRuleAudienceAuth}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
"github.com/pocketbase/pocketbase/tools/hook"
|
||||
)
|
||||
|
||||
func TestDefaultRateLimitMiddleware(t *testing.T) {
|
||||
@@ -85,9 +86,8 @@ func TestDefaultRateLimitMiddleware(t *testing.T) {
|
||||
{"/norate", 0, false, 200},
|
||||
|
||||
{"/rate/a", 0, false, 200},
|
||||
{"/rate/a", 800, false, 200}, // (fixed window check) wait enough to ensure that it can't fit more than 2 requests in 1s
|
||||
{"/rate/a", 600, false, 200},
|
||||
{"/rate/a", 850, false, 200},
|
||||
{"/rate/a", 900, false, 200}, // (fixed window check) wait enough to ensure that it can't fit more than 2 requests in 1s
|
||||
{"/rate/a", 900, false, 200},
|
||||
{"/rate/a", 0, false, 200},
|
||||
{"/rate/a", 0, false, 429},
|
||||
{"/rate/a", 0, false, 429},
|
||||
@@ -160,3 +160,163 @@ func TestDefaultRateLimitMiddleware(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRateLimitMiddlewareSkipChecks(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
app.Settings().RateLimits.Enabled = true
|
||||
app.Settings().RateLimits.Rules = []core.RateLimitRule{
|
||||
{
|
||||
Label: "/rate",
|
||||
MaxRequests: 1,
|
||||
Duration: 5,
|
||||
},
|
||||
}
|
||||
|
||||
pbRouter, err := apis.NewRouter(app)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// just for the exclude tests - load the user IP from a query param
|
||||
pbRouter.Bind(&hook.Handler[*core.RequestEvent]{
|
||||
Priority: apis.DefaultRateLimitMiddlewarePriority - 1,
|
||||
Func: func(e *core.RequestEvent) error {
|
||||
testIp := e.Request.URL.Query().Get("testIP")
|
||||
if testIp != "" {
|
||||
e.Request.Header.Set("x-test-ip", testIp)
|
||||
}
|
||||
|
||||
return e.Next()
|
||||
},
|
||||
})
|
||||
|
||||
pbRouter.GET("/rate", func(e *core.RequestEvent) error {
|
||||
return e.String(200, "test")
|
||||
})
|
||||
|
||||
mux, err := pbRouter.BuildMux()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkStatusCodes := func(t *testing.T, got []int, expected []int) {
|
||||
if len(expected) != len(got) {
|
||||
t.Fatalf("Expected status codes %v, got %v", expected, got)
|
||||
}
|
||||
|
||||
for i, item := range expected {
|
||||
if got[i] != item {
|
||||
t.Fatalf("Expected %d status code to be %d, got %d:\n%v", i, item, got[i], got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("base check", func(t *testing.T) {
|
||||
app.Settings().RateLimits.Enabled = true
|
||||
|
||||
statusCodes := []int{}
|
||||
for range 3 {
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/rate", nil)
|
||||
|
||||
mux.ServeHTTP(rec, req)
|
||||
|
||||
result := rec.Result()
|
||||
|
||||
statusCodes = append(statusCodes, result.StatusCode)
|
||||
}
|
||||
|
||||
checkStatusCodes(t, statusCodes, []int{200, 429, 429})
|
||||
})
|
||||
|
||||
t.Run("disabled rate limiter", func(t *testing.T) {
|
||||
app.Settings().RateLimits.Enabled = false
|
||||
|
||||
statusCodes := []int{}
|
||||
for range 3 {
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/rate", nil)
|
||||
|
||||
mux.ServeHTTP(rec, req)
|
||||
|
||||
result := rec.Result()
|
||||
|
||||
statusCodes = append(statusCodes, result.StatusCode)
|
||||
}
|
||||
|
||||
checkStatusCodes(t, statusCodes, []int{200, 200, 200})
|
||||
})
|
||||
|
||||
t.Run("authenticated as superuser", func(t *testing.T) {
|
||||
app.Settings().RateLimits.Enabled = true
|
||||
|
||||
superuser, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
token, err := superuser.NewAuthToken()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
statusCodes := []int{}
|
||||
for range 3 {
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/rate", nil)
|
||||
req.Header.Add("Authorization", token)
|
||||
|
||||
mux.ServeHTTP(rec, req)
|
||||
|
||||
result := rec.Result()
|
||||
|
||||
statusCodes = append(statusCodes, result.StatusCode)
|
||||
}
|
||||
|
||||
checkStatusCodes(t, statusCodes, []int{200, 200, 200})
|
||||
})
|
||||
|
||||
t.Run("excludedIPs (different)", func(t *testing.T) {
|
||||
app.Settings().RateLimits.Enabled = true
|
||||
app.Settings().RateLimits.ExcludedIPs = []string{"10.0.0.0"}
|
||||
app.Settings().TrustedProxy.Headers = []string{"x-test-ip"}
|
||||
|
||||
statusCodes := []int{}
|
||||
for range 3 {
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/rate", nil)
|
||||
req.Header.Set("x-test-ip", "127.0.0.1")
|
||||
|
||||
mux.ServeHTTP(rec, req)
|
||||
|
||||
result := rec.Result()
|
||||
|
||||
statusCodes = append(statusCodes, result.StatusCode)
|
||||
}
|
||||
|
||||
checkStatusCodes(t, statusCodes, []int{200, 429, 429})
|
||||
})
|
||||
|
||||
t.Run("excludedIPs (match)", func(t *testing.T) {
|
||||
app.Settings().RateLimits.Enabled = true
|
||||
app.Settings().RateLimits.ExcludedIPs = []string{"127.0.0.1"}
|
||||
app.Settings().TrustedProxy.Headers = []string{"x-test-ip"}
|
||||
|
||||
statusCodes := []int{}
|
||||
for range 3 {
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest("GET", "/rate", nil)
|
||||
req.Header.Set("x-test-ip", "127.0.0.1")
|
||||
|
||||
mux.ServeHTTP(rec, req)
|
||||
|
||||
result := rec.Result()
|
||||
|
||||
statusCodes = append(statusCodes, result.StatusCode)
|
||||
}
|
||||
|
||||
checkStatusCodes(t, statusCodes, []int{200, 200, 200})
|
||||
})
|
||||
}
|
||||
+11
-3
@@ -290,7 +290,7 @@ func (s *Settings) PostValidate(ctx context.Context, app App) error {
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
return validation.ValidateStructWithContext(ctx, s,
|
||||
validation.Field(&s.SuperuserIPs, validation.Each(validation.By(validators.IPOrSubnet))),
|
||||
validation.Field(&s.SuperuserIPs, validation.Each(validation.Required, validation.By(validators.IPOrSubnet))),
|
||||
validation.Field(&s.Meta),
|
||||
validation.Field(&s.Logs),
|
||||
validation.Field(&s.SMTP),
|
||||
@@ -604,8 +604,9 @@ func (c TrustedProxyConfig) Validate() error {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type RateLimitsConfig struct {
|
||||
Rules []RateLimitRule `form:"rules" json:"rules"`
|
||||
Enabled bool `form:"enabled" json:"enabled"`
|
||||
Rules []RateLimitRule `form:"rules" json:"rules"`
|
||||
ExcludedIPs []string `form:"excludedIPs" json:"excludedIPs"`
|
||||
Enabled bool `form:"enabled" json:"enabled"`
|
||||
}
|
||||
|
||||
// FindRateLimitRule returns the first matching rule based on the provided labels.
|
||||
@@ -650,6 +651,9 @@ func (c RateLimitsConfig) MarshalJSON() ([]byte, error) {
|
||||
if c.Rules == nil {
|
||||
c.Rules = []RateLimitRule{}
|
||||
}
|
||||
if c.ExcludedIPs == nil {
|
||||
c.ExcludedIPs = []string{}
|
||||
}
|
||||
|
||||
return json.Marshal(alias(c))
|
||||
}
|
||||
@@ -662,6 +666,10 @@ func (c RateLimitsConfig) Validate() error {
|
||||
validation.When(c.Enabled, validation.Required),
|
||||
validation.By(checkUniqueRuleLabel),
|
||||
),
|
||||
validation.Field(
|
||||
&c.ExcludedIPs,
|
||||
validation.Each(validation.Required, validation.By(validators.IPOrSubnet)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ func TestSettings_DBExport(t *testing.T) {
|
||||
valueStr = string(export["value"].([]byte))
|
||||
}
|
||||
|
||||
expected := `{"superuserIPs":[],"smtp":{"enabled":false,"port":0,"host":"smtp_host","username":"smtp_username","password":"","authMethod":"","tls":false,"localName":""},"backups":{"cron":"* * * * *","cronMaxKeep":0,"s3":{"enabled":true,"bucket":"","region":"","endpoint":"","accessKey":"","forcePathStyle":false}},"s3":{"enabled":false,"bucket":"","region":"","endpoint":"s3_endpoint","accessKey":"","secret":"s3_secret","forcePathStyle":false},"meta":{"accentColor":"","appName":"test_app_name","appURL":"","senderName":"","senderAddress":"","hideControls":false},"rateLimits":{"rules":[],"enabled":true},"trustedProxy":{"headers":[],"useLeftmostIP":true},"batch":{"enabled":false,"maxRequests":0,"timeout":15,"maxBodySize":0},"logs":{"maxDays":123,"minLevel":0,"logIP":false,"logAuthId":false}}`
|
||||
expected := `{"superuserIPs":[],"smtp":{"enabled":false,"port":0,"host":"smtp_host","username":"smtp_username","password":"","authMethod":"","tls":false,"localName":""},"backups":{"cron":"* * * * *","cronMaxKeep":0,"s3":{"enabled":true,"bucket":"","region":"","endpoint":"","accessKey":"","forcePathStyle":false}},"s3":{"enabled":false,"bucket":"","region":"","endpoint":"s3_endpoint","accessKey":"","secret":"s3_secret","forcePathStyle":false},"meta":{"accentColor":"","appName":"test_app_name","appURL":"","senderName":"","senderAddress":"","hideControls":false},"rateLimits":{"rules":[],"excludedIPs":[],"enabled":true},"trustedProxy":{"headers":[],"useLeftmostIP":true},"batch":{"enabled":false,"maxRequests":0,"timeout":15,"maxBodySize":0},"logs":{"maxDays":123,"minLevel":0,"logIP":false,"logAuthId":false}}`
|
||||
if valueStr != expected {
|
||||
t.Fatalf("Expected exported settings\n%s\ngot\n%s", expected, valueStr)
|
||||
}
|
||||
@@ -180,7 +180,7 @@ func TestSettingsMarshalJSON(t *testing.T) {
|
||||
}
|
||||
rawStr := string(raw)
|
||||
|
||||
expected := `{"superuserIPs":[],"smtp":{"enabled":false,"port":0,"host":"","username":"abc","authMethod":"","tls":false,"localName":""},"backups":{"cron":"","cronMaxKeep":0,"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","forcePathStyle":false}},"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","forcePathStyle":false},"meta":{"accentColor":"","appName":"test123","appURL":"","senderName":"","senderAddress":"","hideControls":false},"rateLimits":{"rules":[],"enabled":false},"trustedProxy":{"headers":[],"useLeftmostIP":false},"batch":{"enabled":false,"maxRequests":0,"timeout":0,"maxBodySize":0},"logs":{"maxDays":0,"minLevel":0,"logIP":false,"logAuthId":false}}`
|
||||
expected := `{"superuserIPs":[],"smtp":{"enabled":false,"port":0,"host":"","username":"abc","authMethod":"","tls":false,"localName":""},"backups":{"cron":"","cronMaxKeep":0,"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","forcePathStyle":false}},"s3":{"enabled":false,"bucket":"","region":"","endpoint":"","accessKey":"","forcePathStyle":false},"meta":{"accentColor":"","appName":"test123","appURL":"","senderName":"","senderAddress":"","hideControls":false},"rateLimits":{"rules":[],"excludedIPs":[],"enabled":false},"trustedProxy":{"headers":[],"useLeftmostIP":false},"batch":{"enabled":false,"maxRequests":0,"timeout":0,"maxBodySize":0},"logs":{"maxDays":0,"minLevel":0,"logIP":false,"logAuthId":false}}`
|
||||
|
||||
if rawStr != expected {
|
||||
t.Fatalf("Expected\n%v\ngot\n%v", expected, rawStr)
|
||||
@@ -196,7 +196,7 @@ func TestSettingsValidate(t *testing.T) {
|
||||
s := app.Settings()
|
||||
|
||||
// set invalid settings data
|
||||
s.SuperuserIPs = []string{"127.0.0.1", "invalid"}
|
||||
s.SuperuserIPs = []string{"127.0.0.1", ""}
|
||||
s.Meta.AppName = ""
|
||||
s.Logs.MaxDays = -10
|
||||
s.SMTP.Enabled = true
|
||||
@@ -597,7 +597,8 @@ func TestRateLimitsConfigValidate(t *testing.T) {
|
||||
{
|
||||
"invalid data",
|
||||
core.RateLimitsConfig{
|
||||
Enabled: true,
|
||||
Enabled: true,
|
||||
ExcludedIPs: []string{"", "127.0.0.1"},
|
||||
Rules: []core.RateLimitRule{
|
||||
{
|
||||
Label: "/123abc/",
|
||||
@@ -611,12 +612,13 @@ func TestRateLimitsConfigValidate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
[]string{"rules"},
|
||||
[]string{"rules", "excludedIPs"},
|
||||
},
|
||||
{
|
||||
"valid data",
|
||||
core.RateLimitsConfig{
|
||||
Enabled: true,
|
||||
Enabled: true,
|
||||
ExcludedIPs: []string{"127.0.0.1", "10.0.0.1/20"},
|
||||
Rules: []core.RateLimitRule{
|
||||
{
|
||||
Label: "123_abc",
|
||||
|
||||
@@ -63,7 +63,7 @@ To target the newly submitted ones you can use @request.body.*.`)})],name:`updat
|
||||
{MMM} {DD}`,null,`
|
||||
{MMM} {DD}`,null,null,null,1],[60,`{h}:{mm}{aa}`,`
|
||||
{MMM} {DD}`,null,`
|
||||
{MMM} {DD}`,null,null,null,1]],grid:{show:!0,stroke:o,width:1},ticks:{show:!0,stroke:o,width:1,size:5}},{show:!0,stroke:a,grid:{show:!0,stroke:o,width:1},ticks:{show:!0,stroke:o,width:1,size:5}}],plugins:[Rr(),Ir(r),Lr(r)]};e._uplot?.destroy(),e._uplot=new uPlot(l,n,e)}function Ir(e){let n;return{hooks:{init:r=>{r.over.ondblclick=n=>{e.zoom={}},n=watch(()=>{!e.zoom?.min||!e.zoom?.max?Pr(r):r.setScale(`x`,{min:e.zoom.min,max:e.zoom.max})})},destroy:e=>{n?.unwatch()},setSelect:n=>{n.select.width>0&&(e.zoom={min:n.posToVal(n.select.left,`x`),max:n.posToVal(n.select.left+n.select.width,`x`)})}}}}function Lr(e){let n;return{hooks:{init:r=>{let i=r.root.querySelectorAll(`.u-axis`);if(!i.length){console.warn(`xPanPlugin requires x axis to be defined`);return}i[0].addEventListener(`mousedown`,i=>{if(!e.zoom?.min)return;let a=i.clientX,{min:o,max:s}=r.scales.x,c=r.bbox.width,l=(s-o)/(c/uPlot.pxRatio),u=i=>{let c=(a-i.clientX)*l;r.setScale(`x`,{min:o+c,max:s+c}),clearTimeout(n),n=setTimeout(()=>{r?.scales?.x&&(e.zoom={min:r.scales.x.min,max:r.scales.x.max})},100)},d=e=>{document.removeEventListener(`mousemove`,u),document.removeEventListener(`mouseup`,d)};document.addEventListener(`mousemove`,u),document.addEventListener(`mouseup`,d)})},destroy:e=>{n&&clearTimeout(n)}}}}function Rr(e){let n;return{hooks:{init:e=>{let r=e.over;n=store({date:``,total:0,left:0,top:0,show:!1});let i=t.div({className:()=>`chart-tooltip ${n.show?``:`hidden`}`,onmount(e){e._positionWatcher?.unwatch(),e._positionWatcher=watch(()=>[n.left,n.top],()=>{if(!e)return;let i=e.getBoundingClientRect(),a=n.left;a<0?a=0:a+i.width>r.clientWidth&&(a=r.clientWidth-i.width),e.style.left=a+`px`;let o=n.top-i.height-5;o<0&&(o=n.top+5,o+i.high>r.clientHeight&&(o=r.clientHeight-i.height)),e.style.top=o+`px`})},onunmount(e){e._positionWatcher?.unwatch()}},t.div({className:`content-primary`},()=>`${n.total} ${n.total==1?`request`:`requests`}`),t.div({className:`content-secondary`},()=>n.date));r.appendChild(i),r.addEventListener(`mouseleave`,()=>{n&&(n.show=!1)})},destroy:()=>{n.show=!1},setCursor:e=>{if(!n)return;let r=e.data[0][e.cursor.idx]||0,i=e.data[1][e.cursor.idx]||0;if(r==0||i==0){n.show=!1;return}n.show=!0,n.total=i;let a=new Date(r*1e3),o=new Date(r*1e3+36e5),s=a.toLocaleString(`default`,{month:`short`}),c=a.getDate().toString().padStart(2,`0`);n.date=`${s} ${c} ${zr(a)}-${zr(o)}`,n.left=Math.round(e.valToPos(r,`x`)),n.top=Math.round(e.valToPos(i,`y`))}}}}function zr(e){let n=e.getHours(),r=n>=12?`pm`:`am`;return n=n%12||12,n+r}var Br=50;function Vr(e){let n=store({logs:[],lastLoadCount:0,lastPage:1,bulkSelected:{},get canLoadMore(){return n.lastLoadCount>=Br},get totalSelected(){return Object.keys(n.bulkSelected).length},get areAllSelected(){return n.logs.length&&n.logs.length==n.totalSelected}}),r;async function i(i=!1){e.isListLoading=!0;try{let a;i?(a=1,r=new Date().toISOString().replace(`T`,` `)):a=n.lastPage+1;let o=(e.presets||[]).concat(app.utils.normalizeSearchFilter(e.filter,[`level`,`message`,`data`]));if(e.zoom?.min&&e.zoom?.max){let n=new Date(e.zoom.min*1e3);n.setSeconds(0),n.setMilliseconds(0);let r=app.utils.toRFC3339Datetime(n),i=new Date(e.zoom.max*1e3);i.setSeconds(59),i.setMilliseconds(999);let a=app.utils.toRFC3339Datetime(i);o.push(`created >= "${r}" && created <= "${a}"`)}else o.push(`created <= "${r}"`);let s=await app.pb.logs.getList(a,Br,{skipTotal:1,sort:`-@rowid`,requestKey:`logs_list`,filter:o.filter(Boolean).map(e=>`(`+e+`)`).join(`&&`)});s.page==1&&(n.logs=[],n.bulkSelected={}),n.lastPage=s.page,n.lastLoadCount=s.items.length;for(let e=0;e<s.items.length;e++)app.utils.pushOrReplaceObject(n.logs,s.items[e]),e>1&&e%20==0&&await new Promise(e=>setTimeout(e,20));e.isListLoading=!1,e.isFirstLoadReady||=!0}catch(n){n.isAbort||(e.isListLoading=!1,app.checkApiError(n))}}function a(e=!0){let r={};if(e)for(let e of n.logs)r[e.id]=e;n.bulkSelected=r}function o(e){let n=[];if(!e.data)return n;if(e.data.type==`request`){for(let r of[`status`,`execTime`,`auth`,`authId`,`userIP`])e.data[r]!==void 0&&n.push({key:r});e.data.referer&&!e.data.referer.includes(window.location.host)&&n.push({key:`referer`})}else{let r=Object.keys(e.data);for(let e of r)e!=`error`&&e!=`details`&&n.length<6&&n.push({key:e})}return e.data.error&&n.push({key:`error`,label:`danger`}),e.data.details&&n.push({key:`details`,label:`warning`}),n}let s=/[-:\. ]/gi;function c(){let e=Object.values(n.bulkSelected).sort((e,n)=>e.created<n.created?1:e.created>n.created?-1:0);if(!e.length)return;if(e.length==1)return app.utils.downloadJSON(e[0],`log_`+e[0].created.replaceAll(s,``)+`.json`);let r=e[0].created.replaceAll(s,``),i=e[e.length-1].created.replaceAll(s,``);return app.utils.downloadJSON(e,`${e.length}_logs_${i}_to_${r}.json`)}let l=[];return t.div({pbEvent:`logsList`,className:`page-table-wrapper`,onmount(n){l.push(watch(()=>[e.filter,e.presets?.length],()=>{e.zoom={}}),watch(()=>[e.reset,e.filter,e.presets?.length,e.zoom?.min,e.zoom?.max],()=>{i(!0),n&&(n.scrollT
Line truncated
|
||||
{MMM} {DD}`,null,null,null,1]],grid:{show:!0,stroke:o,width:1},ticks:{show:!0,stroke:o,width:1,size:5}},{show:!0,stroke:a,grid:{show:!0,stroke:o,width:1},ticks:{show:!0,stroke:o,width:1,size:5}}],plugins:[Rr(),Ir(r),Lr(r)]};e._uplot?.destroy(),e._uplot=new uPlot(l,n,e)}function Ir(e){let n;return{hooks:{init:r=>{r.over.ondblclick=n=>{e.zoom={}},n=watch(()=>{!e.zoom?.min||!e.zoom?.max?Pr(r):r.setScale(`x`,{min:e.zoom.min,max:e.zoom.max})})},destroy:e=>{n?.unwatch()},setSelect:n=>{n.select.width>0&&(e.zoom={min:n.posToVal(n.select.left,`x`),max:n.posToVal(n.select.left+n.select.width,`x`)})}}}}function Lr(e){let n;return{hooks:{init:r=>{let i=r.root.querySelectorAll(`.u-axis`);if(!i.length){console.warn(`xPanPlugin requires x axis to be defined`);return}i[0].addEventListener(`mousedown`,i=>{if(!e.zoom?.min)return;let a=i.clientX,{min:o,max:s}=r.scales.x,c=r.bbox.width,l=(s-o)/(c/uPlot.pxRatio),u=i=>{let c=(a-i.clientX)*l;r.setScale(`x`,{min:o+c,max:s+c}),clearTimeout(n),n=setTimeout(()=>{r?.scales?.x&&(e.zoom={min:r.scales.x.min,max:r.scales.x.max})},100)},d=e=>{document.removeEventListener(`mousemove`,u),document.removeEventListener(`mouseup`,d)};document.addEventListener(`mousemove`,u),document.addEventListener(`mouseup`,d)})},destroy:e=>{n&&clearTimeout(n)}}}}function Rr(e){let n;return{hooks:{init:e=>{let r=e.over;n=store({date:``,total:0,left:0,top:0,show:!1});let i=t.div({className:()=>`chart-tooltip ${n.show?``:`hidden`}`,onmount(e){e._positionWatcher?.unwatch(),e._positionWatcher=watch(()=>[n.left,n.top],()=>{if(!e)return;let i=e.getBoundingClientRect(),a=n.left;a<0?a=0:a+i.width>r.clientWidth&&(a=r.clientWidth-i.width),e.style.left=a+`px`;let o=n.top-i.height-5;o<0&&(o=n.top+5,o+i.high>r.clientHeight&&(o=r.clientHeight-i.height)),e.style.top=o+`px`})},onunmount(e){e._positionWatcher?.unwatch()}},t.div({className:`content-primary`},()=>`${n.total} ${n.total==1?`request`:`requests`}`),t.div({className:`content-secondary`},()=>n.date));r.appendChild(i),r.addEventListener(`mouseleave`,()=>{n&&(n.show=!1)})},destroy:()=>{n.show=!1},setCursor:e=>{if(!n)return;let r=e.data[0][e.cursor.idx]||0,i=e.data[1][e.cursor.idx]||0;if(r==0||i==0){n.show=!1;return}n.show=!0,n.total=i;let a=new Date(r*1e3),o=new Date(r*1e3+36e5),s=a.toLocaleString(`default`,{month:`short`}),c=a.getDate().toString().padStart(2,`0`);n.date=`${s} ${c} ${zr(a)}-${zr(o)}`,n.left=Math.round(e.valToPos(r,`x`)),n.top=Math.round(e.valToPos(i,`y`))}}}}function zr(e){let n=e.getHours(),r=n>=12?`pm`:`am`;return n=n%12||12,n+r}var Br=50;function Vr(e){let n=store({logs:[],lastLoadCount:0,lastPage:1,bulkSelected:{},get canLoadMore(){return n.lastLoadCount>=Br},get totalSelected(){return Object.keys(n.bulkSelected).length},get areAllSelected(){return n.logs.length&&n.logs.length==n.totalSelected}}),r;async function i(i=!1){e.isListLoading=!0;try{let a;i?(a=1,r=new Date().toISOString().replace(`T`,` `)):a=n.lastPage+1;let o=(e.presets||[]).concat(app.utils.normalizeSearchFilter(e.filter,[`level`,`message`,`data`]));if(e.zoom?.min&&e.zoom?.max){let n=new Date(e.zoom.min*1e3);n.setSeconds(0),n.setMilliseconds(0);let r=app.utils.toRFC3339Datetime(n),i=new Date(e.zoom.max*1e3);i.setSeconds(59),i.setMilliseconds(999);let a=app.utils.toRFC3339Datetime(i);o.push(`created >= "${r}" && created <= "${a}"`)}else o.push(`created <= "${r}"`);let s=await app.pb.logs.getList(a,Br,{skipTotal:1,sort:`-@rowid`,requestKey:`logs_list`,filter:o.filter(Boolean).map(e=>`(`+e+`)`).join(`&&`)});s.page==1&&(n.logs=[],n.bulkSelected={}),n.lastPage=s.page,n.lastLoadCount=s.items.length;for(let e=0;e<s.items.length;e++)app.utils.pushOrReplaceObject(n.logs,s.items[e]),e>1&&e%20==0&&await new Promise(e=>setTimeout(e,20));e.isListLoading=!1,e.isFirstLoadReady||=!0}catch(n){n.isAbort||(e.isListLoading=!1,app.checkApiError(n))}}function a(e=!0){let r={};if(e)for(let e of n.logs)r[e.id]=e;n.bulkSelected=r}function o(e){let n=[];if(!e.data)return n;if(e.data.type==`request`){for(let r of[`status`,`execTime`,`auth`,`authId`,`userIP`])e.data[r]!==void 0&&n.push({key:r});e.data.referer&&!e.data.referer.includes(window.location.host)&&n.push({key:`referer`})}else{let r=Object.keys(e.data);for(let e of r)e!=`error`&&e!=`details`&&n.length<6&&n.push({key:e})}return e.data.error&&n.push({key:`error`,label:`danger`}),e.data.details&&n.push({key:`details`,label:`warning`}),n}let s=/[-:\. ]/gi;function c(){let e=Object.values(n.bulkSelected).sort((e,n)=>e.created<n.created?1:e.created>n.created?-1:0);if(!e.length)return;if(e.length==1)return app.utils.downloadJSON(e[0],`log_`+e[0].created.replaceAll(s,``)+`.json`);let r=e[0].created.replaceAll(s,``),i=e[e.length-1].created.replaceAll(s,``);return app.utils.downloadJSON(e,`${e.length}_logs_${i}_to_${r}.json`)}let l=[];return t.div({pbEvent:`logsList`,className:`page-table-wrapper`,onmount(n){l.push(watch(()=>[e.filter,e.presets?.length],()=>{e.zoom={}}),watch(()=>[e.reset,e.filter,e.presets?.length,e.zoom?.min,e.zoom?.max],()=>{i(!0),n&&(n.scrollT
Line truncated
|
||||
It is recommend to list it as trusted.`,`right`)});if(i.isEnabled&&i.possibleProxyHeader&&!e.formSettings.trustedProxy.headers.includes(i.possibleProxyHeader))return t.i({className:`ri-alert-line txt-hint`,ariaDescription:app.attrs.tooltip(`The configured proxy header doesn't match with the detected one.`,`right`)})},t.div({className:`flex-fill`}),()=>i.isEnabled?t.span({className:`label success`},`Enabled`):t.span({className:`label`},`Disabled`),()=>{if(!app.utils.isEmpty(app.store.errors?.trustedProxy))return t.i({className:`ri-error-warning-fill txt-danger`,ariaDescription:app.attrs.tooltip(`Has errors`,`left`)})}),t.p({className:`m-t-0`},`Below you should see your real IP. If not - configure the correct proxy header for your environment.`),t.div({className:`alert info m-b-sm`},t.div({className:`flex gap-5`},t.span(null,`Resolved user IP:`),t.strong(null,()=>i.isLoading?`...`:i.realIP||`N/A`)),t.div({className:`flex gap-5`},t.span(null,`Detected proxy header:`),t.strong(null,()=>i.isLoading?`...`:i.possibleProxyHeader||`N/A`))),t.div({className:`content m-b-sm`},t.p(null,`
|
||||
When PocketBase is deployed on platforms like Fly or it is accessible through proxies such as
|
||||
NGINX, requests from different users will originate from the same IP address (the IP of the proxy
|
||||
@@ -72,7 +72,7 @@ It is recommend to list it as trusted.`,`right`)});if(i.isEnabled&&i.possiblePro
|
||||
In this case to retrieve the actual user IP (used for rate limiting, logging, etc.) you need to
|
||||
properly configure your proxy and list below the trusted headers that PocketBase could use to
|
||||
extract the user IP.
|
||||
`),t.p({className:`txt-bold`},`When using such proxy, to avoid spoofing it is recommended to:`),t.ul({className:`txt-bold`},t.li(null,`use headers that are controlled only by the proxy and cannot be manually set by the users`),t.li(null,`make sure that the PocketBase server can be accessed ONLY through the proxy`)),t.p(null,`You can clear the headers field if PocketBase is not deployed behind a proxy.`)),t.div({className:`grid sm`},t.div({className:`col-lg-9`},t.div({className:`fields`},t.div({className:`field`},t.label({htmlFor:`trustedProxy.headers`},`Trusted IP proxy headers`),t.input({type:`text`,id:`trustedProxy.headers`,name:`trustedProxy.headers`,placeholder:`Leave empty to disable`,value:()=>app.utils.joinNonEmpty(e.formSettings.trustedProxy.headers),oninput:n=>{let r=app.utils.splitNonEmpty(n.target.value,`,`),i=app.utils.joinNonEmpty(r);app.utils.joinNonEmpty(e.formSettings.trustedProxy.headers)!=i&&(e.formSettings.trustedProxy.headers=r)}})),t.div({className:`field addon`},t.button({type:`button`,className:()=>`btn sm secondary transparent ${app.utils.isEmpty(e.formSettings.trustedProxy.headers)?`hidden`:``}`,onclick:()=>{e.formSettings.trustedProxy.headers=[]}},t.span({className:`txt`},`Clear`)))),t.div({className:`field-help`},`Comma separated list of headers such as: `,t.div({className:`inline-flex gap-5`},()=>i.suggestedProxyHeaders.map(n=>t.div({role:`button`,className:`label sm link-primary`,onclick:()=>{e.formSettings.trustedProxy.headers=[n]},textContent:n}))))),t.div({className:`col-lg-3`},t.div({className:`field`},t.label({htmlFor:`trustedProxy.useLeftmostIP`},t.span({className:`txt`},`IP priority`),t.i({className:`ri-information-line tooltip-right`,ariaDescription:app.attrs.tooltip(`This is in case the proxy returns more than 1 IP as header value. The rightmost IP is usually considered to be the more trustworthy but this could vary depending on the proxy.`)})),app.components.select({id:`trustedProxy.useLeftmostIP`,name:`trustedProxy.useLeftmostIP`,options:r,required:!0,value:()=>e.formSettings.trustedProxy.useLeftmostIP||!1,onchange:n=>{e.formSettings.trustedProxy.useLeftmostIP=n?.[0]?.value}})))))}function Zr(){app.store.title=`Application settings`;let e=store({isLoading:!1,isSaving:!1,formSettings:null,originalFormSettings:null,get originalFormSettingsHash(){return JSON.stringify(e.originalFormSettings)},get hasChanges(){return e.originalFormSettingsHash!=JSON.stringify(e.formSettings)}});n();async function n(){e.isLoading=!0;try{o(await app.pb.settings.getAll()),e.isLoading=!1}catch(e){e.isAbort||app.checkApiError(e)}}function r(){return JSON.stringify(e.formSettings?.superuserIPs)!=JSON.stringify(e.originalFormSettings?.superuserIPs)}async function i(){let n=app.utils.toArray(e.formSettings?.superuserIPs);return n.length?app.modals.confirm(t.div({className:`txt-center`},t.h6(null,`The ONLY allowed superuser IPs will change to: `,t.br(),t.strong(null,n.join(`, `))),t.p(null,`Please make sure that your IP is in the list or you'll be locked.`),t.p({className:`txt-hint`},`In case of lockout, you can reset the setting with the `,t.a({href:`https://pocketbase.io/docs/going-to-production/#limit-superusers-to-specific-ipssubnets`,target:`_blank`,rel:`noopener noreferrer`,className:`link-primary txt-bold txt-sm`},t.code(null,`superuser ips`,t.i({ariaHidden:!0,className:`ri-arrow-right-up-line txt-sm`}))),` console command.`)),()=>a(),null,{yesButton:`Yes, save changes`}):a()}async function a(){if(!(e.isSaving||!e.hasChanges)){e.isSaving=!0,e.formSettings.rateLimits.rules=qr(e.formSettings.rateLimits.rules);try{let n=app.utils.filterRedactedProps(e.formSettings),i=await app.pb.settings.update(n);if(r())try{await app.pb.collection(`_superusers`).authRefresh()}catch{app.pb.authStore.clear()}o(i),app.toasts.success(`Successfully saved application settings.`)}catch(e){app.checkApiError(e)}e.isSaving=!1}}function o(n={}){if(app.store.settings=JSON.parse(JSON.stringify(n)),!n.meta?.accentColor){let e=window.getComputedStyle(document.documentElement)?.getPropertyValue(`--accentColor`);e?.startsWith(`#`)&&(n.meta=n.meta||{},n.meta.accentColor=e.toLowerCase()||``)}e.originalFormSettings={superuserIPs:n.superuserIPs||[],meta:n.meta||{},batch:n.batch||{},trustedProxy:n.trustedProxy||{headers:[]},rateLimits:n.rateLimits||{rules:[]}},qr(e.originalFormSettings.rateLimits.rules),e.formSettings=JSON.parse(JSON.stringify(e.originalFormSettings))}function s(){e.formSettings=JSON.parse(e.originalFormSettingsHash)}return t.div({pbEvent:`pageApplicationSettings`,className:`page page-application-settings`},$(),t.div({className:`page-content full-height`},t.header({className:`page-header`},t.nav({className:`breadcrumbs`},t.div({className:`breadcrumb-item`},`Settings`),t.div({className:`breadcrumb-item`},`Application`))),t.div({className:`wrapper m-b-base`},()=>e.isLoading?t.div({className:`block txt-center`},t.span({className:`loader lg`})):t.form({pbEvent:`applicationSettingsForm`,className:`grid ap
Line truncated
|
||||
`),t.p({className:`txt-bold`},`When using such proxy, to avoid spoofing it is recommended to:`),t.ul({className:`txt-bold`},t.li(null,`use headers that are controlled only by the proxy and cannot be manually set by the users`),t.li(null,`make sure that the PocketBase server can be accessed ONLY through the proxy`)),t.p(null,`You can clear the headers field if PocketBase is not deployed behind a proxy.`)),t.div({className:`grid sm`},t.div({className:`col-lg-9`},t.div({className:`fields`},t.div({className:`field`},t.label({htmlFor:`trustedProxy.headers`},`Trusted IP proxy headers`),t.input({type:`text`,id:`trustedProxy.headers`,name:`trustedProxy.headers`,placeholder:`Leave empty to disable`,value:()=>app.utils.joinNonEmpty(e.formSettings.trustedProxy.headers),oninput:n=>{let r=app.utils.splitNonEmpty(n.target.value,`,`),i=app.utils.joinNonEmpty(r);app.utils.joinNonEmpty(e.formSettings.trustedProxy.headers)!=i&&(e.formSettings.trustedProxy.headers=r)}})),t.div({className:`field addon`},t.button({type:`button`,className:()=>`btn sm secondary transparent ${app.utils.isEmpty(e.formSettings.trustedProxy.headers)?`hidden`:``}`,onclick:()=>{e.formSettings.trustedProxy.headers=[]}},t.span({className:`txt`},`Clear`)))),t.div({className:`field-help`},`Comma separated list of headers such as: `,t.div({className:`inline-flex gap-5`},()=>i.suggestedProxyHeaders.map(n=>t.div({role:`button`,className:`label sm link-primary`,onclick:()=>{e.formSettings.trustedProxy.headers=[n]},textContent:n}))))),t.div({className:`col-lg-3`},t.div({className:`field`},t.label({htmlFor:`trustedProxy.useLeftmostIP`},t.span({className:`txt`},`IP priority`),t.i({className:`ri-information-line tooltip-right`,ariaDescription:app.attrs.tooltip(`This is in case the proxy returns more than 1 IP as header value. The rightmost IP is usually considered to be the more trustworthy but this could vary depending on the proxy.`)})),app.components.select({id:`trustedProxy.useLeftmostIP`,name:`trustedProxy.useLeftmostIP`,options:r,required:!0,value:()=>e.formSettings.trustedProxy.useLeftmostIP||!1,onchange:n=>{e.formSettings.trustedProxy.useLeftmostIP=n?.[0]?.value}})))))}function Zr(){app.store.title=`Application settings`;let e=store({isLoading:!1,isSaving:!1,formSettings:null,originalFormSettings:null,get originalFormSettingsHash(){return JSON.stringify(e.originalFormSettings)},get formSettingsHash(){return JSON.stringify(e.formSettings)},get hasChanges(){return e.originalFormSettingsHash!=e.formSettingsHash}});n();async function n(){e.isLoading=!0;try{o(await app.pb.settings.getAll()),e.isLoading=!1}catch(e){e.isAbort||app.checkApiError(e)}}function r(){return JSON.stringify(e.formSettings?.superuserIPs)!=JSON.stringify(e.originalFormSettings?.superuserIPs)}async function i(){let n=app.utils.toArray(e.formSettings?.superuserIPs);return n.length?app.modals.confirm(t.div({className:`txt-center`},t.h6(null,`The ONLY allowed superuser IPs will change to: `,t.br(),t.strong(null,n.join(`, `))),t.p(null,`Please make sure that your IP is in the list or you'll be locked.`),t.p({className:`txt-hint`},`In case of lockout, you can reset the setting with the `,t.a({href:`https://pocketbase.io/docs/going-to-production/#limit-superusers-to-specific-ipssubnets`,target:`_blank`,rel:`noopener noreferrer`,className:`link-primary txt-bold txt-sm`},t.code(null,`superuser ips`,t.i({ariaHidden:!0,className:`ri-arrow-right-up-line txt-sm`}))),` console command.`)),()=>a(),null,{yesButton:`Yes, save changes`}):a()}async function a(){if(!(e.isSaving||!e.hasChanges)){e.isSaving=!0,e.formSettings.rateLimits.rules=qr(e.formSettings.rateLimits.rules);try{let n=app.utils.filterRedactedProps(e.formSettings),i=await app.pb.settings.update(n);if(r())try{await app.pb.collection(`_superusers`).authRefresh()}catch{app.pb.authStore.clear()}o(i),app.toasts.success(`Successfully saved application settings.`)}catch(e){app.checkApiError(e)}e.isSaving=!1}}function o(n={}){if(app.store.settings=JSON.parse(JSON.stringify(n)),!n.meta?.accentColor){let e=window.getComputedStyle(document.documentElement)?.getPropertyValue(`--accentColor`);e?.startsWith(`#`)&&(n.meta=n.meta||{},n.meta.accentColor=e.toLowerCase()||``)}e.originalFormSettings={superuserIPs:n.superuserIPs||[],meta:n.meta||{},batch:n.batch||{},trustedProxy:n.trustedProxy||{headers:[]},rateLimits:n.rateLimits||{excludedIPs:[],rules:[]}},qr(e.originalFormSettings.rateLimits.rules),e.formSettings=JSON.parse(JSON.stringify(e.originalFormSettings))}function s(){e.formSettings=JSON.parse(e.originalFormSettingsHash)}return t.div({pbEvent:`pageApplicationSettings`,className:`page page-application-settings`},$(),t.div({className:`page-content full-height`},t.header({className:`page-header`},t.nav({className:`breadcrumbs`},t.div({className:`breadcrumb-item`},`Settings`),t.div({className:`breadcrumb-item`},`Application`))),t.div({className:`wrapper m-b-base`},()=>e.isLoading?t.div({className:`block txt-center`},t.span({className:`loader lg
Line truncated
|
||||
Records update will also require an extra unlock step before save.`)})))),t.div({className:`col-lg-12`},t.hr()),t.div({className:`col-lg-12`},t.div({className:`flex`},t.div({className:`m-r-auto`}),t.button({type:`button`,className:`btn transparent secondary`,disabled:()=>e.isSaving,hidden:()=>!e.hasChanges,onclick:s},t.span({className:`txt`},`Cancel`)),t.button({className:()=>`btn expanded-lg ${e.isSaving?`loading`:``}`,disabled:()=>!e.hasChanges||e.isSaving},t.span({className:`txt`},`Save changes`)))))),t.footer({className:`page-footer`},app.components.credits())))}function Qr(e){let n=`accent_`+app.utils.randomString(),r=store({isTooLight:!1}),i,a;function o(e){clearTimeout(a),document.documentElement.style.setProperty(`--animationSpeed`,`0`),e?document.documentElement.style.setProperty(`--accentColor`,e.toLowerCase()):document.documentElement.style.removeProperty(`--accentColor`),a=setTimeout(()=>{document.documentElement.style.removeProperty(`--animationSpeed`)},100)}let s=[watch(()=>e.formSettings?.meta?.accentColor,e=>{clearTimeout(i),i=setTimeout(()=>{o(e)},100)})];return t.div({className:`field`,ariaDescription:app.attrs.tooltip(()=>r.isTooLight?`Invalid - color is too light`:``),onunmount:()=>{clearTimeout(i),o(e.formSettings.meta.accentColor),s.forEach(e=>e?.unwatch())}},t.label({htmlFor:n},t.span({className:`txt`},`Accent`),t.i({hidden:()=>!r.isTooLight,className:`txt-warning ri-alert-line`})),app.components.colorPicker({id:n,name:`meta.accentColor`,predefinedColors:()=>app.store.predefinedAccentColors,value:()=>e.formSettings.meta.accentColor,onchange:n=>{if(r.isTooLight=!1,!app.utils.isDarkEnoughForWhiteText(n)){r.isTooLight=!0;return}e.formSettings.meta.accentColor=n}}))}function $r(e={}){let n=store({onsave:null}),r=app.utils.extendStore(n,e),i=[{cron:`0 0 * * *`,label:`Every day at 00:00h`},{cron:`0 0 * * 0`,label:`Every sunday at 00:00h`},{cron:`0 0 * * 1,3`,label:`Every Mon and Wed at 00:00h`},{cron:`0 0 1 * *`,label:`Every first day of the month at 00:00h`}],a=store({showForm:!1,isLoading:!1,isSaving:!1,formSettings:null,initSerialized:`null`,enableAutoBackups:!1,get hasChanges(){return a.initSerialized!=JSON.stringify(a.formSettings)}});async function o(){a.isLoading=!0;try{c(await app.pb.settings.getAll()),a.isLoading=!1}catch(e){e.isAbort||app.checkApiError(e)}}async function s(){if(!(a.isSaving||!a.hasChanges)){a.isSaving=!0;try{let e=app.utils.filterRedactedProps(a.formSettings),r=await app.pb.settings.update(e);n.onsave?.(r),c(r),app.toasts.success(`Successfully saved backups settings.`)}catch(e){app.checkApiError(e)}a.isSaving=!1}}function c(e={}){app.store.settings=JSON.parse(JSON.stringify(e)),a.formSettings={backups:e?.backups||{}},a.enableAutoBackups=!!a.formSettings.backups.cron,a.initSerialized=JSON.stringify(a.formSettings)}function l(){a.formSettings=JSON.parse(a.initSerialized),a.enableAutoBackups=!!a.formSettings.backups.cron}return r.push(watch(()=>{!a.enableAutoBackups&&a.formSettings?.backups?.cron&&(a.formSettings.backups.cron=``)})),t.div({className:`block backups-settings-form-wrapper`,onmount:()=>{o()},onunmount:()=>{r.forEach(e=>e?.unwatch())}},t.button({type:`button`,className:()=>`btn secondary ${a.isLoading?`loading`:``}`,disabled:()=>a.isLoading||a.hasChanges,onclick:()=>a.showForm=!a.showForm},t.span({className:`txt`},`Backup options`),t.i({className:()=>a.showForm?`ri-arrow-up-s-line`:`ri-arrow-down-s-line`,ariaHidden:!0})),app.components.slide(()=>a.showForm,t.form({pbEvent:`backupsSettingsForm`,className:`grid backups-settings-form m-t-base`,inert:()=>a.isSaving,onsubmit:e=>{e.preventDefault(),s()}},()=>a.isLoading?t.div({className:`col-lg-12 txt-center`},t.span({className:`loader lg`})):[t.div({className:`col-lg-12`},t.div({className:`field`},t.input({id:`enableAutoBackupsToggle`,type:`checkbox`,className:`switch`,checked:()=>a.enableAutoBackups,onchange:e=>{a.enableAutoBackups=e.target.checked,a.formSettings.backups.cron||(a.formSettings.backups.cron=i[0].cron)}}),t.label({htmlFor:`enableAutoBackupsToggle`},`Enable auto backups`)),app.components.slide(()=>a.enableAutoBackups,t.div({className:`grid m-t-base m-b-base`},t.div({className:`col-lg-6`},t.div({className:`fields`},t.div({className:`field`},t.label({htmlFor:`backups.cron`},`Cron expression`),t.input({id:`backups.cron`,name:`backups.cron`,className:`txt-code`,type:`text`,placeholder:`e.g. 0 0 * * *`,required:()=>a.enableAutoBackups,value:()=>a.formSettings.backups.cron,oninput:e=>a.formSettings.backups.cron=e.target.value})),t.div({className:`field addon`},t.button({type:`button`,className:`btn outline sm`,"html-popovertarget":`cron-presets-dropdown`},t.span({className:`txt`},`Presets`),t.i({className:`ri-arrow-drop-down-line`,ariaHidden:!0})),t.div({id:`cron-presets-dropdown`,className:`dropdown sm txt-nowrap`,popover:`auto`},()=>i.map(e=>t.button({type:`button`,className:()=>`dropdown-item ${a.formSettings.backups.cron==e.cron?`active`:``}`,textContent:e.label,onclick:n=>{a.formSettings.backups.
Line truncated
|
||||
@annually
|
||||
@monthly
|
||||
@@ -1 +1 @@
|
||||
@font-face{font-display:block;font-family:remixicon;src:url(../fonts/remixicon/remixicon.woff2?t=1734404658139)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:normal;font-weight:400;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-regular.woff2)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:italic;font-weight:400;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-italic.woff2)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:normal;font-weight:600;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-600.woff2)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:italic;font-weight:600;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-600italic.woff2)format("woff2")}@font-face{font-display:swap;font-family:IBM Plex Mono;font-style:normal;font-weight:400;src:url(../fonts/ibm-plex-mono/ibm-plex-mono-v20-latin-regular.woff2)format("woff2")}@font-face{font-display:swap;font-family:IBM Plex Mono;font-style:normal;font-weight:600;src:url(../fonts/ibm-plex-mono/ibm-plex-mono-v20-latin-600.woff2)format("woff2")}.ri-24-hours-fill:before{content:""}.ri-24-hours-line:before{content:""}.ri-4k-fill:before{content:""}.ri-4k-line:before{content:""}.ri-a-b:before{content:""}.ri-account-box-fill:before{content:""}.ri-account-box-line:before{content:""}.ri-account-circle-fill:before{content:""}.ri-account-circle-line:before{content:""}.ri-account-pin-box-fill:before{content:""}.ri-account-pin-box-line:before{content:""}.ri-account-pin-circle-fill:before{content:""}.ri-account-pin-circle-line:before{content:""}.ri-add-box-fill:before{content:""}.ri-add-box-line:before{content:""}.ri-add-circle-fill:before{content:""}.ri-add-circle-line:before{content:""}.ri-add-fill:before{content:""}.ri-add-line:before{content:""}.ri-admin-fill:before{content:""}.ri-admin-line:before{content:""}.ri-advertisement-fill:before{content:""}.ri-advertisement-line:before{content:""}.ri-airplay-fill:before{content:""}.ri-airplay-line:before{content:""}.ri-alarm-fill:before{content:""}.ri-alarm-line:before{content:""}.ri-alarm-warning-fill:before{content:""}.ri-alarm-warning-line:before{content:""}.ri-album-fill:before{content:""}.ri-album-line:before{content:""}.ri-alert-fill:before{content:""}.ri-alert-line:before{content:""}.ri-aliens-fill:before{content:""}.ri-aliens-line:before{content:""}.ri-align-bottom:before{content:""}.ri-align-center:before{content:""}.ri-align-justify:before{content:""}.ri-align-left:before{content:""}.ri-align-right:before{content:""}.ri-align-top:before{content:""}.ri-align-vertically:before{content:""}.ri-alipay-fill:before{content:""}.ri-alipay-line:before{content:""}.ri-amazon-fill:before{content:""}.ri-amazon-line:before{content:""}.ri-anchor-fill:before{content:""}.ri-anchor-line:before{content:""}.ri-ancient-gate-fill:before{content:""}.ri-ancient-gate-line:before{content:""}.ri-ancient-pavilion-fill:before{content:""}.ri-ancient-pavilion-line:before{content:""}.ri-android-fill:before{content:""}.ri-android-line:before{content:""}.ri-angularjs-fill:before{content:""}.ri-angularjs-line:before{content:""}.ri-anticlockwise-2-fill:before{content:""}.ri-anticlockwise-2-line:before{content:""}.ri-anticlockwise-fill:before{content:""}.ri-anticlockwise-line:before{content:""}.ri-app-store-fill:before{content:""}.ri-app-store-line:before{content:""}.ri-apple-fill:before{content:""}.ri-apple-line:before{content:""}.ri-apps-2-fill:before{content:""}.ri-apps-2-line:before{content:""}.ri-apps-fill:before{content:""}.ri-apps-line:before{content:""}.ri-archive-drawer-fill:before{content:""}.ri-archive-drawer-line:before{content:""}.ri-archive-fill:before{content:""}.ri-archive-line:before{content:""}.ri-arrow-down-circle-fill:before{content:""}.ri-arrow-down-circle-line:before{content:""}.ri-arrow-down-fill:before{content:""}.ri-arrow-down-line:before{content:""}.ri-arrow-down-s-fill:before{content:""}.ri-arrow-down-s-line:before{content:""}.ri-arrow-drop-down-fill:before{content:""}.ri-arrow-drop-down-line:before{content:""}.ri-arrow-drop-left-fill:before{content:""}.ri-arrow-drop-left-line:before{content:""}.ri-arrow-drop-right-fill:before{content:""}.ri-arrow-drop-right-line:before{content:""}.ri-arrow-drop-up-fill:before{content:""}.ri-arrow-drop-up-line:before{content:""}.ri-arrow-go-back-fill:before{content:""}.ri-arrow-go-back-line:before{content:""}.ri-arrow-go-forward-fill:before{content:""}.ri-arrow-go-forward-line:before{content:""}.ri-arrow-left-circle-fill:before{content:""}.ri-arrow-left-circle-line:before{content:""}.ri-aLine truncated
|
||||
@font-face{font-display:block;font-family:remixicon;src:url(../fonts/remixicon/remixicon.woff2?t=1734404658139)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:normal;font-weight:400;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-regular.woff2)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:italic;font-weight:400;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-italic.woff2)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:normal;font-weight:600;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-600.woff2)format("woff2")}@font-face{font-display:block;font-family:IBM Plex Sans;font-style:italic;font-weight:600;src:url(../fonts/ibm-plex-sans/ibm-plex-sans-v23-cyrillic_latin_latin-ext-600italic.woff2)format("woff2")}@font-face{font-display:swap;font-family:IBM Plex Mono;font-style:normal;font-weight:400;src:url(../fonts/ibm-plex-mono/ibm-plex-mono-v20-latin-regular.woff2)format("woff2")}@font-face{font-display:swap;font-family:IBM Plex Mono;font-style:normal;font-weight:600;src:url(../fonts/ibm-plex-mono/ibm-plex-mono-v20-latin-600.woff2)format("woff2")}.ri-24-hours-fill:before{content:""}.ri-24-hours-line:before{content:""}.ri-4k-fill:before{content:""}.ri-4k-line:before{content:""}.ri-a-b:before{content:""}.ri-account-box-fill:before{content:""}.ri-account-box-line:before{content:""}.ri-account-circle-fill:before{content:""}.ri-account-circle-line:before{content:""}.ri-account-pin-box-fill:before{content:""}.ri-account-pin-box-line:before{content:""}.ri-account-pin-circle-fill:before{content:""}.ri-account-pin-circle-line:before{content:""}.ri-add-box-fill:before{content:""}.ri-add-box-line:before{content:""}.ri-add-circle-fill:before{content:""}.ri-add-circle-line:before{content:""}.ri-add-fill:before{content:""}.ri-add-line:before{content:""}.ri-admin-fill:before{content:""}.ri-admin-line:before{content:""}.ri-advertisement-fill:before{content:""}.ri-advertisement-line:before{content:""}.ri-airplay-fill:before{content:""}.ri-airplay-line:before{content:""}.ri-alarm-fill:before{content:""}.ri-alarm-line:before{content:""}.ri-alarm-warning-fill:before{content:""}.ri-alarm-warning-line:before{content:""}.ri-album-fill:before{content:""}.ri-album-line:before{content:""}.ri-alert-fill:before{content:""}.ri-alert-line:before{content:""}.ri-aliens-fill:before{content:""}.ri-aliens-line:before{content:""}.ri-align-bottom:before{content:""}.ri-align-center:before{content:""}.ri-align-justify:before{content:""}.ri-align-left:before{content:""}.ri-align-right:before{content:""}.ri-align-top:before{content:""}.ri-align-vertically:before{content:""}.ri-alipay-fill:before{content:""}.ri-alipay-line:before{content:""}.ri-amazon-fill:before{content:""}.ri-amazon-line:before{content:""}.ri-anchor-fill:before{content:""}.ri-anchor-line:before{content:""}.ri-ancient-gate-fill:before{content:""}.ri-ancient-gate-line:before{content:""}.ri-ancient-pavilion-fill:before{content:""}.ri-ancient-pavilion-line:before{content:""}.ri-android-fill:before{content:""}.ri-android-line:before{content:""}.ri-angularjs-fill:before{content:""}.ri-angularjs-line:before{content:""}.ri-anticlockwise-2-fill:before{content:""}.ri-anticlockwise-2-line:before{content:""}.ri-anticlockwise-fill:before{content:""}.ri-anticlockwise-line:before{content:""}.ri-app-store-fill:before{content:""}.ri-app-store-line:before{content:""}.ri-apple-fill:before{content:""}.ri-apple-line:before{content:""}.ri-apps-2-fill:before{content:""}.ri-apps-2-line:before{content:""}.ri-apps-fill:before{content:""}.ri-apps-line:before{content:""}.ri-archive-drawer-fill:before{content:""}.ri-archive-drawer-line:before{content:""}.ri-archive-fill:before{content:""}.ri-archive-line:before{content:""}.ri-arrow-down-circle-fill:before{content:""}.ri-arrow-down-circle-line:before{content:""}.ri-arrow-down-fill:before{content:""}.ri-arrow-down-line:before{content:""}.ri-arrow-down-s-fill:before{content:""}.ri-arrow-down-s-line:before{content:""}.ri-arrow-drop-down-fill:before{content:""}.ri-arrow-drop-down-line:before{content:""}.ri-arrow-drop-left-fill:before{content:""}.ri-arrow-drop-left-line:before{content:""}.ri-arrow-drop-right-fill:before{content:""}.ri-arrow-drop-right-line:before{content:""}.ri-arrow-drop-up-fill:before{content:""}.ri-arrow-drop-up-line:before{content:""}.ri-arrow-go-back-fill:before{content:""}.ri-arrow-go-back-line:before{content:""}.ri-arrow-go-forward-fill:before{content:""}.ri-arrow-go-forward-line:before{content:""}.ri-arrow-left-circle-fill:before{content:""}.ri-arrow-left-circle-line:before{content:""}.ri-aLine truncated
|
||||
Vendored
+2
-2
@@ -13,9 +13,9 @@
|
||||
|
||||
<!-- prism -->
|
||||
<script src="./libs/prism/prism.js" data-manual></script>
|
||||
<script type="module" crossorigin src="./assets/index-C6EtHz6e.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-1sz-Wwkw.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/pocketbase.es-B_4DUNUU.js">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-DMmjfPb3.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-D92J8BMA.css">
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
|
||||
@@ -61,6 +61,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
td.col-new-btn {
|
||||
padding: 3px;
|
||||
&:focus-within {
|
||||
background: var(--surfaceAlt2Color);
|
||||
}
|
||||
}
|
||||
.col-label {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,11 @@ export function pageApplicationSettings() {
|
||||
get originalFormSettingsHash() {
|
||||
return JSON.stringify(data.originalFormSettings);
|
||||
},
|
||||
get formSettingsHash() {
|
||||
return JSON.stringify(data.formSettings);
|
||||
},
|
||||
get hasChanges() {
|
||||
return data.originalFormSettingsHash != JSON.stringify(data.formSettings);
|
||||
return data.originalFormSettingsHash != data.formSettingsHash;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -136,7 +139,7 @@ export function pageApplicationSettings() {
|
||||
meta: settings.meta || {},
|
||||
batch: settings.batch || {},
|
||||
trustedProxy: settings.trustedProxy || { headers: [] },
|
||||
rateLimits: settings.rateLimits || { rules: [] },
|
||||
rateLimits: settings.rateLimits || { excludedIPs: [], rules: [] },
|
||||
};
|
||||
|
||||
sortRules(data.originalFormSettings.rateLimits.rules);
|
||||
|
||||
@@ -80,6 +80,23 @@ export function rateLimitAccordion(pageData) {
|
||||
|
||||
const accordionData = store({
|
||||
predefinedTags: basePredefinedTags,
|
||||
showMoreOptions: false,
|
||||
get originalRateLimitFieldsHash() {
|
||||
return JSON.stringify(pageData.originalFormSettings?.rateLimits.rules)
|
||||
+ JSON.stringify(pageData.originalFormSettings?.rateLimits.excludedIPs);
|
||||
},
|
||||
get hasRateLimitFieldsChanged() {
|
||||
const newHash = JSON.stringify(pageData.formSettings?.rateLimits.rules)
|
||||
+ JSON.stringify(pageData.formSettings?.rateLimits.excludedIPs);
|
||||
return accordionData.originalRateLimitFieldsHash != newHash;
|
||||
},
|
||||
get enableWarn() {
|
||||
return (
|
||||
!pageData.formSettings?.rateLimits?.enabled
|
||||
&& pageData.formSettings?.rateLimits?.rules?.length
|
||||
&& accordionData.hasRateLimitFieldsChanged
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
loadPredefinedTags();
|
||||
@@ -206,6 +223,16 @@ export function rateLimitAccordion(pageData) {
|
||||
delete app.store.errors.rateLimits;
|
||||
},
|
||||
),
|
||||
// ensure that the excluded IP field is visible in case of an error
|
||||
watch(
|
||||
() => app.store.errors?.rateLimits?.excludedIPs,
|
||||
(newErr) => {
|
||||
if (newErr) {
|
||||
accordionData.showMoreOptions = true;
|
||||
return;
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
onunmount: () => {
|
||||
@@ -224,7 +251,7 @@ export function rateLimitAccordion(pageData) {
|
||||
return t.span({ className: "label" }, "Disabled");
|
||||
},
|
||||
() => {
|
||||
if (!app.utils.isEmpty(app.store.errors?.rateLimits)) {
|
||||
if (pageData.formSettingsHash && !app.utils.isEmpty(app.store.errors?.rateLimits)) {
|
||||
return t.i({
|
||||
className: "ri-error-warning-fill txt-danger",
|
||||
ariaDescription: app.attrs.tooltip("Has errors", "left"),
|
||||
@@ -237,18 +264,32 @@ export function rateLimitAccordion(pageData) {
|
||||
t.div(
|
||||
{ className: "col-lg-12" },
|
||||
t.div(
|
||||
{ className: "field" },
|
||||
t.input({
|
||||
id: "rateLimits.enabled",
|
||||
name: "rateLimits.enabled",
|
||||
type: "checkbox",
|
||||
className: "switch",
|
||||
checked: () => pageData.formSettings.rateLimits.enabled || false,
|
||||
onchange: (e) => (pageData.formSettings.rateLimits.enabled = e.target.checked),
|
||||
}),
|
||||
t.label(
|
||||
{ htmlFor: "rateLimits.enabled" },
|
||||
t.span({ className: "txt" }, "Enable"),
|
||||
{ className: "flex" },
|
||||
t.div(
|
||||
{ className: "field" },
|
||||
t.input({
|
||||
id: "rateLimits.enabled",
|
||||
name: "rateLimits.enabled",
|
||||
type: "checkbox",
|
||||
className: "switch",
|
||||
checked: () => pageData.formSettings.rateLimits.enabled || false,
|
||||
onchange: (e) => (pageData.formSettings.rateLimits.enabled = e.target.checked),
|
||||
}),
|
||||
t.label(
|
||||
{ htmlFor: "rateLimits.enabled" },
|
||||
t.span(
|
||||
{ className: () => `txt ${accordionData.enableWarn ? "txt-warning" : ""}` },
|
||||
"Enable",
|
||||
),
|
||||
),
|
||||
),
|
||||
t.button(
|
||||
{
|
||||
type: "button",
|
||||
className: "link-hint txt-sm m-l-auto",
|
||||
onclick: () => openRateLimitInfoModal(),
|
||||
},
|
||||
t.em(null, "Learn more about the rate limit rules"),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -281,131 +322,208 @@ export function rateLimitAccordion(pageData) {
|
||||
t.th({ className: "col-action" }),
|
||||
),
|
||||
),
|
||||
t.tbody(null, () => {
|
||||
const rows = [];
|
||||
const rules = pageData.formSettings.rateLimits.rules || [];
|
||||
t.tbody(
|
||||
null,
|
||||
() => {
|
||||
const rows = [];
|
||||
const rules = pageData.formSettings.rateLimits.rules || [];
|
||||
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
const rule = rules[i];
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
const rule = rules[i];
|
||||
|
||||
rows.push(
|
||||
t.tr(
|
||||
{ className: "rate-limit-row" },
|
||||
t.td(
|
||||
{ className: "col-label" },
|
||||
t.div(
|
||||
{ className: "field" },
|
||||
t.input({
|
||||
type: "text",
|
||||
required: true,
|
||||
className: "inline-error",
|
||||
id: "rateLimits.rules." + i + ".label",
|
||||
name: "rateLimits.rules." + i + ".label",
|
||||
placeholder: "tag (users:create) or path (/api/)",
|
||||
"html-list": "rateLimits.rules." + i + ".label_list",
|
||||
value: () => rule.label,
|
||||
oninput: (e) => (rule.label = e.target.value),
|
||||
}),
|
||||
t.datalist(
|
||||
rows.push(
|
||||
t.tr(
|
||||
{ className: "rate-limit-row" },
|
||||
t.td(
|
||||
{ className: "col-label" },
|
||||
t.div(
|
||||
{ className: "field" },
|
||||
t.input({
|
||||
type: "text",
|
||||
required: true,
|
||||
className: "inline-error",
|
||||
id: "rateLimits.rules." + i + ".label",
|
||||
name: "rateLimits.rules." + i + ".label",
|
||||
placeholder: "tag (users:create) or path (/api/)",
|
||||
"html-list": "rateLimits.rules." + i + ".label_list",
|
||||
value: () => rule.label,
|
||||
oninput: (e) => (rule.label = e.target.value),
|
||||
}),
|
||||
t.datalist(
|
||||
{
|
||||
id: "rateLimits.rules." + i + ".label_list",
|
||||
},
|
||||
() => {
|
||||
return accordionData.predefinedTags.map((tag) => {
|
||||
return t.option({ value: tag.value }, tag.label || "");
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
t.td(
|
||||
{ className: "col-requests" },
|
||||
t.div(
|
||||
{ className: "field" },
|
||||
t.input({
|
||||
type: "number",
|
||||
required: true,
|
||||
placeholder: "Max requests*",
|
||||
className: "inline-error",
|
||||
min: 1,
|
||||
step: 1,
|
||||
name: "rateLimits.rules." + i + ".maxRequests",
|
||||
value: () => rule.maxRequests || 0,
|
||||
oninput: (e) => rule.maxRequests = parseInt(e.target.value, 10),
|
||||
}),
|
||||
),
|
||||
),
|
||||
t.td(
|
||||
{ className: "col-duration" },
|
||||
t.div(
|
||||
{ className: "field" },
|
||||
t.input({
|
||||
type: "number",
|
||||
required: true,
|
||||
placeholder: "Interval*",
|
||||
className: "inline-error",
|
||||
min: 1,
|
||||
step: 1,
|
||||
name: "rateLimits.rules." + i + ".duration",
|
||||
value: () => rule.duration,
|
||||
oninput: (e) => rule.duration = parseInt(e.target.value, 10),
|
||||
}),
|
||||
),
|
||||
),
|
||||
t.td(
|
||||
{ className: "col-audience" },
|
||||
t.div(
|
||||
{ className: "field" },
|
||||
app.components.select({
|
||||
name: "rateLimits.rules." + i + ".audience",
|
||||
className: "inline-error",
|
||||
options: audienceOptions,
|
||||
required: true,
|
||||
value: () => rule.audience || "",
|
||||
onchange: (selected) => {
|
||||
rule.audience = selected?.[0]?.value;
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
t.td(
|
||||
{ className: "col-action" },
|
||||
t.button(
|
||||
{
|
||||
id: "rateLimits.rules." + i + ".label_list",
|
||||
},
|
||||
() => {
|
||||
return accordionData.predefinedTags.map((tag) => {
|
||||
return t.option({ value: tag.value }, tag.label || "");
|
||||
});
|
||||
type: "button",
|
||||
araiaDescription: app.attrs.tooltip("Remove rule"),
|
||||
className: "btn sm secondary transparent circle",
|
||||
onclick: () => removeRule(i),
|
||||
},
|
||||
t.i({ className: "ri-close-line" }),
|
||||
),
|
||||
),
|
||||
),
|
||||
t.td(
|
||||
{ className: "col-requests" },
|
||||
t.div(
|
||||
{ className: "field" },
|
||||
t.input({
|
||||
type: "number",
|
||||
required: true,
|
||||
placeholder: "Max requests*",
|
||||
className: "inline-error",
|
||||
min: 1,
|
||||
step: 1,
|
||||
name: "rateLimits.rules." + i + ".maxRequests",
|
||||
value: () => rule.maxRequests || 0,
|
||||
oninput: (e) => rule.maxRequests = parseInt(e.target.value, 10),
|
||||
}),
|
||||
),
|
||||
),
|
||||
t.td(
|
||||
{ className: "col-duration" },
|
||||
t.div(
|
||||
{ className: "field" },
|
||||
t.input({
|
||||
type: "number",
|
||||
required: true,
|
||||
placeholder: "Interval*",
|
||||
className: "inline-error",
|
||||
min: 1,
|
||||
step: 1,
|
||||
name: "rateLimits.rules." + i + ".duration",
|
||||
value: () => rule.duration,
|
||||
oninput: (e) => rule.duration = parseInt(e.target.value, 10),
|
||||
}),
|
||||
),
|
||||
),
|
||||
t.td(
|
||||
{ className: "col-audience" },
|
||||
t.div(
|
||||
{ className: "field" },
|
||||
app.components.select({
|
||||
name: "rateLimits.rules." + i + ".audience",
|
||||
className: "inline-error",
|
||||
options: audienceOptions,
|
||||
required: true,
|
||||
value: () => rule.audience || "",
|
||||
onchange: (selected) => {
|
||||
rule.audience = selected?.[0]?.value;
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
t.td(
|
||||
{ className: "col-action" },
|
||||
t.button(
|
||||
{
|
||||
type: "button",
|
||||
araiaDescription: app.attrs.tooltip("Remove rule"),
|
||||
className: "btn sm secondary transparent circle",
|
||||
onclick: () => removeRule(i),
|
||||
},
|
||||
t.i({ className: "ri-close-line" }),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}),
|
||||
return rows;
|
||||
},
|
||||
t.tr(
|
||||
{ className: "rate-limit-row" },
|
||||
t.td(
|
||||
{ colSpan: 99, className: "col-new-btn" },
|
||||
t.button(
|
||||
{
|
||||
type: "button",
|
||||
className: "btn secondary sm full-width",
|
||||
onclick: () => newRule(),
|
||||
},
|
||||
t.i({ className: "ri-add-line", ariaHidden: true }),
|
||||
t.span({ className: "txt" }, "Add rate limit rule"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
t.div(
|
||||
{ className: "flex m-t-sm" },
|
||||
t.button(
|
||||
{
|
||||
type: "button",
|
||||
className: "btn secondary sm",
|
||||
onclick: () => newRule(),
|
||||
},
|
||||
t.i({ className: "ri-add-line", ariaHidden: true }),
|
||||
t.span({ className: "txt" }, "Add rate limit rule"),
|
||||
),
|
||||
t.button(
|
||||
{
|
||||
type: "button",
|
||||
className: "link-hint txt-sm m-l-auto",
|
||||
onclick: () => openRateLimitInfoModal(),
|
||||
},
|
||||
t.em(null, "Learn more about the rate limit rules"),
|
||||
),
|
||||
t.div(
|
||||
{ className: "col-lg-12" },
|
||||
t.button(
|
||||
{
|
||||
type: "button",
|
||||
className: () => `btn secondary sm ${!accordionData.showMoreOptions ? "transparent" : ""}`,
|
||||
onclick: () => accordionData.showMoreOptions = !accordionData.showMoreOptions,
|
||||
},
|
||||
t.span({ className: "txt" }, "More options"),
|
||||
t.i({
|
||||
ariaHidden: true,
|
||||
className: () => accordionData.showMoreOptions ? "ri-arrow-up-s-line" : "ri-arrow-down-s-line",
|
||||
}),
|
||||
),
|
||||
app.components.slide(
|
||||
() => accordionData.showMoreOptions,
|
||||
t.div(
|
||||
{ className: "p-t-10" },
|
||||
t.div(
|
||||
{ className: "fields" },
|
||||
t.div(
|
||||
{ className: "field" },
|
||||
t.label(
|
||||
{ htmlFor: "excludedIPs" },
|
||||
t.span({ className: "txt" }, "Excluded IPs and subnets"),
|
||||
),
|
||||
t.input({
|
||||
id: "excludedIPs",
|
||||
name: "rateLimits.excludedIPs",
|
||||
type: "text",
|
||||
value: () => app.utils.joinNonEmpty(pageData.formSettings.rateLimits.excludedIPs),
|
||||
oninput: (e) => {
|
||||
const newValue = app.utils.splitNonEmpty(e.target.value, ",");
|
||||
const newStr = app.utils.joinNonEmpty(newValue);
|
||||
const oldStr = app.utils.joinNonEmpty(
|
||||
pageData.formSettings.rateLimits.excludedIPs,
|
||||
);
|
||||
|
||||
// has an actual change
|
||||
if (oldStr != newStr) {
|
||||
pageData.formSettings.rateLimits.excludedIPs = newValue;
|
||||
}
|
||||
},
|
||||
}),
|
||||
),
|
||||
t.div(
|
||||
{ className: "field addon" },
|
||||
t.button(
|
||||
{
|
||||
type: "button",
|
||||
className: () =>
|
||||
`btn sm secondary transparent ${
|
||||
app.utils.isEmpty(pageData.formSettings.rateLimits.excludedIPs)
|
||||
? "hidden"
|
||||
: ""
|
||||
}`,
|
||||
onclick: () => {
|
||||
pageData.formSettings.rateLimits.excludedIPs = [];
|
||||
|
||||
if (app.store.errors?.rateLimits?.excludedIPs) {
|
||||
delete app.store.errors.rateLimits.excludedIPs;
|
||||
}
|
||||
},
|
||||
},
|
||||
t.span({ className: "txt" }, "Clear"),
|
||||
),
|
||||
),
|
||||
),
|
||||
t.div(
|
||||
{ className: "field-help" },
|
||||
t.p(null, "Comma separated list of IPs and CIDR subnets to exclude from the rate limiter."),
|
||||
t.p(
|
||||
null,
|
||||
"Superusers are always excluded and they can send as many requests as they want.",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -61,7 +61,7 @@ export function trustedProxyAccordion(pageData) {
|
||||
t.summary(
|
||||
null,
|
||||
t.i({ className: "ri-route-line", ariaHidden: true }),
|
||||
t.span({ className: "txt" }, "User IP proxy headers"),
|
||||
t.span({ className: "txt" }, "IP proxy headers"),
|
||||
() => {
|
||||
if (proxyInfo.isLoading) {
|
||||
return t.span({ className: "loader sm" });
|
||||
|
||||
Reference in new issue
Block a user