mirror of
https://github.com/pocketbase/pocketbase.git
synced 2026-10-04 21:41:42 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d438c6a96a | ||
|
|
6828ca1d4e | ||
|
|
88193b9396 | ||
|
|
3cedf032ca | ||
|
|
4396369bb9 | ||
|
|
4a4f8ad9db | ||
|
|
5a144e1342 | ||
|
|
24d72877e9 | ||
|
|
c40f519b7b | ||
|
|
b026c6844b | ||
|
|
21a5524fed | ||
|
|
fe2d90641c | ||
|
|
8091a756e8 | ||
|
|
7542cd14e2 | ||
|
|
7da023f345 | ||
|
|
a104de5b9f | ||
|
|
97c5f3fa02 | ||
|
|
fccfa20653 | ||
|
|
29c6dacbfc | ||
|
|
d33927718b | ||
|
|
d153553d52 | ||
|
|
db88253aac | ||
|
|
0cf34c475b | ||
|
|
547ee715c0 | ||
|
|
4850da6f56 | ||
|
|
53ac0d29da | ||
|
|
d90aaedc00 | ||
|
|
74defc48b9 | ||
|
|
9205b11dc3 | ||
|
|
8d0881db3f | ||
|
|
dbcd95eb62 | ||
|
|
905256b0c8 | ||
|
|
602f3a4442 | ||
|
|
5c9bcfaf8e | ||
|
|
44bf55097a | ||
|
|
338d672bee | ||
|
|
5bd9d87bad | ||
|
|
6ba78d5218 | ||
|
|
260bd59c5b | ||
|
|
006566478a | ||
|
|
419f335f5b | ||
|
|
326f150db2 | ||
|
|
1c86addc4c | ||
|
|
494f47efb8 | ||
|
|
555a4f1a1e | ||
|
|
37b258810a | ||
|
|
ca7cf1162f | ||
|
|
dddb0a029f | ||
|
|
5d55fc18ee | ||
|
|
449e5af590 | ||
|
|
1e460d3f96 | ||
|
|
b5030ddfa1 | ||
|
|
fbeb09c40b | ||
|
|
69cdda4bf3 | ||
|
|
e708f39e1b | ||
|
|
52eccb3aac | ||
|
|
1d593476b0 | ||
|
|
8a04904de1 | ||
|
|
a3ac674f36 | ||
|
|
ae7041a889 | ||
|
|
257f03e1fa | ||
|
|
3566ba3729 | ||
|
|
a6002c4622 | ||
|
|
2ddf161314 | ||
|
|
b15f358fc9 |
No files matched your search
@@ -72,6 +72,18 @@ If someone is able to tamper with the OAuth2 responses then the entire OAuth2 fl
|
||||
~Nonetheless, in future PocketBase releases there will be [extra `localhost` domain like checks](https://github.com/orgs/pocketbase/projects/2/views/1?pane=issue&itemId=159545722) when assigning the OAuth2 avatar URL to a `file` field that will further minimize the risk of internal network probing requests in case of a vulnerable OAuth2 provider.~ _Done._
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Users enumeration</strong></summary>
|
||||
|
||||
This is a common and usually valid report but there is no easy solution without confusing and degrading the users experience.
|
||||
|
||||
Some endpoints, like the user create/register, can be used for usernames or emails enumeration based on various response heuristics - timing, specific error messages, etc.
|
||||
|
||||
In many places where applicable we've tried to minimize the impact by using constant time checks, returning non-descriptive error messages, applying an internal rate limit for some operations, etc. but it is not bulletproof and if somebody wants to find out if a user is registered they will be able to do it one way or another.
|
||||
|
||||
If you think that there is a place where we can improve the handling without hurting too much the user experience, feel free to open a regular public issue and it will be considered.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong><code>disintegration/imaging</code> CVE-2023-36308</strong></summary>
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '>=1.26.2'
|
||||
go-version: '>=1.26.3'
|
||||
|
||||
# This step usually is not needed because the /ui/dist is pregenerated locally
|
||||
# but its here to ensure that each release embeds the latest admin ui artifacts.
|
||||
|
||||
@@ -1,3 +1,68 @@
|
||||
## v0.38.0
|
||||
|
||||
- Fixed UI logs pagination when no custom range is specified.
|
||||
|
||||
- Fixed default CSP not allowing audio/video previews ([#7677](https://github.com/pocketbase/pocketbase/issues/7677)).
|
||||
|
||||
- Serve fixed `Content-Type` for `.xlsx`, `.docx` and `.pptx` files to allow previews on iOS ([#7467](https://github.com/pocketbase/pocketbase/discussions/7467)).
|
||||
|
||||
- Changed settings app URL input to `type="text"` for compatibility with earlier versions ([#7681](https://github.com/pocketbase/pocketbase/issues/7681)).
|
||||
|
||||
- Added an internal watcher to sync various runtime states between multiple PocketBase processes (e.g. memory store) using the same `pb_data`.
|
||||
_This is helpful in case for example a separate PocketBase console command change the collections or application settings while the server is still running._
|
||||
_The watcher is debounced and implemented by watching the special `pb_data/.notify` dir as a workaround to avoid depending on OS and SQLite driver specific APIs._
|
||||
|
||||
- Added new [Superuser IPs/CIDR subnets whitelist setting](https://pocketbase.io/docs/going-to-production/#limit-superusers-to-specific-ipssubnets).
|
||||
The optional setting can be changed from the UI under _Dasboard > Settings > Application > Superuser IPs_.
|
||||
To avoid lockout in case your superuser IP change, the ips whitelist can be updated also via the `superuser ips` console command:
|
||||
```sh
|
||||
# note: --dir is optional and defaults to pb_data next to the executable
|
||||
|
||||
# clear whitelisted IPs
|
||||
./pocketbase superuser ips --dir=/custom/path/to/pb_data
|
||||
|
||||
# OR change the whitelisted IPs to 127.0.0.1 and 10.0.0.0 (replace with your real IP(s))
|
||||
./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)).
|
||||
|
||||
- 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
|
||||
|
||||
- Fixed password fields not being detected as changed ([#7670](https://github.com/pocketbase/pocketbase/issues/7670)).
|
||||
|
||||
- Added the local time zone name next to the `date` field label.
|
||||
|
||||
- Reload trusted proxy info UI after settings save.
|
||||
|
||||
- Other minor improvements (skips the duplicated record ids from the `IN` expand list, reordered confirm-email-change error checks to minimize enumeration attacks, etc.).
|
||||
|
||||
|
||||
## v0.37.4
|
||||
|
||||
- Added backups list scroll container ([#7655](https://github.com/pocketbase/pocketbase/issues/7655)).
|
||||
|
||||
- Optimized record upsert and preview modals data loading to minimize layout jumps.
|
||||
|
||||
- Fixed SMTP IPv6 network address format ([#7659](https://github.com/pocketbase/pocketbase/issues/7659)).
|
||||
|
||||
- Fixed autocomplete selection not properly updating the underlying input value ([#7664](https://github.com/pocketbase/pocketbase/issues/7664)).
|
||||
|
||||
- Added `ghupdate.BaseURL` config option ([#7665](https://github.com/pocketbase/pocketbase/issues/7665)).
|
||||
|
||||
- Added dummy bcrypt password check for the failure auth path to minimize enumeration timing attacks when registrations are disabled.
|
||||
|
||||
- Adjusted Bitbucket, GitHub, GitLab and Gitea/Forgejo OAuth2 providers to better reflect recent API updates and doc references.
|
||||
_In case the userinfo data is not sufficient, some of the providers now send a separate list emails request in order to minimize eventual linking security issues caused by custom onpremise setups (e.g. Gitea/Forgejo allows skipping the email verification if an ENV variable is configured)._
|
||||
|
||||
- ⚠️ Fixed a pre-hijacking OAuth2 linking vulnerability ([#7662](https://github.com/pocketbase/pocketbase/discussions/7662); thanks @Alardiians for reporting it privately).
|
||||
|
||||
- Bumped Go and npm dependencies.
|
||||
|
||||
|
||||
## v0.37.3
|
||||
|
||||
- Fixed total count load on page back/forward navigation.
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
> For the most recent versions, please refer to [CHANGELOG.md](./CHANGELOG.md)
|
||||
---
|
||||
|
||||
## v0.22.43
|
||||
|
||||
- (_Backported from v0.38.0_) 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.22.42
|
||||
|
||||
- (_Backported from v0.37.4_) Adjusted Bitbucket, GitHub, GitLab and Gitea/Forgejo OAuth2 providers to better reflect recent API updates and doc references.
|
||||
_In case the userinfo data is not sufficient, some of the providers now send a separate list emails request in order to minimize eventual linking security issues caused by custom onpremise setups (e.g. Gitea/Forgejo allows skipping the email verification if an ENV variable is configured)._
|
||||
|
||||
- (_Backported from v0.37.4_) ⚠️ Fixed a pre-hijacking OAuth2 linking vulnerability ([#7662](https://github.com/pocketbase/pocketbase/discussions/7662)).
|
||||
|
||||
|
||||
## v0.22.41
|
||||
|
||||
- (_Backported from v0.36.9_) Updated the Discord `AuthUser.Name` field to use `global_name`.
|
||||
|
||||
+7
-2
@@ -70,8 +70,10 @@ func backupDownload(e *core.RequestEvent) error {
|
||||
return e.ForbiddenError("Insufficient permissions to access the resource.", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
defer cancel()
|
||||
allowedIPs := e.App.Settings().SuperuserIPs
|
||||
if len(allowedIPs) > 0 && !isIPInList(allowedIPs, e.RealIP()) {
|
||||
return e.ForbiddenError("Insufficient permissions to access the resource.", nil)
|
||||
}
|
||||
|
||||
fsys, err := e.App.NewBackupsFilesystem()
|
||||
if err != nil {
|
||||
@@ -79,6 +81,9 @@ func backupDownload(e *core.RequestEvent) error {
|
||||
}
|
||||
defer fsys.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
fsys.SetContext(ctx)
|
||||
|
||||
key := e.Request.PathValue("key")
|
||||
|
||||
@@ -528,6 +528,58 @@ func TestBackupsDownload(t *testing.T) {
|
||||
},
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
{
|
||||
Name: "with valid superuser file token AND whitelisted IP",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/backups/test1.zip?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6ImZpbGUiLCJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyJ9.Lupz541xRvrktwkrl55p5pPCF77T69ZRsohsIcb2dxc",
|
||||
Headers: map[string]string{"x-test-ip": "127.0.0.1"},
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
app.Settings().TrustedProxy = core.TrustedProxyConfig{
|
||||
Headers: []string{"x-test-ip"},
|
||||
}
|
||||
|
||||
app.Settings().SuperuserIPs = []string{"127.0.0.1"}
|
||||
|
||||
if err := app.Save(app.Settings()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
"storage/",
|
||||
"data.db",
|
||||
"auxiliary.db",
|
||||
},
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
{
|
||||
Name: "with valid superuser file token BUT non-whitelisted IP",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/backups/test1.zip?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6ImZpbGUiLCJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyJ9.Lupz541xRvrktwkrl55p5pPCF77T69ZRsohsIcb2dxc",
|
||||
Headers: map[string]string{"x-test-ip": "127.0.0.1"},
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
if err := createTestBackups(app); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
app.Settings().TrustedProxy = core.TrustedProxyConfig{
|
||||
Headers: []string{"x-test-ip"},
|
||||
}
|
||||
|
||||
app.Settings().SuperuserIPs = []string{"0.0.0.0"}
|
||||
|
||||
if err := app.Save(app.Settings()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 403,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
|
||||
@@ -31,6 +31,7 @@ func NewRouter(app core.App) (*router.Router[*core.RequestEvent], error) {
|
||||
pbRouter.Bind(panicRecover())
|
||||
pbRouter.Bind(rateLimit())
|
||||
pbRouter.Bind(loadAuthToken())
|
||||
pbRouter.Bind(superuserIPsWhitelist())
|
||||
pbRouter.Bind(securityHeaders())
|
||||
pbRouter.Bind(BodyLimit(DefaultMaxBodySize))
|
||||
|
||||
|
||||
+65
-1
@@ -224,7 +224,7 @@ func TestBatchRequest(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "mixed create/update/delete (rules failure)",
|
||||
Name: "mixed create/update/delete (non-superuser rule failure)",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/batch",
|
||||
Body: strings.NewReader(`{
|
||||
@@ -284,6 +284,70 @@ func TestBatchRequest(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "mixed create/update/delete (superuser rule failure)",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/batch",
|
||||
Headers: map[string]string{
|
||||
// test@example.com, clients
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0",
|
||||
},
|
||||
Body: strings.NewReader(`{
|
||||
"requests": [
|
||||
{"method":"DELETE", "url":"/api/collections/demo2/records/achvryl401bhse3", "headers": {"Authorization": "ignored"}},
|
||||
{"method":"PATCH", "url":"/api/collections/demo3/records/1tmknxy2868d869", "body": {"title": "batch_update"}, "headers": {"Authorization": "ignored"}},
|
||||
{"method":"POST", "url":"/api/collections/_superusers/records", "body": {"email":"test_batch@example.com","password":"1234567890"}}
|
||||
]
|
||||
}`),
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{
|
||||
`"data":{`,
|
||||
`"requests":{`,
|
||||
`"2":{"code":"batch_request_failed"`,
|
||||
`403`,
|
||||
},
|
||||
NotExpectedContent: []string{
|
||||
`"0":`,
|
||||
`"1":`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"*": 0,
|
||||
"OnBatchRequest": 1,
|
||||
"OnModelUpdate": 1,
|
||||
"OnModelUpdateExecute": 1,
|
||||
"OnModelAfterUpdateError": 1,
|
||||
"OnModelDelete": 1,
|
||||
"OnModelDeleteExecute": 1,
|
||||
"OnModelAfterDeleteError": 1,
|
||||
"OnModelValidate": 1,
|
||||
"OnRecordUpdateRequest": 1,
|
||||
"OnRecordUpdate": 1,
|
||||
"OnRecordUpdateExecute": 1,
|
||||
"OnRecordAfterUpdateError": 1,
|
||||
"OnRecordDeleteRequest": 1,
|
||||
"OnRecordDelete": 1,
|
||||
"OnRecordDeleteExecute": 1,
|
||||
"OnRecordAfterDeleteError": 1,
|
||||
"OnRecordEnrich": 1,
|
||||
"OnRecordValidate": 1,
|
||||
},
|
||||
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
|
||||
_, err = app.FindRecordById("demo2", "achvryl401bhse3")
|
||||
if err != nil {
|
||||
t.Fatal("Expected record to not be deleted")
|
||||
}
|
||||
|
||||
_, err = app.FindFirstRecordByFilter("demo3", `title="batch_update"`)
|
||||
if err == nil {
|
||||
t.Fatal("Expected record to not be updated")
|
||||
}
|
||||
|
||||
_, err = app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test_batch@example.com")
|
||||
if err == nil {
|
||||
t.Fatal("Expected superuser to not be created")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "mixed create/update/delete (rules success)",
|
||||
Method: http.MethodPost,
|
||||
|
||||
@@ -60,6 +60,7 @@ type fileApi struct {
|
||||
}
|
||||
|
||||
func (api *fileApi) fileToken(e *core.RequestEvent) error {
|
||||
// extra check for just in case the handler is called in a different context
|
||||
if e.Auth == nil {
|
||||
return e.UnauthorizedError("Missing auth context.", nil)
|
||||
}
|
||||
@@ -114,6 +115,15 @@ func (api *fileApi) download(e *core.RequestEvent) error {
|
||||
token := e.Request.URL.Query().Get("token")
|
||||
authRecord, _ := e.App.FindAuthRecordByToken(token, core.TokenTypeFile)
|
||||
|
||||
// reset the auth state if it is superuser and it is not whitelisted
|
||||
// (not critical because file tokens are short-lived but checked nonetheless as an extra precaution)
|
||||
if authRecord != nil && authRecord.IsSuperuser() {
|
||||
allowedIPs := e.App.Settings().SuperuserIPs
|
||||
if len(allowedIPs) > 0 && !isIPInList(allowedIPs, e.RealIP()) {
|
||||
authRecord = nil
|
||||
}
|
||||
}
|
||||
|
||||
// create a shallow copy of the cached request data and adjust it to the current auth record (if any)
|
||||
requestInfo := *originalRequestInfo
|
||||
requestInfo.Context = core.RequestInfoContextProtectedFile
|
||||
|
||||
@@ -353,6 +353,50 @@ func TestFileDownload(t *testing.T) {
|
||||
"OnFileDownloadRequest": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "protected file - superuser with non-whitelisted IP",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/files/demo1/al1h9ijdeojtsjy/300_Jsjq7RdBgA.png?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6ImZpbGUiLCJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyJ9.Lupz541xRvrktwkrl55p5pPCF77T69ZRsohsIcb2dxc",
|
||||
Headers: map[string]string{"x-test-ip": "127.0.0.1"},
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.Settings().TrustedProxy = core.TrustedProxyConfig{
|
||||
Headers: []string{"x-test-ip"},
|
||||
}
|
||||
|
||||
app.Settings().SuperuserIPs = []string{"0.0.0.0"}
|
||||
|
||||
err := app.Save(app.Settings())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 404,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
{
|
||||
Name: "protected file - superuser with whitelisted IP",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/files/demo1/al1h9ijdeojtsjy/300_Jsjq7RdBgA.png?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsImV4cCI6MjUyNDYwNDQ2MSwidHlwZSI6ImZpbGUiLCJjb2xsZWN0aW9uSWQiOiJwYmNfMzE0MjYzNTgyMyJ9.Lupz541xRvrktwkrl55p5pPCF77T69ZRsohsIcb2dxc",
|
||||
Headers: map[string]string{"x-test-ip": "127.0.0.1"},
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
app.Settings().TrustedProxy = core.TrustedProxyConfig{
|
||||
Headers: []string{"x-test-ip"},
|
||||
}
|
||||
|
||||
app.Settings().SuperuserIPs = []string{"127.0.0.1"}
|
||||
|
||||
if err := app.Save(app.Settings()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"PNG"},
|
||||
ExpectedEvents: map[string]int{
|
||||
"*": 0,
|
||||
"OnFileDownloadRequest": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "protected file - guest without view access",
|
||||
Method: http.MethodGet,
|
||||
|
||||
@@ -42,6 +42,9 @@ const (
|
||||
DefaultLoadAuthTokenMiddlewarePriority = DefaultRateLimitMiddlewarePriority - 20
|
||||
DefaultLoadAuthTokenMiddlewareId = "pbLoadAuthToken"
|
||||
|
||||
DefaultSuperuserIPsWhitelistMiddlewarePriority = DefaultLoadAuthTokenMiddlewarePriority + 5
|
||||
DefaultSuperuserIPsWhitelistMiddlewareId = "pbSuperuserIPsWhitelist"
|
||||
|
||||
DefaultSecurityHeadersMiddlewarePriority = DefaultRateLimitMiddlewarePriority - 10
|
||||
DefaultSecurityHeadersMiddlewareId = "pbSecurityHeaders"
|
||||
|
||||
@@ -299,6 +302,28 @@ func securityHeaders() *hook.Handler[*core.RequestEvent] {
|
||||
}
|
||||
}
|
||||
|
||||
// superuserIPsWhitelist middleware checks the current authenticated superuser IP
|
||||
// against the configured SuperuserIPs whitelist setting.
|
||||
//
|
||||
// This middleware is registered by default for all routes.
|
||||
func superuserIPsWhitelist() *hook.Handler[*core.RequestEvent] {
|
||||
return &hook.Handler[*core.RequestEvent]{
|
||||
Id: DefaultSuperuserIPsWhitelistMiddlewareId,
|
||||
Priority: DefaultSuperuserIPsWhitelistMiddlewarePriority,
|
||||
Func: func(e *core.RequestEvent) error {
|
||||
if e.HasSuperuserAuth() {
|
||||
ips := e.App.Settings().SuperuserIPs
|
||||
|
||||
if len(ips) > 0 && !isIPInList(ips, e.RealIP()) {
|
||||
return e.ForbiddenError("", errors.New("superuser IP is not whitelisted"))
|
||||
}
|
||||
}
|
||||
|
||||
return e.Next()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SkipSuccessActivityLog is a helper middleware that instructs the global
|
||||
// activity logger to log only requests that have failed/returned an error.
|
||||
func SkipSuccessActivityLog() *hook.Handler[*core.RequestEvent] {
|
||||
|
||||
@@ -2,6 +2,7 @@ package apis
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -106,6 +107,41 @@ func checkCollectionRateLimit(e *core.RequestEvent, collection *core.Collection,
|
||||
return nil
|
||||
}
|
||||
|
||||
// isIPInList checks if the specified IP is in a list of other individual IPs or subnets.
|
||||
func isIPInList(ipsOrSubnets []string, ip string) bool {
|
||||
if len(ipsOrSubnets) == 0 || ip == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// normalize
|
||||
searchAddr, err := netip.ParseAddr(ip)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, item := range ipsOrSubnets {
|
||||
// subnet?
|
||||
prefix, err := netip.ParsePrefix(item)
|
||||
if err == nil {
|
||||
if prefix.Contains(searchAddr) {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// individual ip?
|
||||
addr, err := netip.ParseAddr(item)
|
||||
if err == nil {
|
||||
if addr == searchAddr {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// @todo consider exporting as helper?
|
||||
@@ -153,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", 700, false, 200}, // (fixed window check) wait enough to ensure that it can't fit more than 2 requests in 1s
|
||||
{"/rate/a", 800, false, 200},
|
||||
{"/rate/a", 800, 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})
|
||||
})
|
||||
}
|
||||
@@ -553,3 +553,96 @@ func TestRequireSameCollectionContextAuth(t *testing.T) {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuperuserIPsWhitelist(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
setupWhitelist := func(superuserIPs ...string) func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
return func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
// allow loading a mock IP from the test scenario
|
||||
app.Settings().TrustedProxy = core.TrustedProxyConfig{
|
||||
Headers: []string{"x-test-ip"},
|
||||
}
|
||||
|
||||
app.Settings().SuperuserIPs = superuserIPs
|
||||
|
||||
err := app.Save(app.Settings())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
e.Router.GET("/my/test", func(e *core.RequestEvent) error {
|
||||
return e.String(200, "test123")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
scenarios := []tests.ApiScenario{
|
||||
{
|
||||
Name: "guest with non-matching IP",
|
||||
Method: http.MethodGet,
|
||||
URL: "/my/test",
|
||||
Headers: map[string]string{"x-test-ip": "127.0.0.1"},
|
||||
BeforeTestFunc: setupWhitelist("0.0.0.0"),
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"test123"},
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
{
|
||||
Name: "regular user with non-matching IP",
|
||||
Method: http.MethodGet,
|
||||
URL: "/my/test",
|
||||
Headers: map[string]string{
|
||||
"x-test-ip": "127.0.0.1",
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo",
|
||||
},
|
||||
BeforeTestFunc: setupWhitelist("0.0.0.0"),
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"test123"},
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
{
|
||||
Name: "superuser with non-matching IP",
|
||||
Method: http.MethodGet,
|
||||
URL: "/my/test",
|
||||
Headers: map[string]string{
|
||||
"x-test-ip": "127.0.0.1",
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
},
|
||||
BeforeTestFunc: setupWhitelist("0.0.0.0"),
|
||||
ExpectedStatus: 403,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
{
|
||||
Name: "superuser with matching IP",
|
||||
Method: http.MethodGet,
|
||||
URL: "/my/test",
|
||||
Headers: map[string]string{
|
||||
"x-test-ip": "127.0.0.1",
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
},
|
||||
BeforeTestFunc: setupWhitelist("0.0.0.0", "127.0.0.1"),
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"test123"},
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
{
|
||||
Name: "superuser with no whitelisted IPs",
|
||||
Method: http.MethodGet,
|
||||
URL: "/my/test",
|
||||
Headers: map[string]string{
|
||||
"x-test-ip": "127.0.0.1",
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
},
|
||||
BeforeTestFunc: setupWhitelist(),
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"test123"},
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
@@ -102,12 +102,6 @@ func (form *EmailChangeConfirmForm) parseToken() (*core.Record, string, error) {
|
||||
return nil, "", validation.NewError("validation_invalid_token_payload", "Invalid token payload - newEmail must be set.")
|
||||
}
|
||||
|
||||
// ensure that there aren't other users with the new email
|
||||
_, err := form.app.FindAuthRecordByEmail(form.collection, newEmail)
|
||||
if err == nil {
|
||||
return nil, "", validation.NewError("validation_existing_token_email", "The new email address is already registered: "+newEmail)
|
||||
}
|
||||
|
||||
// verify that the token is not expired and its signature is valid
|
||||
authRecord, err := form.app.FindAuthRecordByToken(form.Token, core.TokenTypeEmailChange)
|
||||
if err != nil {
|
||||
@@ -118,5 +112,11 @@ func (form *EmailChangeConfirmForm) parseToken() (*core.Record, string, error) {
|
||||
return nil, "", validation.NewError("validation_token_collection_mismatch", "The provided token is for different auth collection.")
|
||||
}
|
||||
|
||||
// check if there are other users with the new email
|
||||
_, err = form.app.FindAuthRecordByEmail(form.collection, newEmail)
|
||||
if err == nil {
|
||||
return nil, "", validation.NewError("validation_invalid_token_email", "The new email address is invalid.")
|
||||
}
|
||||
|
||||
return authRecord, newEmail, nil
|
||||
}
|
||||
@@ -111,12 +111,51 @@ func TestRecordConfirmEmailChange(t *testing.T) {
|
||||
"OnRecordUpdateExecute": 1,
|
||||
"OnRecordAfterUpdateSuccess": 1,
|
||||
"OnRecordValidate": 1,
|
||||
// unverified->verified external auths removal
|
||||
"OnModelDelete": 2,
|
||||
"OnModelDeleteExecute": 2,
|
||||
"OnModelAfterDeleteSuccess": 2,
|
||||
"OnRecordDelete": 2,
|
||||
"OnRecordDeleteExecute": 2,
|
||||
"OnRecordAfterDeleteSuccess": 2,
|
||||
},
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
user, err := app.FindAuthRecordByEmail("users", "test@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if user.Verified() {
|
||||
t.Fatalf("Expected the user to be unverified before the confirmation")
|
||||
}
|
||||
|
||||
// ensure that there is at least one pre-existing OAuth2 link
|
||||
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(externalAuths) == 0 {
|
||||
t.Fatal("Expected at least one external auths")
|
||||
}
|
||||
},
|
||||
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
|
||||
_, err := app.FindAuthRecordByEmail("users", "change@example.com")
|
||||
user, err := app.FindAuthRecordByEmail("users", "change@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("Expected to find user with email %q, got error: %v", "change@example.com", err)
|
||||
}
|
||||
|
||||
if !user.Verified() {
|
||||
t.Fatalf("Expected the user to be verified after the confirmation")
|
||||
}
|
||||
|
||||
// ensure that all pre-existing OAuth2 links are cleared
|
||||
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(externalAuths) > 0 {
|
||||
t.Fatalf("Expected all external auths to be cleared, found %d", len(externalAuths))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -114,11 +114,18 @@ func TestRecordConfirmPasswordReset(t *testing.T) {
|
||||
"OnModelUpdate": 1,
|
||||
"OnModelUpdateExecute": 1,
|
||||
"OnModelAfterUpdateSuccess": 1,
|
||||
"OnModelValidate": 1,
|
||||
"OnRecordUpdate": 1,
|
||||
"OnRecordUpdateExecute": 1,
|
||||
"OnRecordAfterUpdateSuccess": 1,
|
||||
"OnModelValidate": 1,
|
||||
"OnRecordValidate": 1,
|
||||
// ---
|
||||
"OnModelDelete": 2, // pre-existing OAuth2 links
|
||||
"OnModelDeleteExecute": 2,
|
||||
"OnModelAfterDeleteSuccess": 2,
|
||||
"OnRecordDelete": 2,
|
||||
"OnRecordDeleteExecute": 2,
|
||||
"OnRecordAfterDeleteSuccess": 2,
|
||||
},
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
user, err := app.FindAuthRecordByEmail("users", "test@example.com")
|
||||
@@ -151,6 +158,15 @@ func TestRecordConfirmPasswordReset(t *testing.T) {
|
||||
if !user.ValidatePassword("1234567!") {
|
||||
t.Fatal("Password wasn't changed")
|
||||
}
|
||||
|
||||
// ensure that all pre-existing OAuth2 links are cleared
|
||||
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(externalAuths) > 0 {
|
||||
t.Fatalf("Expected all external auths to be cleared, found %d", len(externalAuths))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -221,6 +237,15 @@ func TestRecordConfirmPasswordReset(t *testing.T) {
|
||||
if !user.ValidatePassword("1234567!") {
|
||||
t.Fatal("Password wasn't changed")
|
||||
}
|
||||
|
||||
// ensure that all pre-existing OAuth2 were NOT deleted
|
||||
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(externalAuths) != 2 {
|
||||
t.Fatalf("Expected 2 external auths, found %d", len(externalAuths))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -251,11 +276,20 @@ func TestRecordConfirmPasswordReset(t *testing.T) {
|
||||
t.Fatalf("Failed to fetch confirm password user: %v", err)
|
||||
}
|
||||
|
||||
oldTokenKey := user.TokenKey()
|
||||
|
||||
// ensure that the user is already verified
|
||||
user.SetVerified(true)
|
||||
if err := app.Save(user); err != nil {
|
||||
t.Fatalf("Failed to update user verified state")
|
||||
}
|
||||
|
||||
// resave with the old token key since the verified change above
|
||||
// would refresh it and will make the password token invalid
|
||||
user.SetTokenKey(oldTokenKey)
|
||||
if err = app.Save(user); err != nil {
|
||||
t.Fatalf("Failed to restore original user tokenKey: %v", err)
|
||||
}
|
||||
},
|
||||
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
|
||||
_, err := app.FindAuthRecordByToken(
|
||||
|
||||
@@ -105,6 +105,51 @@ func TestRecordConfirmVerification(t *testing.T) {
|
||||
"OnRecordValidate": 1,
|
||||
"OnRecordUpdateExecute": 1,
|
||||
"OnRecordAfterUpdateSuccess": 1,
|
||||
// unverified->verified external auths removal
|
||||
"OnModelDelete": 2,
|
||||
"OnModelDeleteExecute": 2,
|
||||
"OnModelAfterDeleteSuccess": 2,
|
||||
"OnRecordDelete": 2,
|
||||
"OnRecordDeleteExecute": 2,
|
||||
"OnRecordAfterDeleteSuccess": 2,
|
||||
},
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
user, err := app.FindAuthRecordByEmail("users", "test@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if user.Verified() {
|
||||
t.Fatalf("Expected the user to be unverified before the confirmation")
|
||||
}
|
||||
|
||||
// ensure that there is at least one pre-existing OAuth2 link
|
||||
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(externalAuths) == 0 {
|
||||
t.Fatal("Expected at least one external auths")
|
||||
}
|
||||
},
|
||||
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
|
||||
user, err := app.FindAuthRecordByEmail("users", "test@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !user.Verified() {
|
||||
t.Fatalf("Expected the user to be verified after the confirmation")
|
||||
}
|
||||
|
||||
// ensure that all pre-existing OAuth2 links are cleared
|
||||
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(externalAuths) > 0 {
|
||||
t.Fatalf("Expected all external auths to be cleared, found %d", len(externalAuths))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -338,26 +338,43 @@ func oauth2Submit(e *core.RecordAuthWithOAuth2RequestEvent, optExternalAuth *cor
|
||||
e.Auth.Id == e.Record.Id &&
|
||||
e.Auth.Collection().Id == e.Record.Collection().Id
|
||||
|
||||
// set random password for users with unverified email
|
||||
// (this is in case a malicious actor has registered previously with the user email)
|
||||
if !isLoggedAuthRecord && e.Record.Email() != "" && !e.Record.Verified() {
|
||||
e.Record.SetRandomPassword()
|
||||
// prevent pre-hijacking with password auth
|
||||
//
|
||||
// reset the unverified user password in case the record was precreated by a malicious actor
|
||||
if !isLoggedAuthRecord && !e.Record.Verified() {
|
||||
needUpdate = true
|
||||
e.Record.SetRandomPassword()
|
||||
}
|
||||
|
||||
// prevent pre-hijacking with different OAuth2 provider
|
||||
//
|
||||
// delete all other previous OAuth2 record links for the cases
|
||||
// when the user was precreated by malicious OAuth2 auth with custom payload data
|
||||
//
|
||||
// while this would be also done automatically on unverified -> verified upgrade,
|
||||
// doing it manually here ensures that a single unverified record could have
|
||||
// max 1 OAuth2 link to prevent further abuse when mixed with other auth flows
|
||||
if !e.Record.Verified() {
|
||||
err := txApp.DeleteAllExternalAuthsByRecord(e.Record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
optExternalAuth = nil // clear to allow recreate below
|
||||
}
|
||||
|
||||
// update the existing auth record empty email if the data.OAuth2User has one
|
||||
// (this is in case previously the auth record was created
|
||||
// with an OAuth2 provider that didn't return an email address)
|
||||
if e.Record.Email() == "" && e.OAuth2User.Email != "" {
|
||||
e.Record.SetEmail(e.OAuth2User.Email)
|
||||
needUpdate = true
|
||||
e.Record.SetEmail(e.OAuth2User.Email)
|
||||
}
|
||||
|
||||
// update the existing auth record verified state
|
||||
// (only if the auth record doesn't have an email or the auth record email match with the one in data.OAuth2User)
|
||||
if !e.Record.Verified() && (e.Record.Email() == "" || e.Record.Email() == e.OAuth2User.Email) {
|
||||
e.Record.SetVerified(true)
|
||||
needUpdate = true
|
||||
e.Record.SetVerified(true)
|
||||
}
|
||||
|
||||
if needUpdate {
|
||||
|
||||
@@ -178,6 +178,20 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// ensure that there is at least one other external auth different than test
|
||||
// so that later we can verify that it was deleted
|
||||
var hasAtLeastOneOtherEA = false
|
||||
externalAuths, _ := app.FindAllExternalAuthsByRecord(user)
|
||||
for _, rel := range externalAuths {
|
||||
if rel.Id != ea.Id {
|
||||
hasAtLeastOneOtherEA = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasAtLeastOneOtherEA {
|
||||
t.Fatal("Expected at least one non-test external auth linked")
|
||||
}
|
||||
|
||||
// test at least once that the correct request info context is properly loaded
|
||||
app.OnRecordAuthRequest().BindFunc(func(e *core.RecordAuthRequestEvent) error {
|
||||
info, err := e.RequestInfo()
|
||||
@@ -213,12 +227,12 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
|
||||
"OnRecordAuthRequest": 1,
|
||||
"OnRecordEnrich": 1,
|
||||
// ---
|
||||
"OnModelCreate": 1,
|
||||
"OnModelCreateExecute": 1,
|
||||
"OnModelAfterCreateSuccess": 1,
|
||||
"OnRecordCreate": 1,
|
||||
"OnRecordCreateExecute": 1,
|
||||
"OnRecordAfterCreateSuccess": 1,
|
||||
"OnModelCreate": 2, // user + recreated external auth
|
||||
"OnModelCreateExecute": 2,
|
||||
"OnModelAfterCreateSuccess": 2,
|
||||
"OnRecordCreate": 2,
|
||||
"OnRecordCreateExecute": 2,
|
||||
"OnRecordAfterCreateSuccess": 2,
|
||||
// ---
|
||||
"OnModelUpdate": 1,
|
||||
"OnModelUpdateExecute": 1,
|
||||
@@ -227,8 +241,15 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
|
||||
"OnRecordUpdateExecute": 1,
|
||||
"OnRecordAfterUpdateSuccess": 1,
|
||||
// ---
|
||||
"OnModelValidate": 2, // create + update
|
||||
"OnRecordValidate": 2,
|
||||
"OnModelDelete": 3, // pre-existing external auths
|
||||
"OnModelDeleteExecute": 3,
|
||||
"OnModelAfterDeleteSuccess": 3,
|
||||
"OnRecordDelete": 3,
|
||||
"OnRecordDeleteExecute": 3,
|
||||
"OnRecordAfterDeleteSuccess": 3,
|
||||
// ---
|
||||
"OnModelValidate": 3, // user create/update + recreated external auth
|
||||
"OnRecordValidate": 3,
|
||||
},
|
||||
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
|
||||
user, err := app.FindAuthRecordByEmail("users", "test@example.com")
|
||||
@@ -248,6 +269,24 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
|
||||
if len(devices) != 1 {
|
||||
t.Fatalf("Expected only 1 auth origin to be created, got %d (%v)", len(devices), err)
|
||||
}
|
||||
|
||||
// ensure that other linked external auths have been deleted
|
||||
externalAuths, _ := app.FindAllExternalAuthsByRecord(user)
|
||||
if len(externalAuths) != 1 {
|
||||
t.Fatalf("Expected only 1 external auth to remain, got %d", len(externalAuths))
|
||||
}
|
||||
if provider := externalAuths[0].Provider(); provider != "test" {
|
||||
t.Fatalf("Expected %q external auth, got %q", "test", provider)
|
||||
}
|
||||
if providerId := externalAuths[0].ProviderId(); providerId != "test_id" {
|
||||
t.Fatalf("Expected %q providerId, got %q", "test_id", providerId)
|
||||
}
|
||||
if recordRef := externalAuths[0].RecordRef(); recordRef != user.Id {
|
||||
t.Fatalf("Expected %q recordRef, got %q", user.Id, recordRef)
|
||||
}
|
||||
if collectionRef := externalAuths[0].CollectionRef(); collectionRef != user.Collection().Id {
|
||||
t.Fatalf("Expected %q collectionRef, got %q", user.Collection().Id, collectionRef)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -343,7 +382,7 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
|
||||
}
|
||||
|
||||
if !user.ValidatePassword("1234567890") {
|
||||
t.Fatalf("Expected old password %q to be valid", "1234567890")
|
||||
t.Fatalf("Expected old password %q to remain valid", "1234567890")
|
||||
}
|
||||
|
||||
devices, err := app.FindAllAuthOriginsByRecord(user)
|
||||
@@ -353,7 +392,7 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "link by email",
|
||||
Name: "link by email (unverified user)",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/users/auth-with-oauth2",
|
||||
Body: strings.NewReader(`{
|
||||
@@ -376,6 +415,20 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
|
||||
t.Fatalf("Expected password %q to be valid", "1234567890")
|
||||
}
|
||||
|
||||
// ensure that there is at least one other external auth different than test
|
||||
// so that later we can verify that it was deleted
|
||||
var hasAtLeastOneOtherEA = false
|
||||
externalAuths, _ := app.FindAllExternalAuthsByRecord(user)
|
||||
for _, rel := range externalAuths {
|
||||
if rel.Provider() != "test" {
|
||||
hasAtLeastOneOtherEA = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasAtLeastOneOtherEA {
|
||||
t.Fatal("Expected at least one non-test external auth linked")
|
||||
}
|
||||
|
||||
// register the test provider
|
||||
auth.Providers["test"] = func() auth.Provider {
|
||||
return &oauth2MockProvider{
|
||||
@@ -432,6 +485,13 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
|
||||
"OnRecordUpdateExecute": 1,
|
||||
"OnRecordAfterUpdateSuccess": 1,
|
||||
// ---
|
||||
"OnModelDelete": 2, // pre-existing external auths
|
||||
"OnModelDeleteExecute": 2,
|
||||
"OnModelAfterDeleteSuccess": 2,
|
||||
"OnRecordDelete": 2,
|
||||
"OnRecordDeleteExecute": 2,
|
||||
"OnRecordAfterDeleteSuccess": 2,
|
||||
// ---
|
||||
"OnModelValidate": 3, // record + authOrigins + externalAuths
|
||||
"OnRecordValidate": 3,
|
||||
},
|
||||
@@ -449,6 +509,145 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
|
||||
if len(devices) != 1 {
|
||||
t.Fatalf("Expected only 1 auth origin to be created, got %d (%v)", len(devices), err)
|
||||
}
|
||||
|
||||
// ensure that other linked external auths have been deleted
|
||||
externalAuths, _ := app.FindAllExternalAuthsByRecord(user)
|
||||
if len(externalAuths) != 1 {
|
||||
t.Fatalf("Expected only 1 external auth to remain, got %d", len(externalAuths))
|
||||
}
|
||||
if provider := externalAuths[0].Provider(); provider != "test" {
|
||||
t.Fatalf("Expected %q external auth, got %q", "test", provider)
|
||||
}
|
||||
if providerId := externalAuths[0].ProviderId(); providerId != "test_id" {
|
||||
t.Fatalf("Expected %q providerId, got %q", "test_id", providerId)
|
||||
}
|
||||
if recordRef := externalAuths[0].RecordRef(); recordRef != user.Id {
|
||||
t.Fatalf("Expected %q recordRef, got %q", user.Id, recordRef)
|
||||
}
|
||||
if collectionRef := externalAuths[0].CollectionRef(); collectionRef != user.Collection().Id {
|
||||
t.Fatalf("Expected %q collectionRef, got %q", user.Collection().Id, collectionRef)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "link by email (verified user)",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/users/auth-with-oauth2",
|
||||
Body: strings.NewReader(`{
|
||||
"provider": "test",
|
||||
"code":"123",
|
||||
"redirectURL": "https://example.com"
|
||||
}`),
|
||||
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
|
||||
user, err := app.FindAuthRecordByEmail("users", "test3@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !user.Verified() {
|
||||
t.Fatalf("Expected user %q to be verified", user.Email())
|
||||
}
|
||||
|
||||
// ensure that the old password works
|
||||
if !user.ValidatePassword("1234567890") {
|
||||
t.Fatalf("Expected password %q to be valid", "1234567890")
|
||||
}
|
||||
|
||||
// register the test provider
|
||||
auth.Providers["test"] = func() auth.Provider {
|
||||
return &oauth2MockProvider{
|
||||
AuthUser: &auth.AuthUser{Id: "test_id", Email: "test3@example.com"},
|
||||
Token: &oauth2.Token{AccessToken: "abc"},
|
||||
}
|
||||
}
|
||||
|
||||
// ensure that there is at least one other external auth different than test
|
||||
// so that later we can verify that they are not deleted
|
||||
var hasAtLeastOneOtherEA = false
|
||||
externalAuths, _ := app.FindAllExternalAuthsByRecord(user)
|
||||
for _, rel := range externalAuths {
|
||||
if rel.Provider() != "test" {
|
||||
hasAtLeastOneOtherEA = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasAtLeastOneOtherEA {
|
||||
t.Fatal("Expected at least one non-test external auth linked")
|
||||
}
|
||||
|
||||
// add the test provider in the collection
|
||||
user.Collection().MFA.Enabled = false
|
||||
user.Collection().OAuth2.Enabled = true
|
||||
user.Collection().OAuth2.Providers = []core.OAuth2ProviderConfig{{
|
||||
Name: "test",
|
||||
ClientId: "123",
|
||||
ClientSecret: "456",
|
||||
}}
|
||||
if err := app.Save(user.Collection()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"record":{`,
|
||||
`"token":"`,
|
||||
`"meta":{`,
|
||||
`"isNew":false`,
|
||||
`"email":"test3@example.com"`,
|
||||
`"id":"bgs820n361vj1qd"`,
|
||||
`"id":"test_id"`,
|
||||
`"verified":true`,
|
||||
},
|
||||
NotExpectedContent: []string{
|
||||
// hidden fields
|
||||
`"tokenKey"`,
|
||||
`"password"`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"*": 0,
|
||||
"OnRecordAuthWithOAuth2Request": 1,
|
||||
"OnRecordAuthRequest": 1,
|
||||
"OnRecordEnrich": 1,
|
||||
// ---
|
||||
"OnModelCreate": 2, // authOrigins + externalAuths
|
||||
"OnModelCreateExecute": 2,
|
||||
"OnModelAfterCreateSuccess": 2,
|
||||
"OnRecordCreate": 2,
|
||||
"OnRecordCreateExecute": 2,
|
||||
"OnRecordAfterCreateSuccess": 2,
|
||||
// ---
|
||||
"OnModelValidate": 2, // authOrigins + externalAuths
|
||||
"OnRecordValidate": 2,
|
||||
},
|
||||
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
|
||||
user, err := app.FindAuthRecordByEmail("users", "test3@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !user.ValidatePassword("1234567890") {
|
||||
t.Fatalf("Expected old password %q to remain valid", "1234567890")
|
||||
}
|
||||
|
||||
devices, err := app.FindAllAuthOriginsByRecord(user)
|
||||
if len(devices) != 1 {
|
||||
t.Fatalf("Expected only 1 auth origin to be created, got %d (%v)", len(devices), err)
|
||||
}
|
||||
|
||||
var hasTestEA = false
|
||||
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
|
||||
if len(externalAuths) <= 1 {
|
||||
t.Fatalf("Expected to have 2+ ExternalAuth records, got %d (%v)", len(externalAuths), err)
|
||||
}
|
||||
for _, rel := range externalAuths {
|
||||
if rel.Provider() == "test" {
|
||||
hasTestEA = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasTestEA {
|
||||
t.Fatal("Expected test external auth to be linked")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -531,6 +730,13 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
|
||||
"OnRecordCreateExecute": 2,
|
||||
"OnRecordAfterCreateSuccess": 2,
|
||||
// ---
|
||||
"OnModelDelete": 2, // pre-existing external auths
|
||||
"OnModelDeleteExecute": 2,
|
||||
"OnModelAfterDeleteSuccess": 2,
|
||||
"OnRecordDelete": 2,
|
||||
"OnRecordDeleteExecute": 2,
|
||||
"OnRecordAfterDeleteSuccess": 2,
|
||||
// ---
|
||||
"OnModelValidate": 2,
|
||||
"OnRecordValidate": 2,
|
||||
},
|
||||
@@ -541,7 +747,7 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
|
||||
}
|
||||
|
||||
if !user.ValidatePassword("1234567890") {
|
||||
t.Fatalf("Expected password %q not to be changed", "1234567890")
|
||||
t.Fatalf("Expected old password %q to remain valid", "1234567890")
|
||||
}
|
||||
|
||||
devices, err := app.FindAllAuthOriginsByRecord(user)
|
||||
@@ -652,6 +858,13 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
|
||||
"OnRecordUpdateExecute": 1,
|
||||
"OnRecordAfterUpdateSuccess": 1,
|
||||
// ---
|
||||
"OnModelDelete": 2, // pre-existing external auths
|
||||
"OnModelDeleteExecute": 2,
|
||||
"OnModelAfterDeleteSuccess": 2,
|
||||
"OnRecordDelete": 2,
|
||||
"OnRecordDeleteExecute": 2,
|
||||
"OnRecordAfterDeleteSuccess": 2,
|
||||
// ---
|
||||
"OnModelValidate": 3, // record + authOrigins + externalAuths
|
||||
"OnRecordValidate": 3,
|
||||
},
|
||||
@@ -662,7 +875,7 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
|
||||
}
|
||||
|
||||
if !user.ValidatePassword("1234567890") {
|
||||
t.Fatalf("Expected password %q not to be changed", "1234567890")
|
||||
t.Fatalf("Expected old password %q to remain valid", "1234567890")
|
||||
}
|
||||
|
||||
devices, err := app.FindAllAuthOriginsByRecord(user)
|
||||
@@ -758,6 +971,13 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
|
||||
"OnRecordUpdateExecute": 1,
|
||||
"OnRecordAfterUpdateSuccess": 1,
|
||||
// ---
|
||||
"OnModelDelete": 2, // pre-existing external auths
|
||||
"OnModelDeleteExecute": 2,
|
||||
"OnModelAfterDeleteSuccess": 2,
|
||||
"OnRecordDelete": 2,
|
||||
"OnRecordDeleteExecute": 2,
|
||||
"OnRecordAfterDeleteSuccess": 2,
|
||||
// ---
|
||||
"OnModelValidate": 3, // record + authOrigins + externalAuths
|
||||
"OnRecordValidate": 3,
|
||||
},
|
||||
@@ -768,7 +988,7 @@ func TestRecordAuthWithOAuth2(t *testing.T) {
|
||||
}
|
||||
|
||||
if !user.ValidatePassword("1234567890") {
|
||||
t.Fatalf("Expected password %q not to be changed", "1234567890")
|
||||
t.Fatalf("Expected old password %q to remain valid", "1234567890")
|
||||
}
|
||||
|
||||
devices, err := app.FindAllAuthOriginsByRecord(user)
|
||||
|
||||
@@ -65,28 +65,38 @@ func recordAuthWithOTP(e *core.RequestEvent) error {
|
||||
// ---
|
||||
|
||||
return e.App.OnRecordAuthWithOTPRequest().Trigger(event, func(e *core.RecordAuthWithOTPRequestEvent) error {
|
||||
otpId := e.OTP.Id
|
||||
otpSentTo := e.OTP.SentTo()
|
||||
|
||||
// eagerly delete the OTP to avoid unnecessery double delete model hook calls
|
||||
// triggered by the password change below
|
||||
err := e.App.Delete(e.OTP)
|
||||
if err != nil {
|
||||
e.App.Logger().Error("Failed to delete used OTP", "error", err, "otpId", e.OTP.Id)
|
||||
}
|
||||
|
||||
// update the user email verified state in case the OTP originate from an email address matching the current record one
|
||||
//
|
||||
// note: don't wait for success auth response (it could fail because of MFA) and because we already validated the OTP above
|
||||
otpSentTo := e.OTP.SentTo()
|
||||
if !e.Record.Verified() && otpSentTo != "" && e.Record.Email() == otpSentTo {
|
||||
e.Record.SetVerified(true)
|
||||
err = e.App.Save(e.Record)
|
||||
if err != nil {
|
||||
|
||||
// this is technically not required but we enforce password
|
||||
// reset on verified upgrades in case the OTP is used on its own
|
||||
// since this makes it less error prone to pre-hijacking attacks
|
||||
if !e.Record.Collection().MFA.Enabled {
|
||||
e.Record.SetRandomPassword()
|
||||
}
|
||||
|
||||
if err := e.App.Save(e.Record); err != nil {
|
||||
e.App.Logger().Error("Failed to update record verified state after successful OTP validation",
|
||||
"error", err,
|
||||
"otpId", e.OTP.Id,
|
||||
"otpId", otpId,
|
||||
"recordId", e.Record.Id,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// try to delete the used otp
|
||||
err = e.App.Delete(e.OTP)
|
||||
if err != nil {
|
||||
e.App.Logger().Error("Failed to delete used OTP", "error", err, "otpId", e.OTP.Id)
|
||||
}
|
||||
|
||||
return RecordAuthResponse(e.RequestEvent, e.Record, core.MFAMethodOTP, nil)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -327,6 +327,15 @@ func TestRecordAuthWithOTP(t *testing.T) {
|
||||
if user.Verified() {
|
||||
t.Fatal("Expected the user to remain unverified because sentTo != email")
|
||||
}
|
||||
|
||||
// ensure that all pre-existing OAuth2 were NOT deleted
|
||||
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(externalAuths) != 2 {
|
||||
t.Fatalf("Expected 2 external auths, found %d", len(externalAuths))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -364,6 +373,15 @@ func TestRecordAuthWithOTP(t *testing.T) {
|
||||
if err := app.Save(otp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// verify that there are at least one pre-existing OAuth2 link
|
||||
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(externalAuths) == 0 {
|
||||
t.Fatal("Expected at least one external auth")
|
||||
}
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
@@ -388,10 +406,10 @@ func TestRecordAuthWithOTP(t *testing.T) {
|
||||
"OnModelCreate": 1,
|
||||
"OnModelCreateExecute": 1,
|
||||
"OnModelAfterCreateSuccess": 1,
|
||||
// OTP delete
|
||||
"OnModelDelete": 1,
|
||||
"OnModelDeleteExecute": 1,
|
||||
"OnModelAfterDeleteSuccess": 1,
|
||||
// record OTP + 2 ExternalAuths delete
|
||||
"OnModelDelete": 3,
|
||||
"OnModelDeleteExecute": 3,
|
||||
"OnModelAfterDeleteSuccess": 3,
|
||||
// user verified update
|
||||
"OnModelUpdate": 1,
|
||||
"OnModelUpdateExecute": 1,
|
||||
@@ -401,9 +419,9 @@ func TestRecordAuthWithOTP(t *testing.T) {
|
||||
"OnRecordCreate": 1,
|
||||
"OnRecordCreateExecute": 1,
|
||||
"OnRecordAfterCreateSuccess": 1,
|
||||
"OnRecordDelete": 1,
|
||||
"OnRecordDeleteExecute": 1,
|
||||
"OnRecordAfterDeleteSuccess": 1,
|
||||
"OnRecordDelete": 3,
|
||||
"OnRecordDeleteExecute": 3,
|
||||
"OnRecordAfterDeleteSuccess": 3,
|
||||
"OnRecordUpdate": 1,
|
||||
"OnRecordUpdateExecute": 1,
|
||||
"OnRecordAfterUpdateSuccess": 1,
|
||||
@@ -417,6 +435,24 @@ func TestRecordAuthWithOTP(t *testing.T) {
|
||||
if !user.Verified() {
|
||||
t.Fatal("Expected the user to be marked as verified")
|
||||
}
|
||||
|
||||
// ensure that all pre-existing OTPs are cleared
|
||||
otps, err := app.FindAllOTPsByRecord(user)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(otps) > 0 {
|
||||
t.Fatalf("Expected all OTPs to be cleared, found %d", len(otps))
|
||||
}
|
||||
|
||||
// ensure that all pre-existing OAuth2 links are cleared
|
||||
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(externalAuths) > 0 {
|
||||
t.Fatalf("Expected all external auths to be cleared, found %d", len(externalAuths))
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -85,6 +85,11 @@ func recordAuthWithPassword(e *core.RequestEvent) error {
|
||||
|
||||
return e.App.OnRecordAuthWithPasswordRequest().Trigger(event, func(e *core.RecordAuthWithPasswordRequestEvent) error {
|
||||
if e.Record == nil || !e.Record.ValidatePassword(e.Password) {
|
||||
// dummy password check to minimize enumeration side-channel attacks
|
||||
if e.Record == nil {
|
||||
dummyPasswordCheck(e.App, e.Collection)
|
||||
}
|
||||
|
||||
return e.BadRequestError("Failed to authenticate.", errors.New("invalid login credentials"))
|
||||
}
|
||||
|
||||
@@ -115,6 +120,21 @@ func (form *authWithPasswordForm) validate(collection *core.Collection) error {
|
||||
)
|
||||
}
|
||||
|
||||
// dummy password check to minimize side-channel attacks
|
||||
// (performed with the collection configured field cost)
|
||||
func dummyPasswordCheck(app core.App, collection *core.Collection) {
|
||||
record := &core.Record{}
|
||||
|
||||
// find any random existing record
|
||||
err := app.RecordQuery(collection).Limit(1).One(record)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// the value and result doesn't matter, we just need a constant-time check
|
||||
_ = record.ValidatePassword("")
|
||||
}
|
||||
|
||||
func findRecordByIdentityField(app core.App, collection *core.Collection, field string, value any) (*core.Record, error) {
|
||||
if !slices.Contains(collection.PasswordAuth.IdentityFields, field) {
|
||||
return nil, errors.New("invalid identity field " + field)
|
||||
|
||||
@@ -43,6 +43,13 @@ func RecordAuthResponse(e *core.RequestEvent, authRecord *core.Record, authMetho
|
||||
}
|
||||
|
||||
func recordAuthResponse(e *core.RequestEvent, authRecord *core.Record, token string, authMethod string, meta any) error {
|
||||
if authRecord.IsSuperuser() {
|
||||
allowedIPs := e.App.Settings().SuperuserIPs
|
||||
if len(allowedIPs) > 0 && !isIPInList(allowedIPs, e.RealIP()) {
|
||||
return e.ForbiddenError("", errors.New("superuser IP is not whitelisted"))
|
||||
}
|
||||
}
|
||||
|
||||
originalRequestInfo, err := e.RequestInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -759,3 +759,39 @@ func TestRecordAuthResponseMFACheck(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRecordAuthResponseSuperuserIPsWhitelistCheck(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
superuser, err := app.FindAuthRecordByEmail(core.CollectionNameSuperusers, "test@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
app.Settings().TrustedProxy.Headers = []string{"x-test-ip"}
|
||||
|
||||
event := new(core.RequestEvent)
|
||||
event.App = app
|
||||
event.Request = httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
event.Request.Header.Set("x-test-ip", "127.0.0.1")
|
||||
event.Response = httptest.NewRecorder()
|
||||
|
||||
t.Run("non-whitelisted", func(t *testing.T) {
|
||||
app.Settings().SuperuserIPs = []string{"0.0.0.0"}
|
||||
|
||||
err = apis.RecordAuthResponse(event, superuser, "example", nil)
|
||||
if err == nil {
|
||||
t.Fatal("Expected response error, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("whitelisted", func(t *testing.T) {
|
||||
app.Settings().SuperuserIPs = []string{"0.0.0.0", "127.0.0.1"}
|
||||
|
||||
err = apis.RecordAuthResponse(event, superuser, "example", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
+1
-1
@@ -22,7 +22,7 @@ import (
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
const defaultCSP = "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' http://127.0.0.1:* https://tile.openstreetmap.org data: blob:; connect-src 'self' http://127.0.0.1:* https://nominatim.openstreetmap.org; script-src 'self' http://127.0.0.1:*; frame-src 'none'"
|
||||
const defaultCSP = "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' http://127.0.0.1:* https://tile.openstreetmap.org data: blob:; connect-src 'self' http://127.0.0.1:* https://nominatim.openstreetmap.org; script-src 'self' http://127.0.0.1:*; frame-ancestors 'none'"
|
||||
|
||||
// ServeConfig defines a configuration struct for apis.Serve().
|
||||
type ServeConfig struct {
|
||||
|
||||
@@ -24,6 +24,7 @@ func NewSuperuserCommand(app core.App) *cobra.Command {
|
||||
command.AddCommand(superuserUpdateCommand(app))
|
||||
command.AddCommand(superuserDeleteCommand(app))
|
||||
command.AddCommand(superuserOTPCommand(app))
|
||||
command.AddCommand(superuserIPsCommand(app))
|
||||
|
||||
return command
|
||||
}
|
||||
@@ -209,3 +210,38 @@ func superuserOTPCommand(app core.App) *cobra.Command {
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func superuserIPsCommand(app core.App) *cobra.Command {
|
||||
command := &cobra.Command{
|
||||
Use: "ips",
|
||||
Example: "superuser ips 127.0.0.1 10.0.0.0/24",
|
||||
Short: "Updates the superuser IPs whitelist setting (the IPs/subnets arguments must be space separated; leave empty to clear the whitelist restriction)",
|
||||
SilenceUsage: true,
|
||||
RunE: func(command *cobra.Command, args []string) error {
|
||||
settings := app.Settings()
|
||||
settings.SuperuserIPs = args
|
||||
|
||||
if err := app.Save(settings); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
color.Green("Successfully cleared SuperuserIPs setting!")
|
||||
} else {
|
||||
color.New(color.BgGreen, color.FgBlack).Println("Successfully updated SuperuserIPs setting:")
|
||||
superuserIPs := app.Settings().SuperuserIPs
|
||||
for i, ip := range superuserIPs {
|
||||
if i == len(superuserIPs)-1 {
|
||||
color.Green("└─ %s", ip)
|
||||
} else {
|
||||
color.Green("├─ %s", ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package cmd_test
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/pocketbase/cmd"
|
||||
@@ -401,3 +402,63 @@ func TestSuperuserOTPCommand(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSuperuserIPsCommand(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
ips []string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
"no ips",
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid ips",
|
||||
[]string{"127.0.0.1", "invalid"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"valid ips",
|
||||
[]string{"127.0.0.1", "::1", "127.0.0.1/24"},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
args := []string{"ips"}
|
||||
args = append(args, s.ips...)
|
||||
|
||||
command := cmd.NewSuperuserCommand(app)
|
||||
command.SetArgs(args)
|
||||
|
||||
err := command.Execute()
|
||||
|
||||
hasErr := err != nil
|
||||
if s.expectError != hasErr {
|
||||
t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err)
|
||||
}
|
||||
|
||||
if hasErr {
|
||||
return
|
||||
}
|
||||
|
||||
settingIPs := app.Settings().SuperuserIPs
|
||||
|
||||
if len(settingIPs) != len(s.ips) {
|
||||
t.Fatalf("Expected %d ips, got %d (%v)", len(s.ips), len(settingIPs), settingIPs)
|
||||
}
|
||||
|
||||
for _, ip := range settingIPs {
|
||||
if !slices.Contains(s.ips, ip) {
|
||||
t.Fatalf("Missing expected ip %q (%v)", ip, settingIPs)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -502,6 +502,11 @@ type App interface {
|
||||
// ExternalAuth model that satisfies the non-nil expression.
|
||||
FindFirstExternalAuthByExpr(expr dbx.Expression) (*ExternalAuth, error)
|
||||
|
||||
// DeleteAllExternalAuthsByRecord deletes all ExternalAuth models associated with the provided record.
|
||||
//
|
||||
// Returns a combined error with the failed deletes.
|
||||
DeleteAllExternalAuthsByRecord(authRecord *Record) error
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
// FindAllMFAsByRecord returns all MFA models linked to the provided auth record.
|
||||
|
||||
+3
-1
@@ -38,8 +38,9 @@ const (
|
||||
|
||||
LocalStorageDirName string = "storage"
|
||||
LocalBackupsDirName string = "backups"
|
||||
LocalTempDirName string = ".pb_temp_to_delete" // temp pb_data sub directory that will be deleted on each app.Bootstrap()
|
||||
LocalAutocertCacheDirName string = ".autocert_cache"
|
||||
LocalNotifyDirName string = ".notify" // optional watched directory that is used as a cross-platform workaround for synchronizing various runtime states between multiple PocketBase instances pointing to the same pb_data
|
||||
LocalTempDirName string = ".pb_temp_to_delete" // temp pb_data sub directory that will be deleted on each app.Bootstrap()
|
||||
|
||||
// @todo consider removing after backups refactoring
|
||||
lostFoundDirName string = "lost+found"
|
||||
@@ -1382,6 +1383,7 @@ func (app *BaseApp) registerBaseHooks() {
|
||||
app.registerMFAHooks()
|
||||
app.registerOTPHooks()
|
||||
app.registerAuthOriginHooks()
|
||||
app.registerNotifyWatcherHooks()
|
||||
}
|
||||
|
||||
// getLoggerMinLevel returns the logger min level based on the
|
||||
|
||||
+7
-1
@@ -54,7 +54,13 @@ func (app *BaseApp) CreateBackup(ctx context.Context, name string) error {
|
||||
event.Context = ctx
|
||||
event.Name = name
|
||||
// default root dir entries to exclude from the backup generation
|
||||
event.Exclude = []string{LocalBackupsDirName, LocalTempDirName, LocalAutocertCacheDirName, lostFoundDirName}
|
||||
event.Exclude = []string{
|
||||
LocalBackupsDirName,
|
||||
LocalTempDirName,
|
||||
LocalNotifyDirName,
|
||||
LocalAutocertCacheDirName,
|
||||
lostFoundDirName,
|
||||
}
|
||||
|
||||
return app.OnBackupCreate().Trigger(event, func(e *BackupEvent) error {
|
||||
// generate a default name if missing
|
||||
|
||||
@@ -60,7 +60,7 @@ func (m *Collection) setDefaultAuthOptions() {
|
||||
},
|
||||
MFA: MFAConfig{
|
||||
Enabled: false,
|
||||
Duration: 1800, // 30min
|
||||
Duration: 600, // 10min
|
||||
},
|
||||
OTP: OTPConfig{
|
||||
Enabled: false,
|
||||
|
||||
@@ -137,4 +137,42 @@ func (app *BaseApp) registerExternalAuthHooks() {
|
||||
},
|
||||
Priority: 99,
|
||||
})
|
||||
|
||||
// delete all pre-existing external auths on verified upgrade
|
||||
app.OnRecordUpdateExecute().Bind(&hook.Handler[*RecordEvent]{
|
||||
Func: func(e *RecordEvent) error {
|
||||
if !e.Record.Collection().IsAuth() {
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
hasUpgradedVerified := !e.Record.Original().IsNew() && !e.Record.Original().Verified() && e.Record.Verified()
|
||||
|
||||
if !hasUpgradedVerified {
|
||||
return e.Next()
|
||||
}
|
||||
|
||||
originalApp := e.App
|
||||
return e.App.RunInTransaction(func(txApp App) error {
|
||||
e.App = txApp
|
||||
defer func() { e.App = originalApp }()
|
||||
|
||||
externalAuths, err := txApp.FindAllExternalAuthsByRecord(e.Record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(externalAuths) > 0 {
|
||||
// delete all pre-existing external auths
|
||||
if err := txApp.DeleteAllExternalAuthsByRecord(e.Record); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// force refresh tokens reset (if not already)
|
||||
e.Record.RefreshTokenKey()
|
||||
}
|
||||
|
||||
return e.Next()
|
||||
})
|
||||
},
|
||||
Priority: 99,
|
||||
})
|
||||
}
|
||||
@@ -308,3 +308,104 @@ func TestExternalAuthValidateHook(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExternalAuthClearOnVerfiedUpgrade(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
t.Run("unverified->no changes", func(t *testing.T) {
|
||||
user, err := app.FindAuthRecordByEmail("users", "test@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if user.Verified() {
|
||||
t.Fatal("Expected user to be unverified")
|
||||
}
|
||||
|
||||
beforeAuths, err := app.FindAllExternalAuthsByRecord(user)
|
||||
if err != nil || len(beforeAuths) == 0 {
|
||||
t.Fatalf("Expected at least one external auth (%v)", err)
|
||||
}
|
||||
|
||||
oldTokenKey := user.TokenKey()
|
||||
|
||||
if err = app.Save(user); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if oldTokenKey != user.TokenKey() {
|
||||
t.Fatal("Expected tokenKey to remain unchanged")
|
||||
}
|
||||
|
||||
afterAuths, err := app.FindAllExternalAuthsByRecord(user)
|
||||
if err != nil || len(afterAuths) != len(beforeAuths) {
|
||||
t.Fatalf("Expected %d external auths, found %d (%v)", len(afterAuths), len(beforeAuths), err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unverified->verified", func(t *testing.T) {
|
||||
user, err := app.FindAuthRecordByEmail("users", "test@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if user.Verified() {
|
||||
t.Fatal("Expected user to be unverified")
|
||||
}
|
||||
|
||||
externalAuths, err := app.FindAllExternalAuthsByRecord(user)
|
||||
if err != nil || len(externalAuths) == 0 {
|
||||
t.Fatalf("Expected at least one external auth (%v)", err)
|
||||
}
|
||||
|
||||
oldTokenKey := user.TokenKey()
|
||||
|
||||
user.SetVerified(true)
|
||||
if err = app.Save(user); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if oldTokenKey == user.TokenKey() {
|
||||
t.Fatal("Expected tokenKey to be renewed")
|
||||
}
|
||||
|
||||
externalAuths, err = app.FindAllExternalAuthsByRecord(user)
|
||||
if err != nil || len(externalAuths) != 0 {
|
||||
t.Fatalf("Expected all user external auths to be deleted, found %d (%v)", len(externalAuths), err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verified->no changes", func(t *testing.T) {
|
||||
user, err := app.FindAuthRecordByEmail("users", "test3@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !user.Verified() {
|
||||
t.Fatal("Expected user to be verified")
|
||||
}
|
||||
|
||||
beforeAuths, err := app.FindAllExternalAuthsByRecord(user)
|
||||
if err != nil || len(beforeAuths) == 0 {
|
||||
t.Fatalf("Expected at least one external auth (%v)", err)
|
||||
}
|
||||
|
||||
oldTokenKey := user.TokenKey()
|
||||
|
||||
if err = app.Save(user); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if oldTokenKey != user.TokenKey() {
|
||||
t.Fatal("Expected tokenKey to remain unchanged")
|
||||
}
|
||||
|
||||
afterAuths, err := app.FindAllExternalAuthsByRecord(user)
|
||||
if err != nil || len(afterAuths) != len(beforeAuths) {
|
||||
t.Fatalf("Expected %d external auths, found %d (%v)", len(afterAuths), len(beforeAuths), err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
)
|
||||
|
||||
@@ -59,3 +61,25 @@ func (app *BaseApp) FindFirstExternalAuthByExpr(expr dbx.Expression) (*ExternalA
|
||||
|
||||
return model, nil
|
||||
}
|
||||
|
||||
// DeleteAllExternalAuthsByRecord deletes all ExternalAuth models associated with the provided record.
|
||||
//
|
||||
// Returns a combined error with the failed deletes.
|
||||
func (app *BaseApp) DeleteAllExternalAuthsByRecord(authRecord *Record) error {
|
||||
models, err := app.FindAllExternalAuthsByRecord(authRecord)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var errs []error
|
||||
for _, m := range models {
|
||||
if err := app.Delete(m); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package core_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
@@ -174,3 +175,68 @@ func TestFindFirstExternalAuthByExpr(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteAllExternalAuthsByRecord(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testApp, _ := tests.NewTestApp()
|
||||
defer testApp.Cleanup()
|
||||
|
||||
demo1, err := testApp.FindRecordById("demo1", "84nmscqy84lsi1t")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
user1, err := testApp.FindAuthRecordByEmail("users", "test@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
client1, err := testApp.FindAuthRecordByEmail("clients", "test@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
client2, err := testApp.FindAuthRecordByEmail("clients", "test2@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
scenarios := []struct {
|
||||
record *core.Record
|
||||
deletedIds []string
|
||||
}{
|
||||
{demo1, nil}, // non-auth record
|
||||
{user1, []string{"dlmflokuq1xl342", "clmflokuq1xl341"}},
|
||||
{client1, []string{"f1z5b3843pzc964"}},
|
||||
{client2, nil},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
t.Run(fmt.Sprintf("%d_%s_%s", i, s.record.Collection().Name, s.record.Id), func(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
deletedIds := []string{}
|
||||
app.OnRecordDelete().BindFunc(func(e *core.RecordEvent) error {
|
||||
deletedIds = append(deletedIds, e.Record.Id)
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
err := app.DeleteAllExternalAuthsByRecord(s.record)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(deletedIds) != len(s.deletedIds) {
|
||||
t.Fatalf("Expected deleted ids\n%v\ngot\n%v", s.deletedIds, deletedIds)
|
||||
}
|
||||
|
||||
for _, id := range s.deletedIds {
|
||||
if !slices.Contains(deletedIds, id) {
|
||||
t.Errorf("Expected to find deleted id %q in %v", id, deletedIds)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -3,6 +3,7 @@ package core
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/hook"
|
||||
@@ -141,11 +142,11 @@ func (app *BaseApp) registerMFAHooks() {
|
||||
if old != new {
|
||||
err = e.App.DeleteAllMFAsByRecord(e.Record)
|
||||
if err != nil {
|
||||
e.App.Logger().Warn(
|
||||
"Failed to delete all previous mfas",
|
||||
"error", err,
|
||||
"recordId", e.Record.Id,
|
||||
"collectionId", e.Record.Collection().Id,
|
||||
return fmt.Errorf(
|
||||
"[%s] failed to delete all previos MFAs for record %q: %w",
|
||||
e.Record.Collection().Name,
|
||||
e.Record.Id,
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,3 +300,64 @@ func TestMFAValidateHook(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMFAClearOnPasswordChange(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
user1, err := app.FindAuthRecordByEmail("users", "test@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
user2, err := app.FindAuthRecordByEmail("users", "test2@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mfasToCreate := map[*core.Record]int{
|
||||
user1: 3,
|
||||
user2: 2,
|
||||
}
|
||||
for user, total := range mfasToCreate {
|
||||
for range total {
|
||||
mfa := core.NewMFA(app)
|
||||
mfa.SetCollectionRef(user.Collection().Id)
|
||||
mfa.SetRecordRef(user.Id)
|
||||
mfa.SetMethod(core.MFAMethodPassword)
|
||||
if err := app.Save(mfa); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update both users
|
||||
err = app.Save(user1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
user2.SetRandomPassword()
|
||||
err = app.Save(user2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedMFAs := map[*core.Record]int{
|
||||
user1: 3,
|
||||
user2: 0,
|
||||
}
|
||||
|
||||
for user, expected := range expectedMFAs {
|
||||
mfas, err := app.FindAllMFAsByRecord(user)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(mfas) != expected {
|
||||
t.Fatalf("Expected %d MFAs, got %d", expected, len(mfas))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/pocketbase/pocketbase/tools/hook"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
)
|
||||
|
||||
const systemHookIdNotifyWatcher = "__pbNotifyWatcherSystemHook__"
|
||||
|
||||
func (app *BaseApp) registerNotifyWatcherHooks() {
|
||||
var notifyWatcher *fsnotify.Watcher
|
||||
|
||||
instanceId := "@" + security.PseudorandomString(10)
|
||||
|
||||
localNotifyDirPath := filepath.Join(app.DataDir(), LocalNotifyDirName)
|
||||
settingsFile := filepath.Join(localNotifyDirPath, "settings"+instanceId)
|
||||
collectionsFile := filepath.Join(localNotifyDirPath, "collections"+instanceId)
|
||||
|
||||
// init
|
||||
app.OnBootstrap().Bind(&hook.Handler[*BootstrapEvent]{
|
||||
Id: systemHookIdNotifyWatcher,
|
||||
Func: func(e *BootstrapEvent) error {
|
||||
err := e.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if notifyWatcher != nil {
|
||||
_ = notifyWatcher.Close()
|
||||
}
|
||||
|
||||
notifyWatcher, err = createNotifyDirWatcher(e.App, instanceId, localNotifyDirPath)
|
||||
if err != nil {
|
||||
e.App.Logger().Warn("Notify dir watcher failure.", "error", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Priority: -998,
|
||||
})
|
||||
|
||||
// cleanup
|
||||
app.OnTerminate().Bind(&hook.Handler[*TerminateEvent]{
|
||||
Id: systemHookIdNotifyWatcher,
|
||||
Func: func(e *TerminateEvent) error {
|
||||
if notifyWatcher != nil {
|
||||
_ = notifyWatcher.Close()
|
||||
}
|
||||
|
||||
_ = os.Remove(settingsFile)
|
||||
_ = os.Remove(collectionsFile)
|
||||
|
||||
return e.Next()
|
||||
},
|
||||
Priority: -998,
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
settingsNotify := func(e *ModelEvent) error {
|
||||
err := e.Next()
|
||||
if err != nil || e.Model.PK() != paramsKeySettings {
|
||||
return err
|
||||
}
|
||||
|
||||
if notifyWatcher != nil {
|
||||
if err := os.WriteFile(settingsFile, nil, 0644); err != nil {
|
||||
e.App.Logger().Warn("Failed to write watcher file", "error", err, "file", settingsFile)
|
||||
}
|
||||
_ = os.Remove(settingsFile)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
app.OnModelAfterCreateSuccess(paramsTable).Bind(&hook.Handler[*ModelEvent]{
|
||||
Id: systemHookIdNotifyWatcher,
|
||||
Func: settingsNotify,
|
||||
Priority: 999,
|
||||
})
|
||||
app.OnModelAfterUpdateSuccess(paramsTable).Bind(&hook.Handler[*ModelEvent]{
|
||||
Id: systemHookIdNotifyWatcher,
|
||||
Func: settingsNotify,
|
||||
Priority: 999,
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
collectionsNotify := func(e *CollectionEvent) error {
|
||||
if err := e.Next(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if notifyWatcher != nil {
|
||||
if err := os.WriteFile(collectionsFile, nil, 0644); err != nil {
|
||||
e.App.Logger().Warn("Failed to write watcher file", "error", err, "file", collectionsFile)
|
||||
}
|
||||
_ = os.Remove(collectionsFile)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
app.OnCollectionAfterCreateSuccess().Bind(&hook.Handler[*CollectionEvent]{
|
||||
Id: systemHookIdNotifyWatcher,
|
||||
Func: collectionsNotify,
|
||||
Priority: 999,
|
||||
})
|
||||
app.OnCollectionAfterUpdateSuccess().Bind(&hook.Handler[*CollectionEvent]{
|
||||
Id: systemHookIdNotifyWatcher,
|
||||
Func: collectionsNotify,
|
||||
Priority: 999,
|
||||
})
|
||||
app.OnCollectionAfterDeleteSuccess().Bind(&hook.Handler[*CollectionEvent]{
|
||||
Id: systemHookIdNotifyWatcher,
|
||||
Func: collectionsNotify,
|
||||
Priority: 999,
|
||||
})
|
||||
}
|
||||
|
||||
func createNotifyDirWatcher(app App, instanceId string, localNotifyDirPath string) (*fsnotify.Watcher, error) {
|
||||
// create the notify dir (if not already)
|
||||
err := os.MkdirAll(localNotifyDirPath, os.ModePerm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create a notify dir: %w", err)
|
||||
}
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to init notify dir watcher: %w", err)
|
||||
}
|
||||
|
||||
err = watcher.Add(localNotifyDirPath)
|
||||
if err != nil {
|
||||
_ = watcher.Close()
|
||||
return nil, fmt.Errorf("unable to watch notify dir: %w", err)
|
||||
}
|
||||
|
||||
var debounceTimer *time.Timer
|
||||
|
||||
stopDebounceTimer := func() {
|
||||
if debounceTimer != nil {
|
||||
debounceTimer.Stop()
|
||||
debounceTimer = nil
|
||||
}
|
||||
}
|
||||
|
||||
// watch
|
||||
go func() {
|
||||
defer stopDebounceTimer()
|
||||
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// modified from within the current app instance or cleanup event
|
||||
if strings.HasSuffix(event.Name, instanceId) || event.Has(fsnotify.Remove) || !app.IsBootstrapped() {
|
||||
continue
|
||||
}
|
||||
|
||||
stopDebounceTimer()
|
||||
|
||||
debounceTimer = time.AfterFunc(50*time.Millisecond, func() {
|
||||
filename := filepath.Base(event.Name)
|
||||
|
||||
// settings changed
|
||||
if strings.HasPrefix(filename, "settings@") {
|
||||
app.Logger().Debug("Reloading settings after notify event")
|
||||
|
||||
err := app.ReloadSettings()
|
||||
if err != nil {
|
||||
app.Logger().Warn("Failed to reload app settings after notify", "error", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// collections changed
|
||||
if strings.HasPrefix(filename, "collections@") {
|
||||
app.Logger().Debug("Reloading cached collections after notify event")
|
||||
|
||||
err := app.ReloadCachedCollections()
|
||||
if err != nil {
|
||||
app.Logger().Warn("Failed to reload cached collections after notify", "error", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
})
|
||||
case err, ok := <-watcher.Errors:
|
||||
if app.IsDev() && err != nil {
|
||||
color.Red("Notify dir watch error:", err)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return watcher, err
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package core_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/store"
|
||||
"golang.org/x/sync/semaphore"
|
||||
)
|
||||
|
||||
func TestNotifyWatcher_SettingsUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testEvents := store.New[core.App, int](nil)
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "pb_notify_test*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
app1 := core.NewBaseApp(core.BaseAppConfig{
|
||||
DataDir: tmpDir,
|
||||
})
|
||||
if err := app1.Bootstrap(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
app2 := core.NewBaseApp(core.BaseAppConfig{
|
||||
DataDir: tmpDir,
|
||||
})
|
||||
if err := app2.Bootstrap(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancelCtx := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancelCtx()
|
||||
|
||||
sem := semaphore.NewWeighted(1)
|
||||
sem.Acquire(ctx, 1)
|
||||
|
||||
app1.OnSettingsReload().BindFunc(func(e *core.SettingsReloadEvent) error {
|
||||
testEvents.SetFunc(app1, func(old int) int {
|
||||
return old + 1
|
||||
})
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
app2.OnSettingsReload().BindFunc(func(e *core.SettingsReloadEvent) error {
|
||||
testEvents.SetFunc(app2, func(old int) int {
|
||||
sem.Release(1)
|
||||
|
||||
return old + 1
|
||||
})
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
// updating app1 settings should trigger a reload in app2
|
||||
app1.Settings().SuperuserIPs = []string{"127.0.0.1"}
|
||||
if err := app1.Save(app1.Settings()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// block until released or timeouted
|
||||
sem.Acquire(ctx, 1)
|
||||
|
||||
if app1Total := testEvents.Get(app1); app1Total != 1 {
|
||||
t.Fatalf("Expected 1 app1 event, got %d", app1Total)
|
||||
}
|
||||
|
||||
if app2Total := testEvents.Get(app2); app2Total != 1 {
|
||||
t.Fatalf("Expected 1 app2 event, got %d", app2Total)
|
||||
}
|
||||
|
||||
app2SuperuserIPs := app2.Settings().SuperuserIPs
|
||||
if len(app2SuperuserIPs) != 1 || app2SuperuserIPs[0] != "127.0.0.1" {
|
||||
t.Fatalf("Expected exactly 127.0.0.1 superuser IP in app2 settings event, got %v", app2SuperuserIPs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotifyWatcher_CollectionsUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "pb_notify_test*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
app1 := core.NewBaseApp(core.BaseAppConfig{
|
||||
DataDir: tmpDir,
|
||||
})
|
||||
if err := app1.Bootstrap(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
app2 := core.NewBaseApp(core.BaseAppConfig{
|
||||
DataDir: tmpDir,
|
||||
})
|
||||
if err := app2.Bootstrap(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testQueries := store.New[string, []string](nil)
|
||||
app2.ConcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
||||
testQueries.SetFunc("concurrent", func(old []string) []string {
|
||||
return append(old, sql)
|
||||
})
|
||||
}
|
||||
app2.ConcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
|
||||
testQueries.SetFunc("concurrent", func(old []string) []string {
|
||||
return append(old, sql)
|
||||
})
|
||||
}
|
||||
app2.NonconcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
||||
testQueries.SetFunc("nonconcurrent", func(old []string) []string {
|
||||
return append(old, sql)
|
||||
})
|
||||
}
|
||||
app2.NonconcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
|
||||
testQueries.SetFunc("nonconcurrent", func(old []string) []string {
|
||||
return append(old, sql)
|
||||
})
|
||||
}
|
||||
|
||||
ctx, cancelCtx := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancelCtx()
|
||||
|
||||
sem := semaphore.NewWeighted(1)
|
||||
sem.Acquire(ctx, 1)
|
||||
|
||||
// currently there is no hook for the collections cache reload so we pool instead
|
||||
done := make(chan bool, 1)
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if len(testQueries.Get("concurrent")) == 1 {
|
||||
sem.Release(1)
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// create/update/delete app1 collections should trigger a reload in app2
|
||||
dummyCollection := core.NewBaseCollection("test")
|
||||
if err := app1.Save(dummyCollection); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dummyCollection.Fields.Add(&core.TextField{Name: "test"})
|
||||
if err := app1.Save(dummyCollection); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := app1.Delete(dummyCollection); err != nil {
|
||||
}
|
||||
|
||||
// block until released or timeouted
|
||||
sem.Acquire(ctx, 1)
|
||||
ticker.Stop()
|
||||
done <- true
|
||||
|
||||
nonconcurrentQueries := testQueries.Get("nonconcurrent")
|
||||
concurrentQueries := testQueries.Get("concurrent")
|
||||
|
||||
if len(nonconcurrentQueries) != 0 {
|
||||
t.Fatalf("Expected 0 concurrent queries, got %d (%v)", len(nonconcurrentQueries), nonconcurrentQueries)
|
||||
}
|
||||
if len(concurrentQueries) != 1 {
|
||||
t.Fatalf("Expected 1 concurrent query, got %d (%v)", len(concurrentQueries), concurrentQueries)
|
||||
}
|
||||
|
||||
expectedQuery := "SELECT {{_collections}}.* FROM `_collections` ORDER BY `rowid` ASC"
|
||||
if concurrentQueries[0] != expectedQuery {
|
||||
t.Fatalf("Expected query\n%s\ngot\n%s", expectedQuery, concurrentQueries[0])
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,10 @@ package core
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/hook"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
@@ -124,4 +126,29 @@ func (app *BaseApp) registerOTPHooks() {
|
||||
app.Logger().Warn("Failed to delete expired OTP sessions", "error", err)
|
||||
}
|
||||
})
|
||||
|
||||
// delete all record OTPs on tokenKey change to minimize the risk of hijacking attacks
|
||||
app.OnRecordUpdateExecute().Bind(&hook.Handler[*RecordEvent]{
|
||||
Func: func(e *RecordEvent) error {
|
||||
err := e.Next()
|
||||
if err != nil || !e.Record.Collection().IsAuth() {
|
||||
return err
|
||||
}
|
||||
|
||||
if !e.Record.Original().IsNew() && e.Record.Original().TokenKey() != e.Record.TokenKey() {
|
||||
err := e.App.DeleteAllOTPsByRecord(e.Record)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
"[%s] failed to delete all previos OTPs for record %q: %w",
|
||||
e.Record.Collection().Name,
|
||||
e.Record.Id,
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Priority: 99,
|
||||
})
|
||||
}
|
||||
@@ -300,3 +300,64 @@ func TestOTPValidateHook(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOTPClearOnTokenKeyChange(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
user1, err := app.FindAuthRecordByEmail("users", "test@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
user2, err := app.FindAuthRecordByEmail("users", "test2@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
otpsToCreate := map[*core.Record]int{
|
||||
user1: 3,
|
||||
user2: 2,
|
||||
}
|
||||
for user, total := range otpsToCreate {
|
||||
for range total {
|
||||
otp := core.NewOTP(app)
|
||||
otp.SetCollectionRef(user.Collection().Id)
|
||||
otp.SetRecordRef(user.Id)
|
||||
otp.SetPassword("123456")
|
||||
if err := app.Save(otp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update both users
|
||||
err = app.Save(user1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
user2.RefreshTokenKey()
|
||||
err = app.Save(user2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedOTPs := map[*core.Record]int{
|
||||
user1: 3,
|
||||
user2: 0,
|
||||
}
|
||||
|
||||
for user, expected := range expectedOTPs {
|
||||
otps, err := app.FindAllOTPsByRecord(user)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(otps) != expected {
|
||||
t.Fatalf("Expected %d OTPs, got %d", expected, len(otps))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1428,13 +1428,13 @@ func onRecordValidate(e *RecordEvent) error {
|
||||
|
||||
func onRecordSaveExecute(e *RecordEvent) error {
|
||||
if e.Record.Collection().IsAuth() {
|
||||
// ensure that the token key is regenerated on password change or email change
|
||||
if !e.Record.IsNew() {
|
||||
lastSavedRecord, err := e.App.FindRecordById(e.Record.Collection(), e.Record.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ensure that the token key is regenerated on password change or email change
|
||||
if lastSavedRecord.TokenKey() == e.Record.TokenKey() &&
|
||||
(lastSavedRecord.Get(FieldNamePassword) != e.Record.Get(FieldNamePassword) ||
|
||||
lastSavedRecord.Email() != e.Record.Email()) {
|
||||
@@ -1442,7 +1442,8 @@ func onRecordSaveExecute(e *RecordEvent) error {
|
||||
}
|
||||
}
|
||||
|
||||
// cross-check that the auth record id is unique across all auth collections.
|
||||
// loosely cross-check that the auth record id is unique across all auth collections
|
||||
// to minimize impact of mistakes in API rules when multiple auth collections are used
|
||||
authCollections, err := e.App.FindAllCollections(CollectionTypeAuth)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to fetch the auth collections for cross-id unique check: %w", err)
|
||||
|
||||
@@ -162,11 +162,20 @@ func (app *BaseApp) expandRecords(records []*Record, expandPath string, fetchFun
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
// extract the id of the relations to expand
|
||||
// extract the unique ids of the relations to expand
|
||||
// (the initial size assumes that most of the relations are single and unique)
|
||||
existsSet := make(map[string]struct{}, len(records))
|
||||
relIds := make([]string, 0, len(records))
|
||||
for _, record := range records {
|
||||
relIds = append(relIds, record.GetStringSlice(relField.Name)...)
|
||||
ids := record.GetStringSlice(relField.Name)
|
||||
for _, id := range ids {
|
||||
if _, ok := existsSet[id]; !ok {
|
||||
existsSet[id] = struct{}{}
|
||||
relIds = append(relIds, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
existsSet = nil
|
||||
|
||||
// fetch rels
|
||||
rels, relsErr := fetchFunc(relCollection, relIds)
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package core_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
"github.com/pocketbase/pocketbase/tools/list"
|
||||
@@ -484,3 +488,41 @@ func TestBackRelationExpandSingeVsArrayResult(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandRecordsQuerySkipDuplicatedIds(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
// fetch records that are known to have at least 1 common relation between them
|
||||
records, err := app.FindRecordsByIds("demo1", []string{"84nmscqy84lsi1t", "al1h9ijdeojtsjy"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// log selects
|
||||
concurrentQueries := []string{}
|
||||
app.ConcurrentDB().(*dbx.DB).QueryLogFunc = func(ctx context.Context, t time.Duration, sql string, rows *sql.Rows, err error) {
|
||||
concurrentQueries = append(concurrentQueries, sql)
|
||||
}
|
||||
app.ConcurrentDB().(*dbx.DB).ExecLogFunc = func(ctx context.Context, t time.Duration, sql string, result sql.Result, err error) {
|
||||
concurrentQueries = append(concurrentQueries, sql)
|
||||
}
|
||||
|
||||
// expand
|
||||
failed := app.ExpandRecords(records, []string{"rel_many"}, nil)
|
||||
if len(failed) > 0 {
|
||||
t.Fatalf("Expected no expand errors, got %v", failed)
|
||||
}
|
||||
|
||||
if len(concurrentQueries) != 1 {
|
||||
t.Fatalf("Expected exactly 1 expand query, got %d:\n%v", len(concurrentQueries), concurrentQueries)
|
||||
}
|
||||
|
||||
// "oap640cot4yru2s" is used in both relations but must exists only once
|
||||
expected := "SELECT `users`.* FROM `users` WHERE `users`.`id` IN ('oap640cot4yru2s', 'bgs820n361vj1qd', '4q1xlclmfloku33')"
|
||||
if concurrentQueries[0] != expected {
|
||||
t.Fatalf("Expected query\n%v\ngot\n%v", expected, concurrentQueries[0])
|
||||
}
|
||||
}
|
||||
+28
-2
@@ -120,6 +120,10 @@ var (
|
||||
)
|
||||
|
||||
type settings struct {
|
||||
// SuperuserIPs defines an optional list of the superuser allowed
|
||||
// individual IPs and subnets (in CIDR notation).
|
||||
SuperuserIPs []string `form:"superuserIPs" json:"superuserIPs"`
|
||||
|
||||
SMTP SMTPConfig `form:"smtp" json:"smtp"`
|
||||
Backups BackupsConfig `form:"backups" json:"backups"`
|
||||
S3 S3Config `form:"s3" json:"s3"`
|
||||
@@ -253,6 +257,12 @@ func (s *Settings) DBExport(app App) (map[string]any, error) {
|
||||
}
|
||||
result["updated"] = now
|
||||
|
||||
// @todo remove with encoding/json/2
|
||||
// serialize as empty array
|
||||
if s.settings.SuperuserIPs == nil {
|
||||
s.settings.SuperuserIPs = []string{}
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(s.settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -280,6 +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.Required, validation.By(validators.IPOrSubnet))),
|
||||
validation.Field(&s.Meta),
|
||||
validation.Field(&s.Logs),
|
||||
validation.Field(&s.SMTP),
|
||||
@@ -343,6 +354,12 @@ func (s *Settings) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// @todo remove with encoding/json/2
|
||||
// serialize as empty array
|
||||
if copy.SuperuserIPs == nil {
|
||||
copy.SuperuserIPs = []string{}
|
||||
}
|
||||
|
||||
return json.Marshal(copy)
|
||||
}
|
||||
|
||||
@@ -531,6 +548,7 @@ func (c MetaConfig) Validate() error {
|
||||
return validation.ValidateStruct(&c,
|
||||
validation.Field(&c.AccentColor, validation.Length(7, 7), is.HexColor),
|
||||
validation.Field(&c.AppName, validation.Required, validation.Length(1, 255)),
|
||||
// @todo when replacing the URL validator we may need a system migration to normalize values without protocol
|
||||
validation.Field(&c.AppURL, validation.Required, is.URL),
|
||||
validation.Field(&c.SenderName, validation.Required, validation.Length(1, 255)),
|
||||
validation.Field(&c.SenderAddress, is.EmailFormat, validation.Required),
|
||||
@@ -587,8 +605,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.
|
||||
@@ -633,6 +652,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))
|
||||
}
|
||||
@@ -645,6 +667,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 := `{"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 := `{"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,6 +196,7 @@ func TestSettingsValidate(t *testing.T) {
|
||||
s := app.Settings()
|
||||
|
||||
// set invalid settings data
|
||||
s.SuperuserIPs = []string{"127.0.0.1", ""}
|
||||
s.Meta.AppName = ""
|
||||
s.Logs.MaxDays = -10
|
||||
s.SMTP.Enabled = true
|
||||
@@ -217,6 +218,7 @@ func TestSettingsValidate(t *testing.T) {
|
||||
}
|
||||
|
||||
expectations := []string{
|
||||
`"superuserIPs":{`,
|
||||
`"meta":{`,
|
||||
`"logs":{`,
|
||||
`"smtp":{`,
|
||||
@@ -595,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/",
|
||||
@@ -609,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",
|
||||
|
||||
@@ -31,7 +31,7 @@ func UploadedFileSize(maxBytes int64) validation.RuleFunc {
|
||||
"validation_file_size_limit",
|
||||
"Failed to upload {{.file}} - the maximum allowed file size is {{.maxSize}} bytes.",
|
||||
).SetParams(map[string]any{
|
||||
"file": v.OriginalName,
|
||||
"file": cutStr(v.OriginalName, 300),
|
||||
"maxSize": maxBytes,
|
||||
})
|
||||
}
|
||||
@@ -60,7 +60,7 @@ func UploadedFileMimeType(validTypes []string) validation.RuleFunc {
|
||||
|
||||
baseErr := validation.NewError(
|
||||
"validation_invalid_mime_type",
|
||||
fmt.Sprintf("Failed to upload %q due to unsupported file type.", v.OriginalName),
|
||||
fmt.Sprintf("Failed to upload %q due to unsupported file type.", cutStr(v.OriginalName, 300)),
|
||||
)
|
||||
|
||||
if len(validTypes) == 0 {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package validators
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"regexp"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
@@ -27,3 +28,30 @@ func IsRegex(value any) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IPOrSubnet checks whether the validated value is an individual
|
||||
// IPv4/IPv6 or CIDR subnet.
|
||||
func IPOrSubnet(value any) error {
|
||||
v, ok := value.(string)
|
||||
if !ok {
|
||||
return ErrUnsupportedValueType
|
||||
}
|
||||
|
||||
if v == "" {
|
||||
return nil // nothing to check
|
||||
}
|
||||
|
||||
// subnet
|
||||
_, err := netip.ParsePrefix(v)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// individual IP
|
||||
_, err = netip.ParseAddr(v)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return validation.NewError("validation_invlaid_ip_or_subnet", "invalid IP or CIDR subnet")
|
||||
}
|
||||
@@ -31,3 +31,32 @@ func TestIsRegex(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPOrSubnet(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scenarios := []struct {
|
||||
val string
|
||||
expectError bool
|
||||
}{
|
||||
{"", false},
|
||||
{`invalid`, true},
|
||||
{`127.0`, true}, // incomplete
|
||||
{`127.0.0.1`, false},
|
||||
{`::1`, false},
|
||||
{`0000:0000:0000:0000:0000:0000:0000:0001`, false},
|
||||
{`127.0.0.1/24`, false},
|
||||
{`::/128`, false},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
t.Run(fmt.Sprintf("%d_%#v", i, s.val), func(t *testing.T) {
|
||||
err := validators.IPOrSubnet(s.val)
|
||||
|
||||
hasErr := err != nil
|
||||
if hasErr != s.expectError {
|
||||
t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -38,3 +38,10 @@ func JoinValidationErrors(errA, errB error) error {
|
||||
|
||||
return errors.Join(errA, errB)
|
||||
}
|
||||
|
||||
func cutStr(str string, max int) string {
|
||||
if len(str) > max {
|
||||
return str[:max] + "..."
|
||||
}
|
||||
return str
|
||||
}
|
||||
@@ -22,7 +22,7 @@ require (
|
||||
golang.org/x/net v0.53.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sync v0.20.0
|
||||
modernc.org/sqlite v1.49.1
|
||||
modernc.org/sqlite v1.50.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -35,7 +35,7 @@ require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
|
||||
@@ -55,8 +55,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -134,8 +134,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U=
|
||||
modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
||||
modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM=
|
||||
modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
expectedDriverVersion = "v1.49.1"
|
||||
expectedDriverVersion = "v1.50.0"
|
||||
expectedLibcVersion = "v1.72.0"
|
||||
|
||||
// ModerncDepsCheckHookId is the id of the hook that performs the modernc.org/* deps checks.
|
||||
|
||||
@@ -46,6 +46,12 @@ type Config struct {
|
||||
// (default to "pocketbase"; an additional ".exe" check is also performed as a fallback).
|
||||
ArchiveExecutable string
|
||||
|
||||
// BaseURL is the base URL of the GitHub API (or similar compatible)
|
||||
// used to fetch the latest releases information.
|
||||
//
|
||||
// Defaults to "https://api.github.com".
|
||||
BaseURL string
|
||||
|
||||
// Optional context to use when fetching and downloading the latest release.
|
||||
Context context.Context
|
||||
|
||||
@@ -82,6 +88,12 @@ func Register(app core.App, rootCmd *cobra.Command, config Config) error {
|
||||
p.config.ArchiveExecutable = "pocketbase"
|
||||
}
|
||||
|
||||
if p.config.BaseURL == "" {
|
||||
p.config.BaseURL = "https://api.github.com"
|
||||
} else {
|
||||
p.config.BaseURL = strings.TrimRight(p.config.BaseURL, "/")
|
||||
}
|
||||
|
||||
if p.config.HttpClient == nil {
|
||||
p.config.HttpClient = http.DefaultClient
|
||||
}
|
||||
@@ -145,12 +157,9 @@ func (p *plugin) updateCmd() *cobra.Command {
|
||||
func (p *plugin) update(withBackup bool) error {
|
||||
color.Yellow("Fetching release information...")
|
||||
|
||||
latest, err := fetchLatestRelease(
|
||||
p.config.Context,
|
||||
p.config.HttpClient,
|
||||
p.config.Owner,
|
||||
p.config.Repo,
|
||||
)
|
||||
url := fmt.Sprintf("%s/repos/%s/%s/releases/latest", p.config.BaseURL, p.config.Owner, p.config.Repo)
|
||||
|
||||
latest, err := fetchLatestRelease(p.config.Context, p.config.HttpClient, url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -260,11 +269,8 @@ func (p *plugin) update(withBackup bool) error {
|
||||
func fetchLatestRelease(
|
||||
ctx context.Context,
|
||||
client HttpClient,
|
||||
owner string,
|
||||
repo string,
|
||||
url string,
|
||||
) (*release, error) {
|
||||
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+4176
-4156
File diff suppressed because it is too large.
Load diff
@@ -137,7 +137,7 @@ migrate((app) => {
|
||||
"listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "`" + `test' = 0",
|
||||
"manageRule": "1 != 2",
|
||||
"mfa": {
|
||||
"duration": 1800,
|
||||
"duration": 600,
|
||||
"enabled": false,
|
||||
"rule": ""
|
||||
},
|
||||
@@ -319,7 +319,7 @@ func init() {
|
||||
"listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "` + \"`\" + `" + `test' = 0",
|
||||
"manageRule": "1 != 2",
|
||||
"mfa": {
|
||||
"duration": 1800,
|
||||
"duration": 600,
|
||||
"enabled": false,
|
||||
"rule": ""
|
||||
},
|
||||
@@ -590,7 +590,7 @@ migrate((app) => {
|
||||
"listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "`" + `test' = 0",
|
||||
"manageRule": "1 != 2",
|
||||
"mfa": {
|
||||
"duration": 1800,
|
||||
"duration": 600,
|
||||
"enabled": false,
|
||||
"rule": ""
|
||||
},
|
||||
@@ -775,7 +775,7 @@ func init() {
|
||||
"listRule": "@request.auth.id != '' && 1 > 0 || 'backtick` + "` + \"`\" + `" + `test' = 0",
|
||||
"manageRule": "1 != 2",
|
||||
"mfa": {
|
||||
"duration": 1800,
|
||||
"duration": 600,
|
||||
"enabled": false,
|
||||
"rule": ""
|
||||
},
|
||||
|
||||
@@ -120,8 +120,9 @@ func (p *Bitbucket) fetchPrimaryEmail(token *oauth2.Token) (string, error) {
|
||||
|
||||
expected := struct {
|
||||
Values []struct {
|
||||
Email string `json:"email"`
|
||||
IsPrimary bool `json:"is_primary"`
|
||||
Email string `json:"email"`
|
||||
IsPrimary bool `json:"is_primary"`
|
||||
IsConfirmed bool `json:"is_confirmed"`
|
||||
} `json:"values"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &expected); err != nil {
|
||||
@@ -129,7 +130,7 @@ func (p *Bitbucket) fetchPrimaryEmail(token *oauth2.Token) (string, error) {
|
||||
}
|
||||
|
||||
for _, v := range expected.Values {
|
||||
if v.IsPrimary {
|
||||
if v.IsPrimary && v.IsConfirmed {
|
||||
return v.Email, nil
|
||||
}
|
||||
}
|
||||
|
||||
+65
-7
@@ -3,6 +3,9 @@ package auth
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
@@ -15,10 +18,10 @@ func init() {
|
||||
|
||||
var _ Provider = (*Gitea)(nil)
|
||||
|
||||
// NameGitea is the unique name of the Gitea provider.
|
||||
// NameGitea is the unique name of the Gitea/Forgejo provider.
|
||||
const NameGitea string = "gitea"
|
||||
|
||||
// Gitea allows authentication via Gitea OAuth2.
|
||||
// Gitea allows authentication via Gitea/Forgejo OAuth2.
|
||||
type Gitea struct {
|
||||
BaseProvider
|
||||
}
|
||||
@@ -29,7 +32,7 @@ func NewGiteaProvider() *Gitea {
|
||||
ctx: context.Background(),
|
||||
order: 11,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 640 640"><path d="m396 484-127-61c-12-6-18-21-12-34l61-127c6-12 21-17 34-11l27 13V154h17v118s57 24 83 40q7 3 13 14 3 10-1 19l-61 127c-6 13-22 18-34 12" style="fill:#fff"/><path d="M623 150c-4-4-10-4-10-4l-178 8-39 1v117l-17-8V155l-89-3-157-8q-15-2-39 1c-9 2-34 8-54 27C-5 212 7 276 8 286c2 12 7 44 32 72 46 56 144 55 144 55s12 29 31 56c25 33 50 59 75 62h189s12 0 29-11c14-8 26-23 26-23s13-14 31-45l14-28s55-118 55-232c-1-34-9-40-11-42M126 354c-26-9-37-19-37-19s-19-13-29-40c-16-44-1-71-1-71s8-22 38-30c14-4 31-3 31-3s7 59 16 94c7 30 25 78 25 78s-26-3-43-9m300 108s-6 14-20 15l-10-1-5-2-113-55s-11-6-13-16c-2-8 3-18 3-18l54-112s5-10 12-13l5-1c8-3 18 2 18 2l110 54s13 6 16 16q1 12-2 17c-6 16-55 114-55 114" style="fill:#609926"/><path d="M327 380q-14 1-17 14-2 13 9 20c7 4 17 2 22-5q8-13-1-24l24-49h6l7-4 29 16 5 5c2 6-2 15-2 15-2 7-18 40-18 40q-13 1-18 13-4 14 9 21a18 18 0 0 0 21-28l6-11 13-30c1-2 6-11 3-22-2-11-13-16-13-16-12-8-29-16-29-16l-1-7-4-6 14-29-12-6-14 29q-11 0-16 10t1 20z" style="fill:#609926"/></svg>`,
|
||||
displayName: "Gitea",
|
||||
displayName: "Gitea/Forgejo",
|
||||
pkce: true,
|
||||
scopes: []string{"read:user", "user:email"},
|
||||
authURL: "https://gitea.com/login/oauth/authorize",
|
||||
@@ -38,9 +41,9 @@ func NewGiteaProvider() *Gitea {
|
||||
}}
|
||||
}
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based on Gitea's user api.
|
||||
// FetchAuthUser returns an AuthUser instance based on Gitea/Forgejo's user api.
|
||||
//
|
||||
// API reference: https://try.gitea.io/api/swagger#/user/userGetCurrent
|
||||
// API reference: https://codeberg.org/api/swagger#/user/userGetCurrent
|
||||
func (p *Gitea) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
@@ -55,26 +58,81 @@ func (p *Gitea) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
extracted := struct {
|
||||
Name string `json:"full_name"`
|
||||
Username string `json:"login"`
|
||||
Email string `json:"email"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Id int64 `json:"id"`
|
||||
Active bool `json:"active"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !extracted.Active {
|
||||
return nil, errors.New("user account is not active")
|
||||
}
|
||||
|
||||
user := &AuthUser{
|
||||
Id: strconv.FormatInt(extracted.Id, 10),
|
||||
Name: extracted.Name,
|
||||
Username: extracted.Username,
|
||||
Email: extracted.Email,
|
||||
AvatarURL: extracted.AvatarURL,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
}
|
||||
|
||||
email, err := p.fetchVerifiedPrimaryEmail(token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch primary email: %w", err)
|
||||
}
|
||||
user.Email = email
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// fetchVerifiedPrimaryEmail sends an API request to retrieve the verified
|
||||
// primary email, in case "Keep my email address private" was set.
|
||||
//
|
||||
// NB! This method can succeed and still return an empty email.
|
||||
// Error responses that are result of insufficient scopes permissions are ignored.
|
||||
//
|
||||
// API reference: https://codeberg.org/api/swagger#/user/userListEmails
|
||||
func (p *Gitea) fetchVerifiedPrimaryEmail(token *oauth2.Token) (string, error) {
|
||||
client := p.Client(token)
|
||||
|
||||
response, err := client.Get(p.userInfoURL + "/emails")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
// ignore common http errors caused by insufficient scope permissions
|
||||
// (the email field is optional, aka. return the auth user without it)
|
||||
if response.StatusCode == 401 || response.StatusCode == 403 || response.StatusCode == 404 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
content, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
emails := []struct {
|
||||
Email string
|
||||
Verified bool
|
||||
Primary bool
|
||||
}{}
|
||||
if err := json.Unmarshal(content, &emails); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// extract the verified primary email
|
||||
for _, email := range emails {
|
||||
if email.Verified && email.Primary {
|
||||
return email.Email, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
+12
-15
@@ -42,7 +42,7 @@ func NewGithubProvider() *Github {
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based the Github's user api.
|
||||
//
|
||||
// API reference: https://docs.github.com/en/rest/reference/users#get-the-authenticated-user
|
||||
// API reference: https://docs.github.com/en/rest/users/users?apiVersion=2026-03-10#get-the-authenticated-user
|
||||
func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
@@ -55,9 +55,8 @@ func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Login string `json:"login"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Login string `json:"login"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Id int64 `json:"id"`
|
||||
}{}
|
||||
@@ -69,7 +68,6 @@ func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
Id: strconv.FormatInt(extracted.Id, 10),
|
||||
Name: extracted.Name,
|
||||
Username: extracted.Login,
|
||||
Email: extracted.Email,
|
||||
AvatarURL: extracted.AvatarURL,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
@@ -78,27 +76,26 @@ func (p *Github) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
// in case user has set "Keep my email address private", send an
|
||||
// **optional** API request to retrieve the verified primary email
|
||||
if user.Email == "" {
|
||||
email, err := p.fetchPrimaryEmail(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.Email = email
|
||||
// always send a primary email request even though the email is
|
||||
// returned in the userinfo endpoint since the API may change and
|
||||
// enterprise setups may have configuration that could allow unverified emails
|
||||
email, err := p.fetchVerifiedPrimaryEmail(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.Email = email
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// fetchPrimaryEmail sends an API request to retrieve the verified
|
||||
// fetchVerifiedPrimaryEmail sends an API request to retrieve the verified
|
||||
// primary email, in case "Keep my email address private" was set.
|
||||
//
|
||||
// NB! This method can succeed and still return an empty email.
|
||||
// Error responses that are result of insufficient scopes permissions are ignored.
|
||||
//
|
||||
// API reference: https://docs.github.com/en/rest/users/emails?apiVersion=2022-11-28
|
||||
func (p *Github) fetchPrimaryEmail(token *oauth2.Token) (string, error) {
|
||||
// API reference: https://docs.github.com/en/rest/users/emails?apiVersion=2022-11-28#list-email-addresses-for-the-authenticated-user
|
||||
func (p *Github) fetchVerifiedPrimaryEmail(token *oauth2.Token) (string, error) {
|
||||
client := p.Client(token)
|
||||
|
||||
response, err := client.Get(p.userInfoURL + "/emails")
|
||||
|
||||
+13
-7
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
"golang.org/x/oauth2"
|
||||
@@ -40,7 +41,7 @@ func NewGitlabProvider() *Gitlab {
|
||||
|
||||
// FetchAuthUser returns an AuthUser instance based the Gitlab's user api.
|
||||
//
|
||||
// API reference: https://docs.gitlab.com/ee/api/users.html#for-admin
|
||||
// API reference: https://docs.gitlab.com/api/users/#retrieve-the-current-user
|
||||
func (p *Gitlab) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
data, err := p.FetchRawUserInfo(token)
|
||||
if err != nil {
|
||||
@@ -53,11 +54,12 @@ func (p *Gitlab) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
}
|
||||
|
||||
extracted := struct {
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
ConfirmedAt string `json:"confirmed_at"`
|
||||
Id int64 `json:"id"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extracted); err != nil {
|
||||
return nil, err
|
||||
@@ -67,7 +69,6 @@ func (p *Gitlab) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
Id: strconv.FormatInt(extracted.Id, 10),
|
||||
Name: extracted.Name,
|
||||
Username: extracted.Username,
|
||||
Email: extracted.Email,
|
||||
AvatarURL: extracted.AvatarURL,
|
||||
RawUser: rawUser,
|
||||
AccessToken: token.AccessToken,
|
||||
@@ -76,5 +77,10 @@ func (p *Gitlab) FetchAuthUser(token *oauth2.Token) (*AuthUser, error) {
|
||||
|
||||
user.Expiry, _ = types.ParseDateTime(token.Expiry)
|
||||
|
||||
confirmedAt, err := time.Parse(time.RFC3339, extracted.ConfirmedAt)
|
||||
if err == nil && !confirmedAt.IsZero() {
|
||||
user.Email = extracted.Email
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
@@ -114,12 +114,12 @@ func Fetch(ctx context.Context, jwksURL string, kid string) (*JWK, error) {
|
||||
}
|
||||
|
||||
for _, key := range jwks.Keys {
|
||||
if key.Kid == kid {
|
||||
if key.Kid == kid && key.Alg != "" {
|
||||
return key, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("JWK with kid %q was not found", kid)
|
||||
return nil, fmt.Errorf("missing JWK with kid %q and non-empty alg", kid)
|
||||
}
|
||||
|
||||
// ValidateTokenSignature validates the signature of a token with the
|
||||
|
||||
@@ -168,6 +168,12 @@ func TestFetch(t *testing.T) {
|
||||
true,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"matching kid (no alg)",
|
||||
"abc",
|
||||
true,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"matching kid",
|
||||
"def",
|
||||
|
||||
@@ -411,10 +411,20 @@ var inlineServeContentTypes = []string{
|
||||
|
||||
// manualExtensionContentTypes is a map of file extensions to content types.
|
||||
var manualExtensionContentTypes = map[string]string{
|
||||
".svg": "image/svg+xml", // (see https://github.com/whatwg/mimesniff/issues/7)
|
||||
".css": "text/css", // (see https://github.com/gabriel-vasile/mimetype/pull/113)
|
||||
".js": "text/javascript", // (see https://github.com/pocketbase/pocketbase/issues/6597)
|
||||
// https://github.com/whatwg/mimesniff/issues/7
|
||||
".svg": "image/svg+xml",
|
||||
|
||||
// https://github.com/gabriel-vasile/mimetype/pull/113
|
||||
".css": "text/css",
|
||||
|
||||
// https://github.com/pocketbase/pocketbase/issues/6597
|
||||
".js": "text/javascript",
|
||||
".mjs": "text/javascript",
|
||||
|
||||
// https://github.com/pocketbase/pocketbase/discussions/7467
|
||||
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
}
|
||||
|
||||
// forceAttachmentParam is the name of the request query parameter to
|
||||
@@ -449,7 +459,7 @@ func (s *System) Serve(res http.ResponseWriter, req *http.Request, fileKey strin
|
||||
// make an exception for specific content types and force a custom
|
||||
// content type to send in the response so that it can be loaded properly
|
||||
extContentType := realContentType
|
||||
if ct, found := manualExtensionContentTypes[filepath.Ext(name)]; found {
|
||||
if ct, found := manualExtensionContentTypes[filepath.Ext(fileKey)]; found {
|
||||
extContentType = ct
|
||||
}
|
||||
|
||||
|
||||
@@ -405,12 +405,12 @@ func TestFileSystemServe(t *testing.T) {
|
||||
{
|
||||
// svg exception
|
||||
"image.svg",
|
||||
"test_name.svg",
|
||||
"test_name.abc",
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
map[string]string{
|
||||
"Content-Disposition": "attachment; filename=test_name.svg",
|
||||
"Content-Disposition": "attachment; filename=test_name.abc",
|
||||
"Content-Type": "image/svg+xml",
|
||||
"Content-Length": "0",
|
||||
"Content-Security-Policy": csp,
|
||||
@@ -420,12 +420,12 @@ func TestFileSystemServe(t *testing.T) {
|
||||
{
|
||||
// css exception
|
||||
"style.css",
|
||||
"test_name.css",
|
||||
"test_name",
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
map[string]string{
|
||||
"Content-Disposition": "attachment; filename=test_name.css",
|
||||
"Content-Disposition": "attachment; filename=test_name",
|
||||
"Content-Type": "text/css",
|
||||
"Content-Length": "0",
|
||||
"Content-Security-Policy": csp,
|
||||
@@ -435,12 +435,12 @@ func TestFileSystemServe(t *testing.T) {
|
||||
{
|
||||
// js exception
|
||||
"main.js",
|
||||
"test_name.js",
|
||||
"test_name.abc",
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
map[string]string{
|
||||
"Content-Disposition": "attachment; filename=test_name.js",
|
||||
"Content-Disposition": "attachment; filename=test_name.abc",
|
||||
"Content-Type": "text/javascript",
|
||||
"Content-Length": "0",
|
||||
"Content-Security-Policy": csp,
|
||||
@@ -450,18 +450,63 @@ func TestFileSystemServe(t *testing.T) {
|
||||
{
|
||||
// mjs exception
|
||||
"main.mjs",
|
||||
"test_name.mjs",
|
||||
"test_name.abc",
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
map[string]string{
|
||||
"Content-Disposition": "attachment; filename=test_name.mjs",
|
||||
"Content-Disposition": "attachment; filename=test_name.abc",
|
||||
"Content-Type": "text/javascript",
|
||||
"Content-Length": "0",
|
||||
"Content-Security-Policy": csp,
|
||||
"Cache-Control": cacheControl,
|
||||
},
|
||||
},
|
||||
{
|
||||
// xlsx exception
|
||||
"dummy.xlsx",
|
||||
"test_name.abc",
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
map[string]string{
|
||||
"Content-Disposition": "attachment; filename=test_name.abc",
|
||||
"Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"Content-Length": "0",
|
||||
"Content-Security-Policy": csp,
|
||||
"Cache-Control": cacheControl,
|
||||
},
|
||||
},
|
||||
{
|
||||
// docx exception
|
||||
"dummy.docx",
|
||||
"test_name.abc",
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
map[string]string{
|
||||
"Content-Disposition": "attachment; filename=test_name.abc",
|
||||
"Content-Type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"Content-Length": "0",
|
||||
"Content-Security-Policy": csp,
|
||||
"Cache-Control": cacheControl,
|
||||
},
|
||||
},
|
||||
{
|
||||
// pptx exception
|
||||
"dummy.pptx",
|
||||
"test_name.abc",
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
map[string]string{
|
||||
"Content-Disposition": "attachment; filename=test_name.abc",
|
||||
"Content-Type": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"Content-Length": "0",
|
||||
"Content-Security-Policy": csp,
|
||||
"Cache-Control": cacheControl,
|
||||
},
|
||||
},
|
||||
{
|
||||
// custom header
|
||||
"test/sub2.txt",
|
||||
@@ -716,6 +761,9 @@ func TestFileSystemList(t *testing.T) {
|
||||
"style.css",
|
||||
"main.js",
|
||||
"main.mjs",
|
||||
"dummy.xlsx",
|
||||
"dummy.docx",
|
||||
"dummy.pptx",
|
||||
"test/sub1.txt",
|
||||
"test/sub2.txt",
|
||||
},
|
||||
@@ -1041,5 +1089,32 @@ func createTestDir(t *testing.T) string {
|
||||
file.Close()
|
||||
}
|
||||
|
||||
// "docx" (we are interested only in the extension)
|
||||
{
|
||||
file, err := os.OpenFile(filepath.Join(dir, "dummy.docx"), os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
|
||||
// "xlsx" (we are interested only in the extension)
|
||||
{
|
||||
file, err := os.OpenFile(filepath.Join(dir, "dummy.xlsx"), os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
|
||||
// "pptx" (we are interested only in the extension)
|
||||
{
|
||||
file, err := os.OpenFile(filepath.Join(dir, "dummy.pptx"), os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
|
||||
return dir
|
||||
}
|
||||
+12
-7
@@ -3,7 +3,9 @@ package mailer
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/domodwyer/mailyak/v3"
|
||||
@@ -70,16 +72,18 @@ func (c *SMTPClient) send(m *Message) error {
|
||||
}
|
||||
}
|
||||
|
||||
hostWithPort := net.JoinHostPort(c.Host, strconv.Itoa(c.Port))
|
||||
|
||||
// create mail instance
|
||||
var yak *mailyak.MailYak
|
||||
if c.TLS {
|
||||
var tlsErr error
|
||||
yak, tlsErr = mailyak.NewWithTLS(fmt.Sprintf("%s:%d", c.Host, c.Port), smtpAuth, nil)
|
||||
yak, tlsErr = mailyak.NewWithTLS(hostWithPort, smtpAuth, nil)
|
||||
if tlsErr != nil {
|
||||
return tlsErr
|
||||
}
|
||||
} else {
|
||||
yak = mailyak.New(fmt.Sprintf("%s:%d", c.Host, c.Port), smtpAuth)
|
||||
yak = mailyak.New(hostWithPort, smtpAuth)
|
||||
}
|
||||
|
||||
if c.LocalName != "" {
|
||||
@@ -133,15 +137,16 @@ func (c *SMTPClient) send(m *Message) error {
|
||||
}
|
||||
|
||||
// add custom headers (if any)
|
||||
var hasMessageId bool
|
||||
var hasMessageIdHeader bool
|
||||
for k, v := range m.Headers {
|
||||
if strings.EqualFold(k, "Message-ID") {
|
||||
hasMessageId = true
|
||||
if !hasMessageIdHeader && strings.EqualFold(k, "Message-ID") {
|
||||
hasMessageIdHeader = true
|
||||
}
|
||||
yak.AddHeader(k, v)
|
||||
}
|
||||
if !hasMessageId {
|
||||
// add a default message id if missing
|
||||
|
||||
// add a default message id if missing
|
||||
if !hasMessageIdHeader {
|
||||
fromParts := strings.Split(m.From.Address, "@")
|
||||
if len(fromParts) == 2 {
|
||||
yak.AddHeader("Message-ID", fmt.Sprintf("<%s@%s>",
|
||||
|
||||
@@ -349,7 +349,7 @@ const DefaultMaxMemory = 16 << 20 // 16mb
|
||||
//
|
||||
// Title string `json:"title" form:"title"`
|
||||
// Total int `json:"total" form:"total"`
|
||||
// }
|
||||
// }{}
|
||||
// err := e.BindBody(&data)
|
||||
func (e *Event) BindBody(dst any) error {
|
||||
if e.Request.ContentLength == 0 {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
# all environments should start with 'PB_' prefix
|
||||
PB_BACKEND_URL = "../"
|
||||
PB_MFA_DOCS = "https://pocketbase.io/docs/authentication#multi-factor-authentication"
|
||||
PB_OAUTH2_DOCS = "https://pocketbase.io/docs/authentication#authenticate-with-oauth2"
|
||||
PB_RULES_SYNTAX_DOCS = "https://pocketbase.io/docs/api-rules-and-filters"
|
||||
PB_FILE_UPLOAD_DOCS = "https://pocketbase.io/docs/files-handling"
|
||||
PB_PROTECTED_FILE_DOCS = "https://pocketbase.io/docs/files-handling#protected-files"
|
||||
PB_REALTIME_DOCS = "https://pocketbase.io/docs/api-realtime/"
|
||||
PB_FIELDS_DOCS = "https://pocketbase.io/docs/collections/#fields"
|
||||
PB_DOCS_URL = "https://pocketbase.io/docs"
|
||||
PB_JS_SDK_URL = "https://github.com/pocketbase/js-sdk"
|
||||
PB_DART_SDK_URL = "https://github.com/pocketbase/dart-sdk"
|
||||
PB_RELEASES = "https://github.com/pocketbase/pocketbase/releases"
|
||||
PB_VERSION = "v0.37.3"
|
||||
PB_BACKEND_URL = "../"
|
||||
PB_MFA_DOCS = "https://pocketbase.io/docs/authentication#multi-factor-authentication"
|
||||
PB_OAUTH2_DOCS = "https://pocketbase.io/docs/authentication#authenticate-with-oauth2"
|
||||
PB_RULES_SYNTAX_DOCS = "https://pocketbase.io/docs/api-rules-and-filters"
|
||||
PB_FILE_UPLOAD_DOCS = "https://pocketbase.io/docs/files-handling"
|
||||
PB_PROTECTED_FILE_DOCS = "https://pocketbase.io/docs/files-handling#protected-files"
|
||||
PB_REALTIME_DOCS = "https://pocketbase.io/docs/api-realtime/"
|
||||
PB_FIELDS_DOCS = "https://pocketbase.io/docs/collections/#fields"
|
||||
PB_SUPERUSER_IPS_RESET_DOCS = "https://pocketbase.io/docs/going-to-production/#limit-superusers-to-specific-ipssubnets"
|
||||
PB_DOCS_URL = "https://pocketbase.io/docs"
|
||||
PB_JS_SDK_URL = "https://github.com/pocketbase/js-sdk"
|
||||
PB_DART_SDK_URL = "https://github.com/pocketbase/dart-sdk"
|
||||
PB_RELEASES = "https://github.com/pocketbase/pocketbase/releases"
|
||||
PB_VERSION = "v0.38.0"
|
||||
Vendored
+82
@@ -0,0 +1,82 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./docsList-BAfVNUIM.js","./expandInfo-DGS0CLSa.js","./fieldsInfo-Bz62125-.js","./filterSyntax-Df6AnI5x.js","./docsView-Be6Uhvcf.js","./docsCreate-Be3S3y5K.js","./docsUpdate-LtD8DbWI.js","./docsListAuthMethods-9feSopQX.js","./docsAuthWithPassword-DEWj8Jyn.js","./docsAuthWithOAuth2-DUIE4EoY.js","./docsAuthWithOTP-B7z2VJzp.js","./docsAuthRefresh-UjveHHwo.js","./pageInstaller-CboSj8bL.js","./pocketbase.es-B_4DUNUU.js","./pageConfirmPasswordReset-xSsXQyFi.js","./pageConfirmVerification-BSn_UexA.js","./pageConfirmEmailChange-DWXtM27Q.js"])))=>i.map(i=>d[i]);
|
||||
import{a as e,n,r,t as i}from"./pocketbase.es-B_4DUNUU.js";var a=Object.create,o=Object.defineProperty,s=Object.getOwnPropertyDescriptor,c=Object.getOwnPropertyNames,l=Object.getPrototypeOf,u=Object.prototype.hasOwnProperty,d=(e,n)=>()=>(n||e((n={exports:{}}).exports,n),n.exports),f=(e,n,r,i)=>{if(n&&typeof n==`object`||typeof n==`function`)for(var a=c(n),l=0,d=a.length,f;l<d;l++)f=a[l],!u.call(e,f)&&f!==r&&o(e,f,{get:(e=>n[e]).bind(null,f),enumerable:!(i=s(n,f))||i.enumerable});return e},p=(e,n,r)=>(r=e==null?{}:a(l(e)),f(n||!e||!e.__esModule?o(r,`default`,{value:e,enumerable:!0}):r,e));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))r(e);new MutationObserver(e=>{for(let n of e)if(n.type===`childList`)for(let e of n.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&r(e)}).observe(document,{childList:!0,subtree:!0});function n(e){let n={};return e.integrity&&(n.integrity=e.integrity),e.referrerPolicy&&(n.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?n.credentials=`include`:e.crossOrigin===`anonymous`?n.credentials=`omit`:n.credentials=`same-origin`,n}function r(e){if(e.ep)return;e.ep=!0;let r=n(e);fetch(e.href,r)}})();var m=`abcdefghijklmnopqrstuvwxyz0123456789`,h=`pb_redirect`,g=store({hash:window.location.hash});window.addEventListener(`hashchange`,()=>{g.hash=window.location.hash}),Prism.languages.pbrule={string:Prism.languages.js.string,number:Prism.languages.js.number,function:Prism.languages.js.function,boolean:/\b(?:true|false)\b/i,constant:/\b(?:null)\b/i,comment:{pattern:/\/\/.*/,greedy:!0},italic:/_via_|\:\w+/,keyword:/&&|\|\||\??(?:!~|!=|>=|<=|=|~|>|>|<)(?=[@\w\s]|$)/};var _={isObject(e){return typeof e==`object`&&!!e&&e.constructor===Object},isEmpty(e){return e==null||e===``||Array.isArray(e)&&e.length===0||typeof e==`object`&&app.utils.isEmptyObject(e)},isEmptyObject(e){for(let n in e)return!1;return!0},toArray(e,n=!1){return Array.isArray(e)?e.slice():(n||!_.isEmpty(e))&&e!==void 0?[e]:[]},removeByValue(e,n){if(!Array.isArray(e)){console.warn(`[removeByValue] not an array:`,e);return}for(let r=e.length-1;r>=0;r--)if(e[r]==n){e.splice(r,1);break}},removeByKey(e,n,r){if(!Array.isArray(e)){console.warn(`[removeByKey] not an array:`,e);return}for(let i in e)if(e[i][n]==r){e.splice(i,1);break}},pushUnique(e,n){if(!Array.isArray(e)){console.warn(`[pushUnique] not an array:`,e);return}e.includes(n)||e.push(n)},mergeUnique(e,n){for(let r of n)app.utils.pushUnique(e,r);return e},pushOrReplaceObject(e,n,r=`id`){for(let i=e.length-1;i>=0;i--)if(e[i][r]==n[r]){e[i]=n;return}e.push(n)},filterDuplicatesByKey(e,n=`id`){e=Array.isArray(e)?e:[];let r={};for(let i of e)r[i[n]]=i;return Object.values(r)},filterRedactedProps(e,n=`******`){let r=JSON.parse(JSON.stringify(e||{}));for(let e in r)typeof r[e]==`object`&&r[e]!==null?r[e]=_.filterRedactedProps(r[e],n):r[e]===n&&delete r[e];return r},getByPath(e,n,r=null,i=`.`){let a=e||{},o=(n||``).split(i);for(let e of o){if(!_.isObject(a)&&!Array.isArray(a)||a[e]===void 0)return r;a=a[e]}return a},setByPath(e,n,r,i=`.`){if(typeof e!=`object`||!e){console.warn(`setByPath: data not an object or array.`);return}let a=e,o=n.split(i),s=o.pop();for(let e of o)(!_.isObject(a)&&!Array.isArray(a)||!_.isObject(a[e])&&!Array.isArray(a[e]))&&(a[e]={}),a=a[e];a[s]=r},deleteByPath(e,n,r=`.`){let i=e||{},a=(n||``).split(r),o=a.pop();for(let e of a)(!_.isObject(i)&&!Array.isArray(i)||!_.isObject(i[e])&&!Array.isArray(i[e]))&&(i[e]={}),i=i[e];Array.isArray(i)?i.splice(o,1):_.isObject(i)&&delete i[o],a.length>0&&(Array.isArray(i)&&!i.length||_.isObject(i)&&!Object.keys(i).length)&&(Array.isArray(e)&&e.length>0||_.isObject(e)&&Object.keys(e).length>0)&&_.deleteByPath(e,a.join(r),r)},emptyClone(e,n=[]){let r=JSON.parse(JSON.stringify(e));for(let e in r)n.includes(e)||(typeof r[e]==`string`?r[e]=``:typeof r[e]==`number`?r[e]=0:typeof r[e]==`boolean`?r[e]=!1:Array.isArray(r[e])?r[e]=[]:app.utils.isObject(r[e])&&(r[e]={}));return r},randomString(e=8,n=m){let r=``;for(let i=0;i<e;i++)r+=n.charAt(Math.floor(Math.random()*n.length));return r},randomSecret(e=30){if(typeof crypto>`u`)return app.utils.randomString(e);let n=new Uint8Array(e);crypto.getRandomValues(n);let r=``;for(let i=0;i<e;i++)r+=`-_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`.charAt(n[i]%64);return r},sentenize(e,n=!0){if(typeof e!=`string`)return``;if(e=e.trim().split(`_`).join(` `),e===``)return e;if(e=e[0].toUpperCase()+e.substring(1),n){let n=e[e.length-1];n!==`.`&&n!==`?`&&n!==`!`&&(e+=`.`)}return e},trimQuotedValue(e){return typeof e==`string`&&(e[0]==`"`||e[0]==`'`||e[0]=="`")&&e[0]==e[e.length-1]?e.slice(1,-1):e},slugify(e,n=`_`,r=[]){if(e===``)return``;let i={a:/а|à|á|å|â/gi,b:/б/gi,c:/ц|ç/gi,d:/д/gi,e:/е|è|é|ê|ẽ|ë/gi,f:/ф/gi,g:/г/gi,h:/х/gi,i:/й|и|ì|í|î/gi,j:/ж/gi,k:/к/gi,l:/л/gi,m:/м/gi,n:/н|ñ/gi,o:/о|ò|ó|ô|øLine truncated
|
||||
`),n+=r.map(e=>{let n=``;return e.name.includes(`(`)||e.name.includes(` `)?n+=e.name:n+="`"+e.name+"`",e.collate&&(n+=` COLLATE `+e.collate),e.sort&&(n+=` `+e.sort.toUpperCase()),n}).join(`,
|
||||
`),r.length>1&&(n+=`
|
||||
`),n+=`)`,e.where&&(n+=` WHERE ${e.where}`),n},replaceIndexFields(e,n){let r=app.utils.parseIndex(e);return typeof n==`function`?Object.assign(r,n(r)||{}):Object.assign(r,n||{}),app.utils.buildIndex(r)},replaceIndexColumn(e,n,r){if(n===r)return e;let i=app.utils.parseIndex(e),a=!1;for(let e of i.columns)e.name===n&&(e.name=r,a=!0);return a?app.utils.buildIndex(i):e}};window.app=window.app||{},window.app.utils=_,window.app=window.app||{},window.app.utils=window.app.utils||{},window.app.utils.mimeTypes=[{ext:``,mimeType:`application/octet-stream`},{ext:`.xpm`,mimeType:`image/x-xpixmap`},{ext:`.7z`,mimeType:`application/x-7z-compressed`},{ext:`.zip`,mimeType:`application/zip`},{ext:`.xlsx`,mimeType:`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`},{ext:`.docx`,mimeType:`application/vnd.openxmlformats-officedocument.wordprocessingml.document`},{ext:`.pptx`,mimeType:`application/vnd.openxmlformats-officedocument.presentationml.presentation`},{ext:`.epub`,mimeType:`application/epub+zip`},{ext:`.apk`,mimeType:`application/vnd.android.package-archive`},{ext:`.jar`,mimeType:`application/jar`},{ext:`.odt`,mimeType:`application/vnd.oasis.opendocument.text`},{ext:`.ott`,mimeType:`application/vnd.oasis.opendocument.text-template`},{ext:`.ods`,mimeType:`application/vnd.oasis.opendocument.spreadsheet`},{ext:`.ots`,mimeType:`application/vnd.oasis.opendocument.spreadsheet-template`},{ext:`.odp`,mimeType:`application/vnd.oasis.opendocument.presentation`},{ext:`.otp`,mimeType:`application/vnd.oasis.opendocument.presentation-template`},{ext:`.odg`,mimeType:`application/vnd.oasis.opendocument.graphics`},{ext:`.otg`,mimeType:`application/vnd.oasis.opendocument.graphics-template`},{ext:`.odf`,mimeType:`application/vnd.oasis.opendocument.formula`},{ext:`.odc`,mimeType:`application/vnd.oasis.opendocument.chart`},{ext:`.sxc`,mimeType:`application/vnd.sun.xml.calc`},{ext:`.pdf`,mimeType:`application/pdf`},{ext:`.fdf`,mimeType:`application/vnd.fdf`},{ext:``,mimeType:`application/x-ole-storage`},{ext:`.msi`,mimeType:`application/x-ms-installer`},{ext:`.aaf`,mimeType:`application/octet-stream`},{ext:`.msg`,mimeType:`application/vnd.ms-outlook`},{ext:`.xls`,mimeType:`application/vnd.ms-excel`},{ext:`.pub`,mimeType:`application/vnd.ms-publisher`},{ext:`.ppt`,mimeType:`application/vnd.ms-powerpoint`},{ext:`.doc`,mimeType:`application/msword`},{ext:`.ps`,mimeType:`application/postscript`},{ext:`.psd`,mimeType:`image/vnd.adobe.photoshop`},{ext:`.p7s`,mimeType:`application/pkcs7-signature`},{ext:`.ogg`,mimeType:`application/ogg`},{ext:`.oga`,mimeType:`audio/ogg`},{ext:`.ogv`,mimeType:`video/ogg`},{ext:`.png`,mimeType:`image/png`},{ext:`.png`,mimeType:`image/vnd.mozilla.apng`},{ext:`.jpg`,mimeType:`image/jpeg`},{ext:`.jxl`,mimeType:`image/jxl`},{ext:`.jp2`,mimeType:`image/jp2`},{ext:`.jpf`,mimeType:`image/jpx`},{ext:`.jpm`,mimeType:`image/jpm`},{ext:`.jxs`,mimeType:`image/jxs`},{ext:`.gif`,mimeType:`image/gif`},{ext:`.webp`,mimeType:`image/webp`},{ext:`.exe`,mimeType:`application/vnd.microsoft.portable-executable`},{ext:``,mimeType:`application/x-elf`},{ext:``,mimeType:`application/x-object`},{ext:``,mimeType:`application/x-executable`},{ext:`.so`,mimeType:`application/x-sharedlib`},{ext:``,mimeType:`application/x-coredump`},{ext:`.a`,mimeType:`application/x-archive`},{ext:`.deb`,mimeType:`application/vnd.debian.binary-package`},{ext:`.tar`,mimeType:`application/x-tar`},{ext:`.xar`,mimeType:`application/x-xar`},{ext:`.bz2`,mimeType:`application/x-bzip2`},{ext:`.fits`,mimeType:`application/fits`},{ext:`.tiff`,mimeType:`image/tiff`},{ext:`.bmp`,mimeType:`image/bmp`},{ext:`.ico`,mimeType:`image/x-icon`},{ext:`.mp3`,mimeType:`audio/mpeg`},{ext:`.flac`,mimeType:`audio/flac`},{ext:`.midi`,mimeType:`audio/midi`},{ext:`.ape`,mimeType:`audio/ape`},{ext:`.mpc`,mimeType:`audio/musepack`},{ext:`.amr`,mimeType:`audio/amr`},{ext:`.wav`,mimeType:`audio/wav`},{ext:`.aiff`,mimeType:`audio/aiff`},{ext:`.au`,mimeType:`audio/basic`},{ext:`.mpeg`,mimeType:`video/mpeg`},{ext:`.mov`,mimeType:`video/quicktime`},{ext:`.mp4`,mimeType:`video/mp4`},{ext:`.avif`,mimeType:`image/avif`},{ext:`.3gp`,mimeType:`video/3gpp`},{ext:`.3g2`,mimeType:`video/3gpp2`},{ext:`.mp4`,mimeType:`audio/mp4`},{ext:`.mqv`,mimeType:`video/quicktime`},{ext:`.m4a`,mimeType:`audio/x-m4a`},{ext:`.m4v`,mimeType:`video/x-m4v`},{ext:`.heic`,mimeType:`image/heic`},{ext:`.heic`,mimeType:`image/heic-sequence`},{ext:`.heif`,mimeType:`image/heif`},{ext:`.heif`,mimeType:`image/heif-sequence`},{ext:`.mj2`,mimeType:`video/mj2`},{ext:`.dvb`,mimeType:`video/vnd.dvb.file`},{ext:`.webm`,mimeType:`video/webm`},{ext:`.avi`,mimeType:`video/x-msvideo`},{ext:`.flv`,mimeType:`video/x-flv`},{ext:`.mkv`,mimeType:`video/x-matroska`},{ext:`.asf`,mimeType:`video/x-ms-asf`},{ext:`.asf`,mimeType:`video/x-ms-wmv`},{ext:`.asf`,mimeType:`video/asf`},{ext:`.aac`,mimeType:`audio/aac`},{ext:`.voc`,mimeType:`audio/x-unknown`},{ext:`.m3u`,mimeType:`application/vnd.apple.mpegurl`},{ext:`.rmvb`,mimeType:`application/Line truncated
|
||||
`)}o&&(n.classList.add(`error`),r.__errListener=function(){ae(r,n),app.utils.deleteByPath(app.store.errors,i)},r.addEventListener(`input`,r.__errListener),r.addEventListener(`change`,r.__errListener),r.setAttribute(`data-error`,!0),r.setCustomValidity&&r.reportValidity&&r.classList.contains(`inline-error`)?(r.setCustomValidity(o),r.reportValidity(),r._oldTitle=r.title,r.title=o):n.after(t.div({"html-data-input-name":i,className:`field-help error generated-error`,textContent:o})))}}),v.onmessage=e=>{e.data?.collections&&JSON.stringify(app.store.collections)!=JSON.stringify(e.data.collections)&&(app.store.collections=e.data.collections),e.data?.settings&&JSON.stringify(app.store.settings)!=JSON.stringify(e.data.settings)&&(app.store.settings=e.data.settings),e.data?.colorScheme&&(app.store.userColorScheme=e.data.colorScheme)},watch(()=>JSON.stringify(app.store.collections),(e,n)=>{e&&e!=`[]`&&n&&n!=`[]`&&e!=n&&v?.postMessage({collections:JSON.parse(e)})}),watch(()=>JSON.stringify(app.store.settings),(e,n)=>{e&&e!=`{}`&&n&&n!=`{}`&&e!=n&&v?.postMessage({settings:JSON.parse(e)}),window.localStorage.setItem(ee,e)});var b=`#/login`,x=window.location.pathname.endsWith(`/`)?window.location.pathname.substring(0,window.location.pathname.length-1):window.location.pathname;window.app=window.app||{},window.app.pb=new i(`../`,new r(`__pb_superusers__`+x)),app.pb.beforeSend=function(e,n){return n.headers[`x-request-source`]=`pbui`,{url:e,options:n}},app.store.superuser=app.pb.authStore.record,app.pb.authStore.onChange((e,n)=>{!n&&window.location.hash!=b&&(app.modals.close(),window.location.hash=b),app.store.superuser=n}),app.pb.authStore.isValid&&app.pb.collection(app.pb.authStore.record?.collectionName||`_superusers`).authRefresh().catch(e=>{console.warn(`Failed to refresh the existing auth token:`,e);let n=e?.status<<0;(n==401||n==403)&&(app.utils.rememberPath(),app.pb.cancelAllRequests(),app.pb.authStore.clear())}),app.pb.authStore.onChange((e,n)=>{n?.id&&(app.store.loadCollections(),app.store.loadSettings(),app.store.loadOAuth2Providers())});var oe=app.pb.collection;app.pb.collection=function(e){let n=oe.call(this,e);return se(n),n};function se(e){if(e.__customUIEvents)return;e.__customUIEvents=!0;let n=e.create;e.create=function(){return n.apply(e,arguments).then(e=>(setTimeout(()=>{document.dispatchEvent(new CustomEvent(`record:create`,{detail:e})),document.dispatchEvent(new CustomEvent(`record:save`,{detail:e}))},0),e))};let r=e.update;e.update=function(){return r.apply(e,arguments).then(e=>(setTimeout(()=>{document.dispatchEvent(new CustomEvent(`record:update`,{detail:e})),document.dispatchEvent(new CustomEvent(`record:save`,{detail:e}))},0),e))};let i=e.delete;e.delete=function(){return i.apply(e,arguments).then(n=>{let r={id:arguments[0],collectionId:e.collectionIdOrName,collectionName:e.collectionIdOrName};return setTimeout(()=>{document.dispatchEvent(new CustomEvent(`record:delete`,{detail:r}))},0),n})}}var ce=`pbLastFileToken`,S=!1,le=[];app.pb.authStore.onChange((e,n)=>{n?.id||window.localStorage.removeItem(ce)}),window.app.getFileToken=async function(n=``){let r=n&&app.store.collections?.find(e=>e.id==n||e.name==n);if(r&&!r.fields?.find(e=>e.type==`file`&&e.protected))return;let i=window.localStorage.getItem(ce);return(!i||e(i,60))&&(i=await C()),i};async function C(){return new Promise(async(e,n)=>{if(le.push({resolve:e,reject:n}),!S){S=!0;try{let e=await app.pb.files.getToken();window.localStorage.setItem(ce,e),le.forEach(n=>n.resolve(e))}catch(e){le.forEach(n=>n.reject(e))}S=!1,le=[]}})}window.app.checkApiError=function(e,n=!0){if(!e||!(e instanceof Error)||e.isAbort){console.warn(`checkApiError - unexpected error type:`,e);return}let r=e?.status<<0,i=e?.response||{},a=n&&(i.message||e.message||`Something went wrong!`);if(a&&app.toasts.error(a),r==0&&console.log(e),app.utils.isEmpty(i.data)||(app.store.errors=i.data),r===401&&window.location.hash!=b)return app.utils.rememberPath(),app.pb.cancelAllRequests(),app.pb.authStore.clear()};function ue(){return()=>{if(!(!app.store._ready||!app.store.showHeader||!app.store.superuser?.id))return t.header({pbEvent:`appHeader`,rid:`appHeader`,className:`app-header accent-surface`,onmount:async e=>{await new Promise(e=>setTimeout(e,0)),e._scrollToActiveMenuItem=function(){e?.querySelector(`.app-main-nav .header-link.active`)?.scrollIntoView()},e._scrollToActiveMenuItem(),window.addEventListener(`hashchange`,e._scrollToActiveMenuItem)},onunmount:e=>{window.removeEventListener(`hashchange`,e?._scrollToActiveMenuItem)}},t.a({href:`#/`,className:`logo`},t.img({src:()=>app.store.headerLogo,alt:`App logo`})),t.nav({pbEvent:`mainNav`,className:`app-main-nav`},()=>app.store.headerLinks.map(e=>{let n=e.href.startsWith(`#/`);return t.a({href:()=>e.href,target:()=>n?void 0:`_blank`,rel:()=>n?void 0:`noopener noreferrer`,className:n=>`header-link ${e.isActive?.(n)||app.utils.isActivePath(e.href)?`active`:``}`},()=>{if(e.icon)return t.i({className:e.icon,ariaHidden:!0})},t.span(
Line truncated
|
||||
`+Fe:null),"html-class":`formatted-date`,className:()=>`formatted-date ${n.short?`short`:`full`}`,onunmount:()=>{r.forEach(e=>e?.unwatch())}},()=>{if(!n.value)return t.span({className:`missing-value`});if(n.short){let e=n.value.split(` `);return[t.span({className:`primary-date`},e[0]),t.span({className:`secondary-date`},e[1])]}return[t.span({className:`primary-date`},app.utils.toLocalDatetime(n.value)),t.span({className:`secondary-date`},n.value)]})},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.refreshButton=function(e={}){let n=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,tooltip:`Refresh`,className:`btn transparent secondary circle rotate-btn`,disabled:!1,onclick:function(e){}}),r=app.utils.extendStore(n,e),i,a=t.button({rid:n.rid,id:()=>n.id,hidden:()=>n.hidden,inert:()=>n.inert,type:`button`,ariaLabel:app.attrs.tooltip(()=>n.tooltip),disabled:()=>n.disabled,className:()=>n.className,onunmount:()=>{clearTimeout(i),r.forEach(e=>e?.unwatch())},onclick:e=>{e.preventDefault(),n.onclick&&n.onclick(e),a.classList.add(`rotate`),a.addEventListener(`animationend`,()=>{a.classList.remove(`rotate`)}),clearTimeout(i),i=setTimeout(()=>{clearTimeout(i),a.classList.remove(`rotate`)},500)}},t.i({className:`ri-refresh-line`,ariaHidden:!0}));return a},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.searchHistoryButton=function(e={}){let n=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,value:void 0,historyKey:`default`,max:15,openInNewTabParam:`filter`,btnClassName:`btn sm pill secondary transparent p-r-5`,onselect:function(e){}}),r=app.utils.extendStore(n,e),i=store({items:app.utils.getLocalHistory(n.historyKey,[])});function a(e){o(e),i.items.unshift(e)}function o(e){app.utils.removeByValue(i.items,e)}let s=`history_dropdown_`+app.utils.randomString();r.push(watch(()=>n.value,e=>{e&&a(e)})),r.push(watch(()=>{i.items.length>n.max&&(i.items=i.items.slice(0,n.max)),app.utils.saveLocalHistory(n.historyKey,i.items)}));let c=t.div({id:s,className:`dropdown sm left nowrap history-searchbar-dropdown`,popover:`hint`,onclick:e=>(e.stopPropagation(),!1)},t.div({className:`block p-5`},t.small({className:`txt-hint`},`Search history`)),()=>i.items?.length?i.items.slice(0,n.max).map(e=>t.button({type:`button`,className:`dropdown-item txt-code`,onclick:()=>{c.hidePopover(),n.onselect?.(e),a(e)},onauxclick:()=>{if(n.openInNewTabParam){a(e),c.hidePopover();let r=app.utils.replaceHashQueryParams({[n.openInNewTabParam]:e},!1);window.open(r,`_blank`)}}},t.span({className:`txt-ellipsis`,title:e,textContent:e}),t.small({role:`button`,className:`remove-btn link-hint m-l-auto p-l-5 p-r-5`,title:`Clear`,onauxclick:e=>(e.stopPropagation(),!1),onclick:n=>(n.stopPropagation(),o(e),!1)},t.i({className:`ri-close-line`,ariaHidden:!0})))):t.div({rid:`no-history`,className:`block p-5`},t.span(null,`Your recent searches will show up here.`)));return t.button({rid:n.rid,id:()=>n.id,hidden:()=>n.hidden,inert:()=>n.inert,type:`button`,title:`Search history`,className:()=>n.btnClassName,"html-popovertarget":s,onunmount:()=>{r?.forEach(e=>e?.unwatch())}},t.i({className:`ri-search-line`,ariaHidden:!0}),t.i({className:`ri-arrow-drop-down-line`,ariaHidden:!0}),c)},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.s3Test=function(e={}){let n=`s3_test_request`,r=store({rid:void 0,config:null,label:`Use S3 storage`,testFilesystem:`storage`}),i=app.utils.extendStore(r,e),a=store({isTesting:!1,testError:null,get hasError(){return!app.utils.isEmpty(a.testError)}}),o,s;function c(e=150){if(!r.config.enabled){clearTimeout(o);return}a.isTesting=!0,clearTimeout(o),o=setTimeout(()=>{l()},e)}async function l(){if(a.isTesting=!0,!r.config.enabled||!r.testFilesystem){a.testError=null,a.isTesting=!1;return}app.pb.cancelRequest(n),clearTimeout(s),s=setTimeout(()=>{app.pb.cancelRequest(n),a.testError=Error(`S3 test connection timeout.`),a.isTesting=!1},3e4);try{await app.pb.props.testS3(r.testFilesystem,{requestKey:n}),a.testError=null,a.isTesting=!1}catch(e){e?.isAbort||(a.testError=e,a.isTesting=!1,clearTimeout(s))}}return i.push(watch(()=>r.testFilesystem&&r.config,()=>c())),t.div({pbEvent:`s3Test`,rid:r.rid,hidden:()=>!r.testFilesystem,className:()=>`label s3-test-label txt-nowrap ${a.hasError?`warning`:`success`}`,ariaDescription:app.attrs.tooltip(()=>a.testError?.data?.message),onunmount:()=>{clearTimeout(s),clearTimeout(o),i.forEach(e=>e?.unwatch())}},()=>a.isTesting?t.span({className:`loader sm`}):a.hasError?[t.i({className:`ri-error-warning-line txt-warning`,ariaHidden:!0}),t.span({className:`txt`},`Failed to establish S3 connection`)]:[t.i({className:`ri-checkbox-circle-line txt-success`,ariaHidden:!0}),t.span({className:`txt`},`S3 connected successfully`)])},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.s3ConfigFields=function(e={}){let n=store({rid:void 0,id
Line truncated
|
||||
<video controls width="300">
|
||||
<source src="${a}" />
|
||||
<p>Download: <a href="${a}" download="${i}">${i}</a>.</p>
|
||||
</video>
|
||||
`)}})}},{type:`menuitem`,text:`Manual embed`,onAction:()=>{tinymce.activeEditor.execCommand(`mceMedia`)}}])}})}var Ke=`DIV.P.A.EM.B.STRONG.H1.H2.H3.H4.H5.H6.TABLE.TR.TD.TH.TBODY.THEAD.TFOOT.BR.HR.Q.SUP.SUB.DEL.IMG.OL.UL.LI.CODE`.split(`.`);function qe(e){if(e){for(let n of e.children)qe(n);Ke.includes(e.tagName)?(e.removeAttribute(`style`),e.removeAttribute(`class`)):Je(e)}}function Je(e){let n=e.parentNode;for(;e.firstChild;)n.insertBefore(e.firstChild,e);n.removeChild(e)}async function Ye(){if(window.tinymce!==void 0)return;let e=`lazy-tinymce-js`;return document.getElementById(e)?new Promise((e,n)=>{function r(){document.removeEventListener(`tinymceLoadSuccess`,i),document.removeEventListener(`tinymceLoadError`,a)}let i=function(){r(),e()},a=function(e){r(),n(e?.details)};document.addEventListener(`tinymceLoadSuccess`,i),document.addEventListener(`tinymceLoadError`,a)}):new Promise((n,r)=>{document.head.querySelector(`#shablon-script`).after(t.script({id:e,src:`./libs/tinymce/tinymce.min.js`,onload:()=>{n()},onerror:e=>{console.warn(`failed to load tinymce.min.js:`,e),r(e)}}))}).then(()=>{document.dispatchEvent(new CustomEvent(`tinymceLoadSuccess`))}).catch(e=>{document.dispatchEvent(new CustomEvent(`tinymceLoadError`,{detail:e}))})}window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.colorPicker=function(e={}){let n=`picker_`+app.utils.randomString(),r=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,className:``,name:``,required:!1,disabled:!1,value:``,predefinedColors:[],onchange:e=>{},onmount:e=>{},onunmount:e=>{}}),i=app.utils.extendStore(r,e),a=store({inputValue:`#ffffff`});function o(e){e=e?.toLowerCase()||``,r.onchange?.(e)}let s,c=t.input({type:`color`,className:`color-picker-input`,id:()=>r.id,name:()=>r.name,required:()=>r.required,disabled:()=>r.disabled,value:()=>(a.inputValue=r.value||`#ffffff`,r.value||void 0),oninput:e=>{a.inputValue=e.target.value,clearTimeout(s),s=setTimeout(()=>{o(e.target.value)},50)}});return t.div({rid:r.rid,hidden:()=>r.hidden,inert:()=>r.inert,className:()=>`color-picker ${r.className}`,onmount:e=>{r.onmount?.(e)},onunmount:e=>{clearTimeout(s),r.onunmount?.(e),i.forEach(e=>e?.unwatch())}},t.div({className:`color-picker-input-wrapper`},c,t.output({className:`result`,style:()=>`color: lch(from ${a.inputValue||`#ffffff`} calc((49 - l) * infinity) 0 0);`,textContent:()=>a.inputValue})),t.button({hidden:()=>!r.predefinedColors.length,type:`button`,title:`Predefined colors`,className:`link-hint predefined-colors-btn`,"html-popovertarget":n+`predefined-colors-dropdown`},t.i({className:`ri-arrow-down-s-line`,ariaHidden:!0})),t.div({pbEvent:`predefinedColorsDropdown`,id:n+`predefined-colors-dropdown`,className:`dropdown predefined-colors-dropdown`,popover:`auto`},t.div({className:`predefined-colors-list`},()=>r.predefinedColors?.map(e=>t.button({type:`button`,className:()=>`color ${r.value==e?`active`:``}`,style:`background:${e}`,onclick:n=>{c&&(n.target.closest(`.dropdown`)?.hidePopover(),c.value=e||void 0,c.dispatchEvent(new Event(`input`,{bubbles:!0})))}})))))},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.ruleField=function(e={}){let n=`rule_`+app.utils.randomString(),r=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,className:``,required:!1,disabled:!1,name:void 0,label:void 0,help:void 0,value:null,nullable:!0,placeholder:`Leave empty to grant everyone access...`,autocomplete:e=>[],oninput:e=>{},onmount:e=>{},onunmount:e=>{},get isLocked(){return r.value==null}}),i=app.utils.extendStore(r,e,`isLocked`),a,o=``;function s(e){r.value=e,r.oninput?.(e),a?.dispatchEvent(new CustomEvent(`change`,{detail:e}))}function c(){r.value!==null&&(o=r.value,s(null))}function l(){s(o??``),setTimeout(()=>{document.getElementById(n)?.focus()},0)}return a=t.div({rid:r.rid,id:()=>r.id,hidden:()=>r.hidden,inert:()=>r.inert,"html-name":()=>r.name,className:()=>[`field`,`rule-field`,r.required?`required`:null,r.value===null?`locked`:null,r.disabled?`disabled`:null].filter(Boolean).join(` `),onmount:e=>{r.onmount?.(e)},onunmount:e=>{r.onunmount?.(e),i.forEach(e=>e?.unwatch())}},t.label({htmlFor:n},e=>r.label?typeof r.label==`function`?r.label(e):typeof r.label==`string`?t.span({className:`txt`},r.label):r.label:t.span({className:`txt`},`Rule`),t.span({hidden:()=>!r.isLocked,className:`txt superusers-label`},`(Superusers only)`)),e=>r.isLocked?t.button({type:`button`,className:`unlock-overlay`,disabled:()=>r.disabled,onclick:l},t.span({className:`txt`},`Unlock and set custom rule`),t.i({className:`ri-lock-unlock-line`,ariaHidden:!0})):[app.components.codeEditor({id:n,language:`pbrule`,required:()=>r.required,disabled:()=>r.disabled,value:()=>r.value,oninput:s,placeholder:()=>r.placeholder,autocomplete:r.autocomplete,autocompleteContainer:e}),t.button({hidden:()=>!r.nullable,type:`button`,className:`superuser-toggle`,disabled:()=>r.disabled,onclick:c},t.
Line truncated
|
||||
<defs>
|
||||
<marker id="${s}_arrow1" class="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto" fill="context-stroke">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
</defs>
|
||||
<g class="paths-group" marker-end="url(#${s}_arrow1)"></g>
|
||||
`;let h=m.querySelector(`.paths-group`),g=document.createElementNS(`http://www.w3.org/2000/svg`,`svg`);g.classList.add(`erd-paths`,`front`),g.innerHTML=`
|
||||
<defs>
|
||||
<marker id="${s}_arrow2" class="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto" fill="context-stroke">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
</defs>
|
||||
<g class="paths-group" marker-end="url(#${s}_arrow2)"></g>
|
||||
`;let _=g.querySelector(`.paths-group`);return a=t.div({tabIndex:-1,rid:n.rid,id:()=>n.id,hidden:()=>n.hidden,inert:()=>n.inert,className:()=>`erd ${n.className} ${i.isUpdating?`updating`:``} ${i.isPanning?`panning`:``} ${i.activeCollection?`active`:``}`,onkeydown:e=>{(e.ctrlKey||e.metaKey)&&e.key==`0`&&f()},onmount:async e=>{e.addEventListener(`wheel`,r=>{r.preventDefault();let a=e.getBoundingClientRect(),o=r.clientX-a.left,s=r.clientY-a.top,c=(o-i.viewX)/n.scale,l=(s-i.viewY)/n.scale,u=Math.min(Math.max(-r.deltaY*.001+n.scale,Qe),Ze);i.viewX=o-c*u,i.viewY=s-l*u,n.scale=u}),e.addEventListener(`pointerdown`,e=>{e.buttons==1&&(i.isPanning=!0,i.panStartX=e.clientX-i.viewX,i.panStartY=e.clientY-i.viewY)}),e._ondragging=function(e){i.isPanning&&(i.viewX=e.clientX-i.panStartX,i.viewY=e.clientY-i.panStartY)},e._ondragstop=function(){i.isPanning=!1},window.addEventListener(`pointermove`,e._ondragging),window.addEventListener(`pointerup`,e._ondragstop),c()},onunmount:e=>{window.removeEventListener(`pointermove`,e._ondragging),window.removeEventListener(`pointerup`,e._ondragstop),r.forEach(e=>e?.unwatch())}},t.div({className:`erd-area`,style:()=>`transform: translate(${i.viewX}px, ${i.viewY}px) scale(${n.scale});`},m,t.div({className:`erd-tables`},()=>n.collections.map(e=>t.div({style:()=>`left:${i.positions[e.id]?.x||0}px;top:${i.positions[e.id]?.y||0}px`,className:()=>`erd-table type-${e.type} ${p(e)?`active`:``} ${e.system?`system`:``}`,"html-data-collection-id":()=>e.id,"html-data-collection-name":()=>e.name,onmouseenter:()=>{h.querySelectorAll(`[data-to="${e.id}"]`)?.forEach(e=>{e.classList.add(`active-to`),_.append(e)}),h.querySelectorAll(`[data-from="${e.id}"]`)?.forEach(e=>{e.classList.add(`active-from`),_.append(e)}),i.activeCollection=e},onmouseleave:()=>{for(let e of _.children)e.classList.remove(`active-from`,`active-to`);h.append(..._.children),i.activeCollection=null}},t.div({className:`erd-table-row header`},()=>e.name),()=>e.fields?.map(e=>t.div({className:`erd-table-row type-${e.type} ${e.primaryKey?`primary-key`:``}`,"html-data-field-id":()=>e.id,"html-data-field-name":()=>e.name},t.i({ariaHidden:!0,title:()=>e.type,className:()=>`field-icon ${app.fieldTypes[e.type].icon||app.utils.fallbackFieldIcon}`}),t.span({className:`field-name`},()=>e.name),()=>{if(e.hidden)return t.span({className:`label danger field-hidden-label`},`Hidden`)},()=>{if(e.maxSelect!==void 0)return t.span({className:`meta`},e.maxSelect>1?`multiple`:`single`)}))))),g),t.nav({className:`erd-nav`,onmousedown:e=>{e.stopImmediatePropagation()}},t.button({type:`button`,className:`btn sm circle secondary`,title:`Zoom in`,onclick:()=>{n.scale+=.05}},t.i({className:`ri-add-line`,ariaHidden:!0})),t.button({type:`button`,className:`btn sm circle secondary`,title:`Zoom out`,onclick:()=>{n.scale-=.05}},t.i({className:`ri-subtract-line`,ariaHidden:!0})))),a};function et(e,n,r=1,i=``,a=``,o=2){if(!e||!n)return;let s=e.closest(`.erd-area`).getBoundingClientRect(),c=e.getBoundingClientRect(),l=n.getBoundingClientRect();o*=r;let u=c.top-s.top+c.height/2,d=l.top-s.top+l.height/2,f,p;c.left<l.left?(f=c.left-s.left+c.width+o,p=l.left-s.left-o):c.left>l.left?(f=c.left-s.left-o,p=l.left-s.left+l.width+o):(f=c.left-s.left-o,p=l.left-s.left-o),f/=r,p/=r,u/=r,d/=r;let m=f+(p-f)/2;f==p&&(m-=20);let h=`M ${f} ${u}
|
||||
L ${m} ${u}
|
||||
L ${m} ${d}
|
||||
L ${p} ${d}`,g=document.createElementNS(`http://www.w3.org/2000/svg`,`path`);return g.setAttribute(`class`,`relation-path`),g.setAttribute(`data-from`,i||``),g.setAttribute(`data-to`,a||``),g.setAttribute(`d`,h),g}window.app=window.app||{},window.app.components=window.app.components||{};var tt=1e3;window.app.components.pageSidebar=function(e={},...n){let r,i=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,className:``,widthHistoryKey:`pbPageSidebarWidth`,onmount:void 0,onunmount:void 0}),a=app.utils.extendStore(i,e),o=store({responsiveShow:!1}),s;function c(){if(r){if(window.innerWidth>tt){o.responsiveShow=!1,r.dataset.responsive=!1,s?.remove(),s=null;return}r.dataset.responsive=!0,s||(s=t.button({type:`button`,className:`btn transparent secondary responsive-sidebar-btn`,title:`Toggle sidebar`,onclick:e=>{e.stopPropagation(),o.responsiveShow=!o.responsiveShow}},t.i({className:`ri-menu-2-line`,ariaHidden:!0})),document.body.querySelector(`.page-header .breadcrumbs`).before(s))}}function l(e){if(e.target.closest(`.responsive-close`)){o.responsiveShow=!1;return}if(!(e.target.closest(`.page-sidebar`)||e.target.closest(`.app-header`)||e.target.closest(`.modal`)))return e.preventDefault(),e.stopImmediatePropagation(),o.responsiveShow=!1,!1}return a.push(watch(()=>o.responsiveShow,e=>{e?window.addEventListener(`click`,l,!0):window.removeEventListener(`click`,l,!0)})),r=t.aside({pbEvent:`pageSidebar`,rid:i.rid,id:()=>i.id,hidden:()=>i.hidden,inert:()=>i.inert,className:()=>`page-sidebar ${i.className} ${o.responsiveShow?`active`:``}`,onmount:e=>{c(e),window.addEventListener(`resize`,c),i.onmount?.(e)},onunmount:e=>{i.onunmount?.(e),window.removeEventListener(`click`,l,!0),window.removeEventListener(`resize`,c),s?.remove(),a.forEach(e=>e?.unwatch())}},e=>{let n;return i.widthHistoryKey&&(n=localStorage.getItem(i.widthHistoryKey),n&&(e.style.width=n)),app.components.dragline({ondragstart:n=>{e._startWidth=e.offsetWidth},ondragging:(r,a,o)=>{n=e._startWidth+a+`px`,e.style.width=n,i.widthHistoryKey&&localStorage.setItem(i.widthHistoryKey,n)}})},...n),r};var P=`modulepreload`,nt=function(e,n){return new URL(e,n).href},rt={},F=function(e,n,r){let i=Promise.resolve();if(n&&n.length>0){let e=document.getElementsByTagName(`link`),a=document.querySelector(`meta[property=csp-nonce]`),o=a?.nonce||a?.getAttribute(`nonce`);function s(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}i=s(n.map(n=>{if(n=nt(n,r),n in rt)return;rt[n]=!0;let i=n.endsWith(`.css`),a=i?`[rel="stylesheet"]`:``;if(r)for(let r=e.length-1;r>=0;r--){let a=e[r];if(a.href===n&&(!i||a.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${n}"]${a}`))return;let s=document.createElement(`link`);if(s.rel=i?`stylesheet`:P,i||(s.as=`script`),s.crossOrigin=``,s.href=n,o&&s.setAttribute(`nonce`,o),document.head.appendChild(s),i)return new Promise((e,r)=>{s.addEventListener(`load`,e),s.addEventListener(`error`,()=>r(Error(`Unable to preload CSS for ${n}`)))})}))}function a(e){let n=new Event(`vite:preloadError`,{cancelable:!0});if(n.payload=e,window.dispatchEvent(n),!n.defaultPrevented)throw e}return i.then(n=>{for(let e of n||[])e.status===`rejected`&&a(e.reason);return e().catch(a)})};window.app=window.app||{},window.app.modals=window.app.modals||{},window.app.modals.openApiPreview=function(e,n={onbeforeopen:null,onafteropen:null,onbeforeclose:null,onafterclose:null}){let r=it(e,n);r&&(document.body.appendChild(r),app.modals.open(r))};function it(e,n){if(!e){console.warn(`[apiPreviewModal] missing required collection`);return}let r,i=store({activeTab:`List/Search`,tabEl:null,isLoading:!1}),a={"List/Search":async e=>{let{docsList:n}=await F(async()=>{let{docsList:e}=await import(`./docsList-BAfVNUIM.js`);return{docsList:e}},__vite__mapDeps([0,1,2,3]),import.meta.url);return i.tabEl=n(e)},View:async e=>{let{docsView:n}=await F(async()=>{let{docsView:e}=await import(`./docsView-Be6Uhvcf.js`);return{docsView:e}},__vite__mapDeps([4,1,2]),import.meta.url);return i.tabEl=n(e)}};e.type!=`view`&&(a.Create=async e=>{let{docsCreate:n}=await F(async()=>{let{docsCreate:e}=await import(`./docsCreate-Be3S3y5K.js`);return{docsCreate:e}},__vite__mapDeps([5,1,2]),import.meta.url);return i.tabEl=n(e)},a.Update=async e=>{let{docsUpdate:n}=await F(async()=>{let{docsUpdate:e}=await import(`./docsUpdate-LtD8DbWI.js`);return{docsUpdate:e}},__vite__mapDeps([6,5,1,2]),import.meta.url);return i.tabEl=n(e)},a.Delete=async e=>{let{docsDelete:n}=await F(async()=>{let{docsDelete:e}=await import(`./docsDelete-CybOn5jy.js`);return{docsDelete:e}},[],import.meta.url);return i.tabEl=n(e)},a.Realtime=async e=>{let{docsRealtime:n}=await F(async()=>{let{docsRealtime:e}=await import(`./docsRealtime-PMESvmJN.js`);return{docsRealtime:e}},[],import.meta.url);return i.tabEl=n(e)},a.Batch=async e=>{let{docsBatch:n}=await F(async()=>{let{docsBatch:e}=await import(`./docsBatch-DNJl1NTn.js`);return{docsBatc
Line truncated
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${i}');
|
||||
|
||||
// load the token into the store
|
||||
const token = '...';
|
||||
pb.authStore.save(token, null);
|
||||
`,footnote:t.div({className:`txt-right`},t.a({href:`https://github.com/pocketbase/js-sdk`,target:`_blank`,rel:`noopener noreferrer`,textContent:`JS SDK docs`}))},{title:`Dart SDK`,language:`dart`,value:`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${i}');
|
||||
|
||||
// load the token into the store
|
||||
final token = '...';
|
||||
pb.authStore.save(token, null);
|
||||
`,footnote:t.div({className:`txt-right`},t.a({href:`https://github.com/pocketbase/dart-sdk`,target:`_blank`,rel:`noopener noreferrer`,textContent:`Dart SDK docs`}))}]})),t.footer({className:`modal-footer`},t.button({type:`button`,className:`btn transparent m-r-auto`,disabled:()=>r.isLoading,onclick:()=>app.modals.close()},t.span({className:`txt`},`Close`)),t.button({hidden:()=>r.token,type:`submit`,"html-form":n+`_form`,className:()=>`btn expanded-lg ${r.isLoading?`loading`:``}`,disabled:()=>r.isLoading},t.span({className:`txt`},`Generate token`)),t.button({hidden:()=>!r.token,type:`button`,className:()=>`btn secondary expanded-lg ${r.isLoading?`loading`:``}`,onclick:()=>o()},t.span({className:`txt`},`Generate new one`))))}window.app=window.app||{},window.app.modals=window.app.modals||{},window.app.modals.openRecordUpsert=function(e,n=null,r={onbeforeopen:null,onafteropen:null,onbeforeclose:null,onafterclose:null,onsave:null,ondelete:null,onduplicate:null,ontokensreset:null,onpasswordresetsend:null,onverificationsend:null}){app.store.errors=null;let i=Ot(e,n,r);i&&(document.body.appendChild(i),app.modals.open(i))};var Et=`main`,Dt=`authProviders`;function Ot(e,n,r){if(!e?.id){console.warn(`[recordUpsertModal] missing required collection`);return}let i,a=`record_upsert_`+app.utils.randomString(),o=app.utils.getLocalHistory(app.consts.COLUMNS_STORAGE_PREFIX+e.id,{}),s=store({isLoading:!0,isSaving:!1,isLocked:!1,originalRecord:{},record:{},initialDraft:null,activeTab:Et,get isNew(){return app.utils.isEmpty(s.originalRecord?.id)},get isAuthCollection(){return e.type==`auth`},get isSuperusersCollection(){return e.name==`_superusers`},get showTabs(){return!s.isNew&&s.isAuthCollection&&!s.isSuperusersCollection},get excludedFields(){let e=[`id`];return s.isAuthCollection&&e.push(`email`,`emailVisibility`,`verified`,`password`,`tokenKey`),e},get initialDraftHash(){return l(s.initialDraft)},get recordHash(){return l(s.record)},get originalRecordHash(){return l(s.originalRecord)},get hasChanges(){return s.originalRecordHash!=s.recordHash||s.isAuthCollection&&(!!s.record.password||!!s.record.passwordConfirm)},get isFormDisabled(){return s.isLoading||s.isSaving||!s.isNew&&!s.hasChanges}});function c(e,n){switch(e){case`expand`:return;case`password`:case`passwordConfirm`:case`tokenKey`:return s.isAuthCollection?void 0:n}return n}function l(e){return e?JSON.stringify(e,c):``}function u(){return`draft_`+e.id+`_`+(s.originalRecord?.id||``)}function d(){return window.localStorage.getItem(u())||``}function f(){try{let e=d();if(e)return JSON.parse(e)}catch(e){console.warn(`getDraft failure:`,e),m()}return null}function p(e){try{window.localStorage.setItem(u(),e)}catch(e){console.warn(`saveDraft failure:`,e),m()}}function m(){window.localStorage.removeItem(u()),s.initialDraft=null}function h(){if(!s.initialDraft)return;app.store.errors&&(app.store.errors=null);let e=JSON.parse(JSON.stringify(s.initialDraft));m(),s.record=e}let g;function _(){s.initialDraft=f(),g?.unwatch(),g=watch(()=>s.recordHash,(e,n)=>{n!==void 0&&(s.hasChanges?p(e):m())})}async function v(n){if(s.isLoading=!0,g?.unwatch(),n=app.utils.isObject(n)?n:{id:n||``},!n.id){s.originalRecord=JSON.parse(JSON.stringify(n)),s.record=JSON.parse(JSON.stringify(n)),s.isLoading=!1,s.isLocked=!1,_();return}try{s.isLocked=!!app.store.settings?.meta?.hideControls,s.originalRecord=JSON.parse(JSON.stringify(n)),s.record=JSON.parse(JSON.stringify(n));let r=await app.pb.collection(e.name).getOne(n.id,{requestKey:`upsert_load_`+n.id});n.expand&&(r.expand=JSON.parse(JSON.stringify(n.expand))),Object.assign(s.originalRecord,JSON.parse(JSON.stringify(r))),Object.assign(s.record,JSON.parse(JSON.stringify(r))),s.isLoading=!1,setTimeout(()=>{_()},0)}catch(e){e?.isAbort||(app.checkApiError(e),s.isLoading=!1,setTimeout(()=>app.modals.close(i,!0),0))}}function ee(e){for(let n in e)(n.startsWith(`@@`)||s.isAuthCollection&&(n==`password`||n==`passwordConfirm`))&&delete e[n]}async function te(){let n={};for(let e in s.record){if(e==`expand`||e.startsWith(`@@`))continue;let r=s.record[e]?.__raw||s.record[e];r===void 0&&(r=null),n[e]=r}for(let r of e.fields){let i=app.fieldTypes[r.type]?.onrecordsave;i&&await i({collection:e,originalRecord:s.originalRecord,record:s.record,field:r,payload:n})}return n}async function y(n=!0){if(!(s.isLocked||s.isSaving||!s.isNew&&!s.hasChanges)){s.isSaving=!0;try{let a=await te(),o=app.utils.isEmpty(s.originalRecord?.id),c;c=o?await app.pb.collection(e.name).create(a):await app.pb.collection(e.name).update(s.originalRecord.id,a),m(),o?(s.originalRecord=structuredClone(c),s.record=structuredClone(c)):(Object.assign(s.originalRecord,structuredClone(c)),Object.assign(s.record,structuredClone(c)),ee(s.originalRecord),ee(s.record)),r.onsave?.(c,o),app.store.errors=null;let l;l=o?`Successfully created ${e.name} "${c.id}".`:`Successfully updated ${e.name} "${c.id}".`,app.toasts.success(l,{key:`recordSave`}),s.isSaving=!1,n&&app.modals.close(i,!0
Line truncated
|
||||
The field cannot be presentable if hidden.`),n})})))},()=>{if(i.showHidden)return t.div({className:`field prop-hidden`},t.input({type:`checkbox`,id:r+`.hidden`,className:`sm`,name:()=>`fields.${e.fieldIndex}.hidden`,checked:()=>!!e.field.hidden,onchange:n=>e.field.hidden=n.target.checked}),t.label({htmlFor:r+`.hidden`},t.span({className:`txt`},`Hidden`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Hide from the JSON API response and filters.`)})))},t.button({hidden:()=>!i.showDuplicate&&(!i.showRemove||e.field.system),type:`button`,title:`More options`,className:`btn sm circle transparent secondary more-btn m-l-auto`,"html-popovertarget":r+`_options_dropdown`},t.i({className:`ri-more-line`,ariaHidden:!0})),t.div({id:r+`_options_dropdown`,className:`dropdown sm field-options-dropdown`,popover:`auto`},()=>{if(i.showDuplicate)return t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,textContent:`Duplicate`,onclick:e=>{o(),e.target.closest(`.dropdown`).hidePopover()}})},()=>{if(!(!i.showRemove||e.field.system))return t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,textContent:`Remove`,onclick:e=>{s(),e.target.closest(`.dropdown`).hidePopover(),e.target.closest(`details`).open=!1}})})))};function Kt(e,n=`field`){let r=n,i=2,a=n.match(/\d+$/)?.[0]||``,o=a?n.substring(0,n.length-a.length):n;for(;e?.find(e=>e.name.toLowerCase()==r.toLowerCase());)r=o+((a<<0)+i),i++;return r}function qt(e,n,r){if(!r||n===void 0||n===r||!e?.passwordAuth?.identityFields?.length)return;let i=e.passwordAuth.identityFields;for(let e=0;e<i.length;e++)i[e]==n&&(i[e]=r)}function W(e,n,r){!r||n===void 0||n===r||!e?.indexes?.length||!e?.fields?.length||e.fields.find(e=>!e[`@toDelete`]&&e.name==n)||(e.indexes=e.indexes.map(e=>app.utils.replaceIndexColumn(e,n,r)))}function G(e){let n=`text_`+app.utils.randomString(),r=store({get hasAutogenerate(){return!app.utils.isEmpty(e.field.autogeneratePattern)&&app.utils.isEmpty(e.originalRecord?.id)},get isDisabled(){return!app.utils.isEmpty(e.originalRecord?.id)&&e.field.primaryKey},get isRequired(){return e.field.required&&!r.hasAutogenerate&&!r.isDisabled}});return t.div({className:`record-field-input field-type-text`},t.div({className:`fields`},t.div({className:`field`},t.label({htmlFor:n},t.i({ariaHidden:!0,className:()=>e.field.primaryKey?`ri-key-line`:app.fieldTypes.text.icon}),t.span({className:`txt`},()=>e.field.name)),t.div({className:`autoexpand-wrapper`},t.textarea({id:n,rows:1,name:()=>e.field.name,required:()=>r.isRequired,disabled:()=>r.isDisabled,placeholder:()=>r.hasAutogenerate?`Leave empty to autogenerate...`:``,value:()=>e.record[e.field.name]||``,oninput:n=>e.record[e.field.name]=n.target.value||``}),t.div({className:`input`},()=>e.record[e.field.name],` `))),()=>{if(!e.field.primaryKey||!e.originalRecord?.id)return;let n=e.collection?.fields?.filter(e=>e.type==`autodate`)||[];if(!n.length)return;let r=[];for(let i of n)r.push(`${i.name}: ${app.utils.displayValue(e.record[i.name])}`);return t.div({className:`field addon`},t.i({className:`ri-information-line txt-hint link-faded`,ariaDescription:app.attrs.tooltip(r.join(`
|
||||
`),`left`)}))}),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function K(e){let n=`f_`+app.utils.randomString();return app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.min`},t.span({className:`txt`},`Min length`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Clear the field or set it to 0 for no limit.`)})),t.input({type:`number`,id:n+`.min`,name:()=>`fields.${e.fieldIndex}.min`,step:1,min:0,max:2**53-1,placeholder:`No min limit`,value:()=>e.field.min||``,oninput:n=>{e.field.min=parseInt(n.target.value,10)}}))),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.max`},t.span({className:`txt`},`Max length`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Clear the field or set it to 0 to fallback to the default limit.`)})),t.input({type:`number`,id:n+`.max`,name:()=>`fields.${e.fieldIndex}.max`,step:1,min:()=>e.field.min||0,max:2**53-1,placeholder:`Default to max 5000 characters`,value:()=>e.field.max||``,oninput:n=>{e.field.max=parseInt(n.target.value,10)}}))),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.pattern`},t.span({className:`txt`},`Validation pattern`),()=>{if(e.field.primaryKey)return t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`All record ids have forbidden characters and unique case-insensitive (ASCII) validations in addition to the user defined regex pattern.`)})}),t.input({type:`text`,id:n+`.pattern`,name:()=>`fields.${e.fieldIndex}.pattern`,value:()=>e.field.pattern||``,oninput:n=>e.field.pattern=n.target.value})),t.div({className:`field-help`},`Ex. `,t.code(null,`^[a-z0-9]+$`))),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.autogeneratePattern`},t.span({className:`txt`},`Autogenerate pattern`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Set and autogenerate text matching the pattern on missing record create value.`)})),t.input({type:`text`,id:n+`.autogeneratePattern`,name:()=>`fields.${e.fieldIndex}.autogeneratePattern`,value:()=>e.field.autogeneratePattern||``,oninput:n=>e.field.autogeneratePattern=n.target.value})),t.div({className:`field-help`},`Ex. `,t.code(null,`[a-z0-9]{30}`))),t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`},`(!='')`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Requires the field value to be nonempty string`)})))]})}function Jt(e){return t.div({className:`record-field-view field-type-text`},()=>{let n=e.record[e.field.name]||``;if(n==``)return t.span({className:`missing-value`});if(e.field.primaryKey){let r=null;return e.record?.collectionName==`_superusers`&&app.store.superuser?.id==e.record?.id&&(r=t.strong({className:`txt`,textContent:` (you)`})),t.span({className:`label`},app.components.copyButton(n),t.span({className:`txt-ellipsis`},app.utils.truncate(n),r))}return e.short?t.span({className:`txt txt-ellipsis`,textContent:app.utils.truncate(n)}):n})}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.text={icon:`ri-text`,label:`Plain text`,settings:K,input:G,view:Jt,filterModifiers:e=>[`lower`],dummyData:(e,n=!1)=>e.primaryKey?app.utils.randomString(15):`example text`};function Yt(e){let n=`editor_`+app.utils.randomString(),r=store({lazyEditor:``}),i;return t.div({className:`record-field-input field-type-editor large-modal`,onmount:()=>{i=setTimeout(()=>{r.lazyEditor=app.components.tinymce({id:n,name:()=>e.field.name,required:()=>e.field.required,convertURLs:()=>e.field.convertURLs,value:()=>e.record[e.field.name]||``,onchange:n=>{e.record[e.field.name]=n}})},0)},onunmount:()=>{clearTimeout(i)}},t.div({className:`field`},t.label({htmlFor:n},t.i({className:app.fieldTypes.editor.icon,ariaHidden:!0}),t.span({className:`txt`},()=>e.field.name)),()=>r.lazyEditor),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function Xt(e){let n=`f_`+app.utils.randomString();return store({showInfo:!1}),app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.maxSize`},t.span(null,`Max size `),t.small(null,`(bytes)`)),t.input({type:`number`,id:n+`.maxSize`,name:()=>`fields.${e.fieldIndex}.maxSize`,min:0,step:1,max:2**53-1,placeholder:`DeLine truncated
|
||||
This field is disabled if "Only domains" is set.`)})),t.input({type:`text`,id:n+`.exceptDomains`,disabled:()=>!app.utils.isEmpty(e.field.onlyDomains),name:()=>`fields.${e.fieldIndex}.exceptDomains`,value:()=>app.utils.joinNonEmpty(e.field.exceptDomains),onchange:n=>e.field.exceptDomains=app.utils.splitNonEmpty(n.target.value,`,`)})),t.div({className:`field-help`},`Use comma as separator.`)),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.onlyDomains`},t.span({className:`txt`},`Only domains`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`List of domains that are ONLY allowed.
|
||||
This field is disabled if "Except domains" is set.`)})),t.input({type:`text`,id:n+`.onlyDomains`,disabled:()=>!app.utils.isEmpty(e.field.exceptDomains),name:()=>`fields.${e.fieldIndex}.onlyDomains`,value:()=>app.utils.joinNonEmpty(e.field.onlyDomains),onchange:n=>e.field.onlyDomains=app.utils.splitNonEmpty(n.target.value,`,`)})),t.div({className:`field-help`},`Use comma as separator.`)),t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`},`(!='')`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Requires the field value to be nonempty string`)})))]})}function on(e){return t.div({className:`record-field-view field-type-email`},()=>{let n=e.record[e.field.name]||``;return n?e.short?t.span({className:`txt txt-ellipsis`,textContent:app.utils.truncate(n)}):n:t.span({className:`missing-value`})})}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.email={icon:`ri-mail-line`,label:`Email`,settings:an,input:rn,view:on,filterModifiers:e=>[`lower`],dummyData:(e,n=!1)=>`test_${app.utils.randomString(3,`123567890`)}@example.com`};function sn(e){let n=`url_`+app.utils.randomString();return t.div({className:`record-field-input field-type-url`},t.div({className:`field`},t.label({htmlFor:n},t.i({className:app.fieldTypes.url.icon,ariaHidden:!0}),t.span({className:`txt`},()=>e.field.name)),t.input({type:`url`,id:n,spellcheck:!1,name:()=>e.field.name,required:()=>e.field.required,value:()=>e.record[e.field.name]||``,oninput:n=>e.record[e.field.name]=n.target.value})),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function cn(e){let n=`f_`+app.utils.randomString();return app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.exceptDomains`},t.span({className:`txt`},`Except domains`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`List of domains that are NOT allowed.
|
||||
This field is disabled if "Only domains" is set.`)})),t.input({type:`text`,id:n+`.exceptDomains`,disabled:()=>!app.utils.isEmpty(e.field.onlyDomains),name:()=>`fields.${e.fieldIndex}.exceptDomains`,value:()=>app.utils.joinNonEmpty(e.field.exceptDomains),onchange:n=>e.field.exceptDomains=app.utils.splitNonEmpty(n.target.value,`,`)})),t.div({className:`field-help`},`Use comma as separator.`)),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.onlyDomains`},t.span({className:`txt`},`Only domains`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`List of domains that are ONLY allowed.
|
||||
This field is disabled if "Except domains" is set.`)})),t.input({type:`text`,id:n+`.onlyDomains`,disabled:()=>!app.utils.isEmpty(e.field.exceptDomains),name:()=>`fields.${e.fieldIndex}.onlyDomains`,value:()=>app.utils.joinNonEmpty(e.field.onlyDomains),onchange:n=>e.field.onlyDomains=app.utils.splitNonEmpty(n.target.value,`,`)})),t.div({className:`field-help`},`Use comma as separator.`)),t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`},`(!='')`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Requires the field value to be nonempty string`)})))]})}function ln(e){return t.div({className:`record-field-view field-type-url`},()=>{let n=e.record[e.field.name]||``;return n?t.a({href:()=>n,className:`txt txt-ellipsis`,rel:`noopener noreferrer`,target:`_blank`,textContent:app.utils.truncate(n),ariaDescription:app.attrs.tooltip(`Open in new tab`),onclick:e=>{e.stopPropagation()}}):t.span({className:`missing-value`})})}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.url={icon:`ri-link`,label:`URL`,settings:cn,input:sn,view:ln,dummyData:(e,n=!1)=>`https://example.com`};var J=Intl.DateTimeFormat().resolvedOptions().timeZone;function un(e){let n=`date_`+app.utils.randomString();return t.div({className:`record-field-input field-type-date`},t.div({className:`field`},t.label({htmlFor:n},t.i({className:app.fieldTypes.date.icon,ariaHidden:!0}),t.span({className:`txt`},()=>e.field.name,` (`,J,`)`)),t.input({id:n,step:1,type:`datetime-local`,name:()=>e.field.name,required:()=>e.field.required,value:()=>app.utils.toDatetimeLocalInputValue(e.record[e.field.name]),onchange:n=>{e.record[e.field.name]=app.utils.toRFC3339Datetime(n.target.value)}})),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function Y(e){let n=`f_`+app.utils.randomString();return app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.min`},t.span({className:`txt`},`Min date (Local)`)),t.input({type:`datetime-local`,id:n+`.min`,step:1,name:()=>`fields.${e.fieldIndex}.min`,value:()=>app.utils.toDatetimeLocalInputValue(e.field.min),onchange:n=>{e.field.min=app.utils.toRFC3339Datetime(n.target.value)}}))),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.max`},t.span({className:`txt`},`Max date (Local)`)),t.input({type:`datetime-local`,id:n+`.max`,step:1,name:()=>`fields.${e.fieldIndex}.max`,value:()=>app.utils.toDatetimeLocalInputValue(e.field.max),onchange:n=>{e.field.max=app.utils.toRFC3339Datetime(n.target.value)}}))),t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`},`(!='')`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Requires the field value to be nonempty string`)})))]})}function dn(e){return t.div({className:`record-field-view field-type-date`},app.components.formattedDate({value:()=>e.record[e.field.name],short:()=>e.short}))}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.date={icon:`ri-calendar-line`,label:`Datetime`,settings:Y,input:un,view:dn,dummyData:(e,n=!1)=>new Date().toISOString().replaceAll(`T`,` `)};function fn(e){delete e.clone[e.field.name]}function pn(e){let n=[{label:`Create`,value:1},{label:`Update`,value:2},{label:`Create/Update`,value:3}];function r(e){return e.onCreate&&e.onUpdate?3:e.onUpdate?2:1}function i(n){switch(n){case 1:e.field.onCreate=!0,e.field.onUpdate=!1;break;case 2:e.field.onCreate=!1,e.field.onUpdate=!0;break;case 3:e.field.onCreate=!0,e.field.onUpdate=!0;break}}let a=store({isDropdownOpen:!1});return app.components.fieldSettings(e,{header:t.div({className:`field header-select autodate-select`,ariaDescription:app.attrs.tooltip(`Auto set on`,`left`),onmount:()=>{i(r(e.field))}},app.components.select({required:!0,options:n,disabled:()=>e.originalCollection?.system,value:()=>r(e.field),onchange:e=>i(e?.[0]?.value),on
Line truncated
|
||||
Leave empty for no restriction.`)})),app.components.select({max:99,placeholder:`No restriction`,options:app.utils.mimeTypes.map(e=>({value:e.mimeType,label:()=>t.div({className:`inline-flex gap-10`},t.span({className:`txt`},e.ext||`-`),t.small({className:`txt-hint`},e.mimeType))})),name:()=>`fields.${e.fieldIndex}.mimeTypes`,value:()=>app.utils.toArray(e.field.mimeTypes),onchange:n=>e.field.mimeTypes=n.map(e=>e.value)})),t.div({className:`field-help`},t.button({type:`button`,className:`link-hint gap-0`,"html-popovertarget":n+`mimeTypesDropdown`},t.span({className:`txt`},`Choose presets`),t.i({className:`ri-arrow-drop-down-fill`,ariaHidden:!0})),t.div({id:n+`mimeTypesDropdown`,className:`dropdown sm nowrap left p-10`,popover:`auto`},t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,onclick:n=>{e.field.mimeTypes=[`image/jpeg`,`image/png`,`image/svg+xml`,`image/gif`,`image/webp`],n.target.closest(`.dropdown`).hidePopover()},textContent:`Images (jpg, png, svg, gif, webp)`}),t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,onclick:n=>{e.field.mimeTypes=[`application/pdf`,`application/msword`,`application/vnd.openxmlformats-officedocument.wordprocessingml.document`,`application/vnd.ms-excel`,`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`],n.target.closest(`.dropdown`).hidePopover()},textContent:`Documents (pdf, doc/docx, xls/xlsx)`}),t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,onclick:n=>{e.field.mimeTypes=[`video/mp4`,`video/mpeg`,`video/x-msvideo`,`video/quicktime`,`video/3gpp`],n.target.closest(`.dropdown`).hidePopover()},textContent:`Videos (mp4, mpeg, avi, mov, 3gp)`}),t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,onclick:n=>{e.field.mimeTypes=[`application/zip`,`application/x-7z-compressed`,`application/x-rar-compressed`],n.target.closest(`.dropdown`).hidePopover()},textContent:`Archives (zip, 7zip, rar)`})))),t.div({className:()=>e.field.maxSelect>1?`col-sm-6`:`col-sm-9`},t.div({className:`field`},t.label({htmlFor:n+`.thumbs`},t.span({className:`txt`},`Thumb sizes`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`List of additional thumb sizes for image files, along with the default thumb size of 100x100. The thumbs are generated lazily on first access.`)})),t.input({type:`text`,id:n+`.thumbs`,placeholder:`e.g. 50x50, 480x720`,name:()=>`fields.${e.fieldIndex}.thumbs`,value:()=>app.utils.joinNonEmpty(e.field.thumbs),onchange:n=>e.field.thumbs=app.utils.splitNonEmpty(n.target.value,`,`)})),t.div({className:`field-help`},t.span({className:`txt m-r-5`},`Use comma as separator.`),t.button({type:`button`,className:`link-hint gap-0`,"html-popovertarget":n+`thumbFormatsDropdown`},t.span({className:`txt`},`Supported formats`),t.i({className:`ri-arrow-drop-down-fill`,ariaHidden:!0})),t.div({id:n+`thumbFormatsDropdown`,className:`dropdown sm nowrap left p-10`,popover:`auto`},t.ul({className:`m-0 p-l-sm`},t.li(null,t.strong(null,`WxH`),t.span(null,` (e.g. 100x50) - crop to WxH viewbox (from center)`)),t.li(null,t.strong(null,`WxHt`),t.span(null,` (e.g. 100x50t) - crop to WxH viewbox (from top)`)),t.li(null,t.strong(null,`WxHb`),t.span(null,` (e.g. 100x50b) - crop to WxH viewbox (from bottom)`)),t.li(null,t.strong(null,`WxHf`),t.span(null,` (e.g. 100x50f) - fit inside a WxH viewbox (without cropping)`)),t.li(null,t.strong(null,`0xH`),t.span(null,` (e.g. 0x50) - resize to H height preserving the aspect ratio`)),t.li(null,t.strong(null,`Wx0`),t.span(null,` (e.g. 100x0) - resize to W width preserving the aspect ratio`)))))),t.div({className:`col-sm-3`},t.div({className:`field`},t.label({htmlFor:n+`.maxSize`},`Max size`),t.input({type:`number`,id:n+`.maxSize`,step:1,min:0,max:2**53-1,placeholder:`~5MB default`,name:()=>`fields.${e.fieldIndex}.maxSize`,value:()=>e.field.maxSize||``,oninput:n=>e.field.maxSize=parseInt(n.target.value,10)})),t.div({className:`field-help`},`In bytes.`)),()=>{if(e.field.maxSelect>1)return t.div({className:`col-sm-3`},t.div({className:`field`},t.label({htmlFor:n+`.maxSelect`},`Max select`),t.input({type:`number`,id:n+`.maxSelect`,placeholder:`Default to single`,step:1,min:2,required:!0,max:2**53-1,name:()=>`fields.${e.fieldIndex}.maxSelect`,value:()=>e.field.maxSelect||``,onchange:e=>{let n=parseInt(e.target.value,10);n>1?props.field.maxSelect=n:props.field.maxSelect=1}})))},t.div({className:`col-sm-12`},t.div({className:`field m-t-5 m-b-5`},t.input({className:`switch`,type:`checkbox`,id:n+`.protected`,name:()=>`fields.${e.fieldIndex}.protected`,checked:()=>!!e.field.protected,onchange:n=>e.field.protected=n.target.checked}),t.label({htmlFor:n+`.protected`},t.span({className:`txt`},`Protected`),t.small({className:`txt-hint`},`File download requests will need to satisfy the View API rule (`,t.a({href:`https://pocketbase.io/docs/files-handling#protected-files`,target:`_blank`,rel:`noopener noreferrer`,textContent:`Learn more`}),`).`)))),t.div({className:`col-sm-12`},t.
Line truncated
|
||||
`),oninput:n=>{let r=n.target.value.trimStart().replaceAll(`
|
||||
|
||||
`,`
|
||||
`).split(`
|
||||
`);e.field.values=r,app.utils.deleteByPath(app.store.errors,`fields.${e.fieldIndex}.values`)},onchange:n=>{let r=new Set,i=n.target.value.split(`
|
||||
`);for(let e of i)e!=``&&r.add(e);e.field.values=Array.from(r)},onblur:e=>{(!e.relatedTarget||!i.contains(e.relatedTarget))&&i.hidePopover()}}))),a=[watch(()=>{e.field.values?.length&&e.field.maxSelect>e.field.values.length&&(e.field.maxSelect=e.field.values.length)})];return app.components.fieldSettings(e,{header:[t.div({className:`field header-select field-select-choices-input`,onunmount:()=>{a.forEach(e=>e?.unwatch())}},t.input({type:`text`,placeholder:`Add choices*`,className:`txt-left inline-error`,value:()=>e.field.values?.join(` • `)||``,name:()=>`fields.${e.fieldIndex}.values`,onfocus:e=>(i?.showPopover({source:e.target}),i.querySelector(`textarea`)?.focus(),!1)}),i),t.div({className:`field header-select single-multiple-select`},app.components.select({required:!0,options:r,value:()=>e.field.maxSelect>1,onchange:n=>{n?.[0]?.value?e.field.maxSelect=e.field.values.length||2:e.field.maxSelect=1}}))],content:()=>t.div({className:`grid sm`},()=>{if(e.field.maxSelect>1)return t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.maxSelect`},`Max select`),t.input({type:`number`,id:n+`.maxSelect`,placeholder:`Default to single`,step:1,min:2,max:()=>e.field.values?.length||2,name:()=>`fields.${e.fieldIndex}.maxSelect`,value:()=>e.field.maxSelect||``,onchange:n=>{let r=parseInt(n.target.value,10);r>1?e.field.maxSelect=r:e.field.maxSelect=1}})))},t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`,textContent:()=>e.field.maxSelect>1?`(!=[])`:`(!='')`}),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(()=>`Requires the field value to be nonempty ${e.field.maxSelect>1?`array`:`string`}.`)})))]})}function En(e){return t.div({className:`record-field-view field-type-select`},t.div({className:`inline-flex gap-5`},()=>{let n=app.utils.toArray(e.record[e.field.name],!1);return n.length?n.map(e=>t.span({className:`label`,title:e,textContent:app.utils.truncate(e,100)})):t.span({className:`missing-value`})}))}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.select={icon:`ri-list-check`,label:`Select`,settings:Tn,input:wn,view:En,filterModifiers:e=>e.maxSelect>1?[`each`,`length`]:[],dummyData:(e,n=!1)=>e.maxSelect>1?e.values?.slice(0,2)||[]:e.values?.[0]||``};function Dn(e){let n=`json_`+app.utils.randomString(),r=store({value:``}),i=[watch(()=>e.record[e.field.name],(n,i)=>{if(!(n!==``&&n===r.value)){if(typeof n==`string`&&!n.startsWith(`"`)&&!n.endsWith(`"`)){r.value=JSON.stringify(n===void 0?null:n),e.record[e.field.name]=r.value;return}typeof n==`string`&&n.startsWith(`"`)&&n.endsWith(`"`)?r.value=n:n===null?r.value=`null`:r.value=JSON.stringify(n===void 0?null:n,null,2)}})];function a(){let n=r.value.trim();if(n===``){e.record[e.field.name]=null;return}try{let r=JSON.parse(n);typeof r==`string`?e.record[e.field.name]=JSON.stringify(r):e.record[e.field.name]=r}catch{e.record[e.field.name]=n}}return t.div({className:`record-field-input field-type-json`},t.div({className:`field`,onunmount:()=>{clearTimeout(void 0),i.forEach(e=>e?.unwatch())}},t.label({htmlFor:n},t.i({className:app.fieldTypes.json.icon,ariaHidden:!0}),t.span({className:`txt`},()=>e.field.name),t.span({hidden:()=>On(r.value.trim()),className:`json-state`,ariaDescription:app.attrs.tooltip(`Invalid JSON`,`left`)},t.i({className:`ri-error-warning-fill txt-danger`,ariaHidden:!0})),t.span({hidden:()=>!On(r.value.trim()),className:`json-state`,ariaDescription:app.attrs.tooltip(`Valid JSON`,`left`)},t.i({className:`ri-checkbox-circle-fill txt-success`,ariaHidden:!0}))),app.components.codeEditor({language:`js`,id:n,name:()=>e.field.name,required:()=>e.field.required,value:()=>r.value,oninput:e=>r.value=e,onblur:()=>a()})),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function On(e){if(e===``)return!0;try{return JSON.parse(e),!0}catch{return!1}}function kn(e){try{let n=e.record[e.field.name];typeof n==`string`&&JSON.parse(n)}catch(r){throw new n({status:400,response:{message:`Invalid JSON data`,data:{[e.field.name]:{code:`invalid_json`,message:r.toString()}}}})}}function An(e){let n=`f_`+app.utils.randomString(),r=store({showInfo:!1});return app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.maxSize`},t.span(null,`Max size `),t.small(null,`(bytes)`)),t.input({type:`number`,id:n+`.maxSize`,name:()=>`fields.${e.fieldIndex}.maxSize`,min:0,step:1
Line truncated
|
||||
To target the newly submitted ones you can use @request.body.*.`)})],name:`updateRule`,autocomplete:i,disabled:()=>e.originalCollection?.system,value:()=>e.collection.updateRule,oninput:n=>e.collection.updateRule=n})),t.div({className:`col-12`,ariaDescription:r()},app.components.ruleField({label:`Delete rule`,name:`deleteRule`,autocomplete:i,disabled:()=>e.originalCollection?.system,value:()=>e.collection.deleteRule,oninput:n=>e.collection.deleteRule=n}))]}),()=>{if(e.collection.type==`auth`)return[t.hr({className:`m-t-base m-b-base`}),t.button({type:`button`,onmount:()=>{n.showAuthRules=e.collection.manageRule!==null||e.collection.authRule!==``},className:()=>`btn secondary sm ${n.showAuthRules?``:`transparent`}`,onclick:()=>{n.showAuthRules=!n.showAuthRules}},t.span({className:`txt`},`Additional auth collection rules`),t.i({ariaHidden:!0,className:()=>n.showAuthRules?`ri-arrow-drop-up-line`:`ri-arrow-drop-down-line`})),app.components.slide(()=>n.showAuthRules,t.div({className:`grid sm m-t-sm`},t.div({className:`col-12`,ariaDescription:r()},app.components.ruleField({label:`Authentication rule`,name:`authRule`,placeholder:``,autocomplete:i,disabled:()=>e.originalCollection?.system,value:()=>e.collection.authRule,oninput:n=>e.collection.authRule=n}),t.div({className:`field-help`},t.p(null,`This rule is executed every time `,t.strong(null,`before authentication`),` allowing you to restrict who can authenticate.`),t.p(null,`For example, to allow only verified users you can set it to `,t.code(null,`verified = true`),`.`),t.p(null,`Leave it empty to allow anyone with an account to authenticate.`),t.p(null,`To disable authentication entirely you can change it to "Set superusers only".`))),t.div({className:`col-12`,ariaDescription:r()},app.components.ruleField({label:`Manage rule`,name:`manageRule`,autocomplete:i,disabled:()=>e.originalCollection?.system,value:()=>e.collection.manageRule,oninput:n=>e.collection.manageRule=n}),t.div({className:`field-help`},t.p(null,`This rule is executed in addition to the `,t.strong(null,`create`),` and `,t.strong(null,`update`),` API rules.`),t.p(null,`It enables superuser-like permissions to allow fully managing the auth record(s), eg. changing the password without requiring to enter the old one, directly updating the verified state or email, etc.`)))))]})}var qn=`test_view_query`;function Z(e){let n=`query_`+app.utils.randomString(),r=`SELECT.FROM.WHERE.LEFT JOIN.INNER JOIN.ON.AS.GROUP BY.HAVING.ORDER BY.ASC.DESC.LIMIT.OFFSET.WITH.NOT.IN.AND.OR.EXISTS.LIKE.CAST.REAL.DECIMAL.NUMERIC.INT.TEXT.BOOL`.split(`.`),i=store({testRecords:[],testError:``,isTesting:!1});async function a(n){if(i.isTesting=!0,i.testRecords=[],(app.store.errors?.viewQuery||app.store.errors?.fields)&&(delete app.store.errors.viewQuery,delete app.store.errors.fields),!n){i.testError=``,i.isTesting=!1;return}try{let r=await app.pb.send(`/api/collections/meta/dry-run-view`,{method:`POST`,body:{query:n},requestKey:qn});e.collection?.id?i.testRecords=r.sample.map(n=>(n.collectionId=e.collection?.id,n.collectionName=e.collection?.name,n)):i.testRecords=r.sample,i.testError=``,i.isTesting=!1}catch(e){e.isAbort||(i.testError=e.message||`Invalid query.`,i.isTesting=!1)}}let o,s=[watch(()=>e.collection?.viewQuery,e=>{clearTimeout(o),o=setTimeout(()=>a(e),200)})];return t.div({pbEvent:`collectionViewQueryTabContent`,className:`collection-tab-content collection-view-query-tab-content`,onunmount:()=>{clearTimeout(o),app.pb.cancelRequest(qn),s.forEach(e=>e?.unwatch())}},t.div({className:`grid`},t.div({className:`col-12`},t.div({className:`txt-right txt-sm m-b-10`},t.button({type:`button`,className:`txt-bold link-hint`,"html-popovertarget":n+`caveats_dropdown`},()=>`Query caveats`)),t.div({id:n+`caveats_dropdown`,className:`dropdown sm query-caveats-dropdown`,popover:`auto`},t.ul(null,t.li(null,`Wildcard columns (*) are not supported.`),t.li(null,`The query must have a unique `,t.code(null,`id`),` column.`,t.br(),`If your query doesn't have a suitable one, you can use the universal `,t.code(null,`(ROW_NUMBER() OVER()) as id`),`.`),t.li(null,`Expressions must be aliased with a valid formatted field name, e.g. `,t.code(null,`MAX(balance) as maxBalance`),`.`),t.li(null,`Combined/multi-spaced expressions must be wrapped in parenthesis, e.g. `,t.code(null,`(MAX(balance) + 1) as maxBalance`),`.`),t.li(null,`UNION expressions are supported but the entire query must be wrapped in parenthesis.`))),t.div({className:`field`},t.label({htmlFor:n+`.viewQuery`},t.span({className:`txt`},`Select query`),t.span({hidden:()=>!i.testError,className:`query-state`,ariaDescription:app.attrs.tooltip(`Invalid query`,`left`)},t.i({className:`ri-error-warning-fill txt-danger`,ariaHidden:!0})),t.span({hidden:()=>!!i.testError,className:`query-state`,ariaDescription:app.attrs.tooltip(`Valid query`,`left`)},t.i({className:`ri-checkbox-circle-fill txt-success`,ariaHidden:!0}))),app.components.codeEditor({id:n+`.viewQuery`,name:`viewQuery`,language:`sql`,
Line truncated
|
||||
`+e)})},t.div({className:`btns`},t.button({type:`button`,className:()=>`btn expanded-lg ${a.isSaving?`loading`:``}`,disabled:()=>!a.canSave,onclick:()=>s(!0)},t.span({className:`txt`},()=>a.isNew?`Create`:`Save changes`)),t.button({type:`button`,title:`Save options`,className:()=>`btn p-5`,disabled:()=>!a.canSave,"html-popovertarget":i+`save_options`},t.i({className:`ri-arrow-up-s-line`,ariaHidden:!0})),t.div({id:i+`save_options`,className:`dropdown nowrap`,popover:`auto`},t.button({type:`button`,className:`dropdown-item`,onclick:e=>{e.target.closest(`.dropdown`).hidePopover(),s(!1)}},t.span({className:`txt`},`Save and continue`),t.small({className:`txt-hint`},`(Ctrl+S)`)),t.hr(),t.button({type:`button`,className:`dropdown-item`,onclick:e=>{e.target.closest(`.dropdown`).hidePopover(),u()}},t.span({className:`txt`},`Reset form`)))))),r}function Yn(e){let n=JSON.parse(JSON.stringify(app.store.collectionScaffolds[e.type])),r=JSON.parse(JSON.stringify(e.fields))||[],i=r.filter(e=>!e.system);e.fields=n.fields||[];for(let n of r){if(!n.system)continue;let r=e.fields.find(e=>e.name==n.name);r&&Object.assign(r,n)}for(let n of i)e.fields.push(n);if(e.indexes=e.indexes||[],e.indexes.length){let r=n?.indexes||[];indexesLoop:for(let n=e.indexes.length-1;n>=0;n--){let i=app.utils.parseIndex(e.indexes[n]),a=i.indexName.toLowerCase();for(let i of r)if(a==app.utils.parseIndex(i).indexName.toLowerCase()){e.indexes.splice(n,1);continue indexesLoop}for(let r of i.columns)if(!e.fields.find(e=>e.name.toLowerCase()==r.name.toLowerCase())){e.indexes.splice(n,1);continue indexesLoop}}}app.utils.mergeUnique(e.indexes,n.indexes)}function Xn(e,n){let r=`truncate_`+app.utils.randomString(),i=store({isSubmitting:!1,nameConfirm:``});async function a(){if(i.isSubmitting||!e.originalCollection?.name||e.originalCollection.name!=i.nameConfirm)return!1;i.isSubmitting=!0;try{return await app.pb.collections.truncate(e.originalCollection.name),n.ontruncate?.(JSON.parse(JSON.stringify(e.originalCollection))),app.toasts.success(`Successfully truncated collection "${e.originalCollection.name}".`),i.isSubmitting=!1,!0}catch(e){i.isSubmitting=!1,app.checkApiError(e)}return!1}return t.button({type:`button`,className:`dropdown-item txt-danger`,disabled:()=>i.isSubmitting,onclick:n=>{n.target.closest(`.dropdown`).hidePopover(),app.modals.confirm(t.div(null,t.h6({className:`block txt-center`},`Do you really want to delete all records of the collection?`),t.div({className:`confirm-collection-label txt-bold m-t-sm m-b-sm`},`Type the collection name `,t.div({className:`label`},()=>e.originalCollection.name,app.components.copyButton(()=>e.originalCollection?.name)),` to confirm:`),t.div({className:`field`},t.label({htmlFor:r+`.confirm_name`},`Collection name`),t.input({id:r+`.confirm_name`,type:`text`,required:!0,pattern:()=>RegExp.escape(e.originalCollection.name),value:()=>i.nameConfirm,oninput:e=>i.nameConfirm=e.target.value}))),async()=>{if(document.getElementById(r+`.confirm_name`)?.reportValidity(),!await a())return!1;app.modals.close(n.target.closest(`.modal`))},()=>{i.nameConfirm=``})}},t.i({className:`ri-eraser-line`,ariaHidden:!0}),t.span({className:`txt`},`Truncate`))}function Zn(e,n){let r=`delete_`+app.utils.randomString(),i=store({isSubmitting:!1,nameConfirm:``});async function a(){if(i.isSubmitting||!e.originalCollection?.name||e.originalCollection.name!=i.nameConfirm)return!1;i.isSubmitting=!0;try{return await app.pb.collections.delete(e.originalCollection.name),n?.ondelete?.(JSON.parse(JSON.stringify(e.originalCollection))),app.utils.removeByKey(app.store.collections,`id`,e.originalCollection.id),app.toasts.success(`Successfully deleted collection "${e.originalCollection.name}".`),i.isSubmitting=!1,!0}catch(e){i.isSubmitting=!1,app.checkApiError(e)}return!1}return t.button({type:`button`,className:`dropdown-item txt-danger`,disabled:()=>i.isSubmitting,onclick:n=>{n.target.closest(`.dropdown`).hidePopover();let o=n.target.closest(`.modal`);app.modals.confirm(t.div({className:`block`},t.h6({className:`block txt-center`},()=>e.originalCollection.type==`view`?`Do you really want to delete the selected collection?`:`Do you really want to delete the selected collection and all its records`),t.div({className:`confirm-collection-label txt-bold m-t-sm m-b-sm`},`Type the collection name `,t.div({className:`label`},()=>e.originalCollection.name,app.components.copyButton(()=>e.originalCollection?.name)),` to confirm:`),t.div({className:`field`},t.label({htmlFor:r+`.confirm_name`},`Collection name`),t.input({id:r+`.confirm_name`,type:`text`,required:!0,pattern:()=>RegExp.escape(e.originalCollection.name),value:()=>i.nameConfirm,oninput:e=>i.nameConfirm=e.target.value}))),async()=>{if(document.getElementById(r+`.confirm_name`)?.reportValidity(),!await a())return!1;app.modals.close(o)},()=>{i.nameConfirm=``})}},t.i({className:`ri-delete-bin-7-line`,ariaHidden:!0}),t.span({className:`txt`},`Delete`))}window.app=window.app||{},window.app.components=window.
Line truncated
|
||||
...
|
||||
-----END PRIVATE KEY-----`,value:()=>a.privateKey||``,oninput:e=>a.privateKey=e.target.value})),t.div({className:`field-help`},`The key is not stored on the server and it is used only for generating the signed JWT.`)))),t.footer({className:`modal-footer`},t.button({type:`button`,className:`btn transparent m-r-auto`,onclick:()=>app.modals.close(n)},t.span({className:`txt`},`Close`)),t.button({"html-form":r+`_form`,type:`submit`,className:`btn expanded`},t.i({className:`ri-key-line`,ariaHidden:!0}),t.span({className:`txt`},`Generate secret`)))),n}function lr(){return t.div({className:`inline-flex gap-5`},t.span(null,`Default log levels:`),()=>{let e=[];for(let n in app.utils.logLevels)e.push(t.code(null,`${n}:${app.utils.logLevels[n].label}`));return e})}window.app=window.app||{},window.app.modals=window.app.modals||{},window.app.modals.openLogsSettings=function(e={onbeforeopen:null,onafteropen:null,onbeforeclose:null,onafterclose:null,onsave:null}){let n=ur(e);n&&(document.body.appendChild(n),app.modals.open(n))};function ur(e){let n,r=store({isLoading:!1,isSaving:!1,formSettings:{},initFormSettingsHash:``,get hasChanges(){return r.initFormSettingsHash!=JSON.stringify(r.formSettings)}});function i(e={}){r.formSettings={logs:e?.logs||{}},r.initFormSettingsHash=JSON.stringify(r.formSettings)}async function a(){r.isLoading=!0;try{i(await app.pb.settings.getAll({requestKey:`logsSettings`})),r.isLoading=!1}catch(e){e.isAbort||(r.isLoading=!1,app.checkApiError(e))}}async function o(){if(r.hasChanges){r.isSaving=!0;try{let a=await app.pb.settings.update(app.utils.filterRedactedProps(r.formSettings));e.onsave?.(a),i(a),app.toasts.success(`Successfully saved logs settings.`),r.isSaving=!1,app.modals.close(n)}catch(e){e.isAbort||(r.isSaving=!1,app.checkApiError(e))}}}return n=t.div({pbEvent:`logsSettingsModal`,className:`modal popup sm logs-settings-modal`,onbeforeopen:n=>(a(),e.onbeforeopen?.(n)),onafteropen:n=>{e.onafteropen?.(n)},onbeforeclose:n=>e.onbeforeclose?.(n),onafterclose:n=>{e.onafterclose?.(n),n?.remove()}},t.header({className:`modal-header`},t.h5({className:`m-auto`},`Logs settings`)),()=>r.isLoading?t.div({className:`modal-content flex`,style:`min-height: 200px`},t.span({className:`loader m-auto`})):[t.form({pbEvent:`logsSettingsForm`,id:`logsSettingsForm`,className:`modal-content logs-settings-form`,onsubmit:e=>{e.preventDefault(),o()}},t.div({className:`grid`},t.div({className:`col-lg-12`},t.field({className:`field`},t.label({htmlFor:`logs.maxDays`},`Max days retention`),t.input({type:`number`,id:`logs.maxDays`,name:`logs.maxDays`,min:0,required:!0,value:()=>r.formSettings.logs.maxDays,oninput:e=>r.formSettings.logs.maxDays=e.target.value<<0})),t.div({className:`field-help`},`Set to `,t.code(null,0),` to disable logs persistence.`)),t.div({className:`col-lg-12`},t.field({className:`field`},t.label({htmlFor:`logs.minLevel`},`Min log level`),t.input({type:`number`,id:`logs.minLevel`,name:`logs.minLevel`,min:-100,max:100,required:!0,value:()=>r.formSettings.logs.minLevel,oninput:e=>r.formSettings.logs.minLevel=e.target.value<<0})),t.div({className:`field-help`},t.div(null,`Logs with level below the minimum will be ignored.`),lr())),t.div({className:`col-lg-12`},t.field({className:`field`},t.input({type:`checkbox`,id:`logs.logIP`,name:`logs.logIP`,className:`switch`,checked:()=>r.formSettings.logs.logIP,onchange:e=>r.formSettings.logs.logIP=e.target.checked}),t.label({htmlFor:`logs.logIP`},`Enable IP logging`))),t.div({className:`col-lg-12`},t.field({className:`field`},t.input({type:`checkbox`,id:`logs.logAuthId`,name:`logs.logAuthId`,className:`switch`,checked:()=>r.formSettings.logs.logAuthId,onchange:e=>r.formSettings.logs.logAuthId=e.target.checked}),t.label({htmlFor:`logs.logAuthId`},`Enable Auth Id logging`))))),t.footer({className:`modal-footer`},t.button({type:`button`,className:`btn transparent m-r-auto`,onclick:()=>app.modals.close(n),disabled:()=>r.isSaving},t.span({className:`txt`},`Close`)),t.button({type:`submit`,"html-form":`logsSettingsForm`,className:()=>`btn ${r.isSaving?`loading`:``}`,disabled:()=>!r.hasChanges||r.isSaving},t.span({className:`txt`},`Save changes`)))]),n}function dr(e){return t.div({className:()=>`label log-level-label level-${e.level}`},t.span({className:`txt`},()=>`${app.utils.logLevels[e.level]?.label||`UNKN`} (${e.level})`))}window.app=window.app||{},window.app.modals=window.app.modals||{},window.app.modals.openLogPreview=function(e,n={onbeforeopen:null,onafteropen:null,onbeforeclose:null,onafterclose:null}){let r=hr(e,n);r&&(document.body.appendChild(r),app.modals.open(r))};var fr=[`execTime`,`type`,`auth`,`authId`,`status`,`method`,`url`,`referer`,`remoteIP`,`userIP`,`userAgent`,`error`,`details`];function pr(e){app.utils.downloadJSON(e,`log_`+e.created.replaceAll(/[-:\. ]/gi,``)+`.json`)}function mr(e){app.utils.copyToClipboard(JSON.stringify(e,null,2)),app.toasts.success(`Log copied to clipboard!`)}function hr(e,n){let r,i=store({isLoading:!1,log:null,get isRequ
Line truncated
|
||||
{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
|
||||
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
|
||||
connecting to your PocketBase app).
|
||||
`),t.p(null,`
|
||||
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 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
|
||||
@weekly
|
||||
@daily
|
||||
@midnight
|
||||
@hourly`)},`macros`),`.`,t.br(),`By default the timezone is in UTC.`)),t.div({className:`col-lg-6`},t.div({className:`field`},t.label({htmlFor:`backups.cronMaxKeep`},`Max @auto backups to keep`),t.input({id:`backups.cronMaxKeep`,name:`backups.cronMaxKeep`,type:`number`,required:()=>a.enableAutoBackups,min:1,value:()=>a.formSettings.backups.cronMaxKeep,oninput:e=>{a.formSettings.backups.cronMaxKeep=parseInt(e.target.value,10)}})))))),t.div({className:`col-lg-12`},app.components.s3ConfigFields({toggleLabel:`Store backups in S3 storage`,testFilesystem:`backups`,config:()=>a.formSettings.backups.s3})),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({hidden:()=>!a.hasChanges,type:`button`,className:`btn transparent secondary`,onclick:l},t.span({className:`txt`},`Cancel`)),t.button({className:()=>`btn expanded-lg ${a.isSaving?`loading`:``}`,disabled:()=>!a.hasChanges||a.isSaving},t.span({className:`txt`},`Save changes`))))])))}function ei(e={oncreated:null}){let n=ti(e);n&&(document.body.appendChild(n),app.modals.open(n))}function ti(e){let n,r=`backup_create_`+app.utils.randomString(),i=store({name:``,isSubmitting:!1}),a;async function o(){if(!i.isSubmitting){i.isSubmitting=!0,clearTimeout(a),a=setTimeout(()=>{app.modals.close(n)},1500);try{await app.pb.backups.create(i.name,{requestKey:r}),i.isSubmitting=!1,e.oncreated&&e.oncreated(i.name),app.toasts.success(`Successfully generated new backup.`),app.modals.close(n)}catch(e){e.isAbort||(clearTimeout(a),i.isSubmitting=!1,app.checkApiError(e))}}}return n=t.div({pbEvent:`backupCreateModal`,className:`modal popup backup-create-modal`,onbeforeclose:()=>{i.isSubmitting&&app.toasts.info(`The backup was started but may take a while to complete. You can come back later.`)},onafterclose:e=>{clearTimeout(a),e?.remove()}},t.header({className:`modal-header`},t.h5({className:`m-auto txt-center`},`Initialize new backup`)),t.form({id:r,className:`modal-content backup-restore-form`,autocomplete:`off`,onsubmit:e=>{e.preventDefault(),o()}},t.div({className:`grid`},t.div({className:`col-lg-12`},t.div({className:`alert warning`},t.div({className:`content`},t.p(null,`Please note that during the backup other concurrent write requests may fail since the database will be temporary "locked" (this usually happens only during the ZIP generation).`),t.p({className:`txt-bold`},`If you are using S3 storage for the collections file upload, you'll have to backup them separately since they are not locally stored and they will not be included in the generated backup!`)))),t.div({className:`col-lg-12`},t.div({className:`field`},t.label({htmlFor:r+`_name`},`Backup name`),t.input({id:r+`_name`,name:`name`,type:`text`,pattern:`^[a-z0-9_-]+.zip$`,placeholder:`Leave empty to autogenerate`,value:()=>i.name,oninput:e=>i.name=e.target.value})),t.div({className:`field-help`},`Must be in the format [a-z0-9_-].zip`)))),t.footer({className:`modal-footer`},t.button({type:`button`,className:`btn transparent m-r-auto`,disabled:()=>i.isSubmitting,onclick:()=>app.modals.close(n)},t.span({className:`txt`},`Cancel`)),t.button({"html-form":r,type:`submit`,className:()=>`btn ${i.isSubmitting?`loading`:``}`,disabled:()=>i.isSubmitting},t.span({className:`txt`},`Start backup`)))),n}function ni(e){let n=ri(e);document.body.appendChild(n),app.modals.open(n)}function ri(e){let n=`backup_restore_`+app.utils.randomString(),r=store({key:e,keyConfirm:``,isSubmitting:!1,get canSubmit(){return r.key&&r.key==r.keyConfirm}}),i;async function a(){if(!(r.isSubmitting||!r.canSubmit)){clearTimeout(i),r.isSubmitting=!0;try{await app.pb.backups.restore(r.keyConfirm),i=setTimeout(()=>{window.location.reload(),r.isSubmitting=!1},2e3)}catch(e){clearTimeout(i),e?.isAbort||(r.isSubmitting=!1,app.checkApiError(e))}}}return t.div({pbEvent:`backupRestoreModal`,className:`modal popup backup-restore-modal`,onbeforeclose:()=>!r.isSubmitting,onafterclose:e=>{e?.remove()},onunmount:()=>{clearTimeout(i)}},t.header({className:`modal-header`},t.h5({className:`m-auto txt-center`},`Restore `,t.strong(null,()=>r.key))),t.form({id:n,className:`modal-content backup-restore-form`,autocomplete:`off`,onsubmit:e=>{e.preventDefault(),a()}},t.div({className:`grid`},t.div({className:`col-lg-12`},t.div({className:`alert danger`},t.div({className:`content`},t.p({className:`txt-bold`},`Please proceed with extreme caution and use it only with trusted backups!`),t.p(null,`Backup restore currently works only on UNIX based systems.`),t.p(null,`The restore operation will attempt to replace your existing `,t.code(null,`pb_data`),` with the one from the backup and will restart the application process.`),t.p(null,`This means that on success all of your data (including app settings, users, superusers, etc.) will be replaced with the ones from the backup.`),t.p(null,`The operation will be reverted if the backup is invalid (ex. missing `,t.code(null,`data.db`),` file).`),t.p(null,`Below is an oLine truncated
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
@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
-81
@@ -1,81 +0,0 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./docsList-BAfVNUIM.js","./expandInfo-DGS0CLSa.js","./fieldsInfo-Bz62125-.js","./filterSyntax-Df6AnI5x.js","./docsView-Be6Uhvcf.js","./docsCreate-Be3S3y5K.js","./docsUpdate-LtD8DbWI.js","./docsListAuthMethods-9feSopQX.js","./docsAuthWithPassword-DEWj8Jyn.js","./docsAuthWithOAuth2-DUIE4EoY.js","./docsAuthWithOTP-B7z2VJzp.js","./docsAuthRefresh-UjveHHwo.js","./pageInstaller-CboSj8bL.js","./pocketbase.es-B_4DUNUU.js","./pageConfirmPasswordReset-xSsXQyFi.js","./pageConfirmVerification-BSn_UexA.js","./pageConfirmEmailChange-DWXtM27Q.js"])))=>i.map(i=>d[i]);
|
||||
import{a as e,n,r,t as i}from"./pocketbase.es-B_4DUNUU.js";var a=Object.create,o=Object.defineProperty,s=Object.getOwnPropertyDescriptor,c=Object.getOwnPropertyNames,l=Object.getPrototypeOf,u=Object.prototype.hasOwnProperty,d=(e,n)=>()=>(n||e((n={exports:{}}).exports,n),n.exports),f=(e,n,r,i)=>{if(n&&typeof n==`object`||typeof n==`function`)for(var a=c(n),l=0,d=a.length,f;l<d;l++)f=a[l],!u.call(e,f)&&f!==r&&o(e,f,{get:(e=>n[e]).bind(null,f),enumerable:!(i=s(n,f))||i.enumerable});return e},p=(e,n,r)=>(r=e==null?{}:a(l(e)),f(n||!e||!e.__esModule?o(r,`default`,{value:e,enumerable:!0}):r,e));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))r(e);new MutationObserver(e=>{for(let n of e)if(n.type===`childList`)for(let e of n.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&r(e)}).observe(document,{childList:!0,subtree:!0});function n(e){let n={};return e.integrity&&(n.integrity=e.integrity),e.referrerPolicy&&(n.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?n.credentials=`include`:e.crossOrigin===`anonymous`?n.credentials=`omit`:n.credentials=`same-origin`,n}function r(e){if(e.ep)return;e.ep=!0;let r=n(e);fetch(e.href,r)}})();var m=`abcdefghijklmnopqrstuvwxyz0123456789`,h=`pb_redirect`,g=store({hash:window.location.hash});window.addEventListener(`hashchange`,()=>{g.hash=window.location.hash}),Prism.languages.pbrule={string:Prism.languages.js.string,number:Prism.languages.js.number,function:Prism.languages.js.function,boolean:/\b(?:true|false)\b/i,constant:/\b(?:null)\b/i,comment:{pattern:/\/\/.*/,greedy:!0},italic:/_via_|\:\w+/,keyword:/&&|\|\||\??(?:!~|!=|>=|<=|=|~|>|>|<)(?=[@\w\s]|$)/};var _={isObject(e){return typeof e==`object`&&!!e&&e.constructor===Object},isEmpty(e){return e==null||e===``||Array.isArray(e)&&e.length===0||typeof e==`object`&&app.utils.isEmptyObject(e)},isEmptyObject(e){for(let n in e)return!1;return!0},toArray(e,n=!1){return Array.isArray(e)?e.slice():(n||!_.isEmpty(e))&&e!==void 0?[e]:[]},removeByValue(e,n){if(!Array.isArray(e)){console.warn(`[removeByValue] not an array:`,e);return}for(let r=e.length-1;r>=0;r--)if(e[r]==n){e.splice(r,1);break}},removeByKey(e,n,r){if(!Array.isArray(e)){console.warn(`[removeByKey] not an array:`,e);return}for(let i in e)if(e[i][n]==r){e.splice(i,1);break}},pushUnique(e,n){if(!Array.isArray(e)){console.warn(`[pushUnique] not an array:`,e);return}e.includes(n)||e.push(n)},mergeUnique(e,n){for(let r of n)app.utils.pushUnique(e,r);return e},pushOrReplaceObject(e,n,r=`id`){for(let i=e.length-1;i>=0;i--)if(e[i][r]==n[r]){e[i]=n;return}e.push(n)},filterDuplicatesByKey(e,n=`id`){e=Array.isArray(e)?e:[];let r={};for(let i of e)r[i[n]]=i;return Object.values(r)},filterRedactedProps(e,n=`******`){let r=JSON.parse(JSON.stringify(e||{}));for(let e in r)typeof r[e]==`object`&&r[e]!==null?r[e]=_.filterRedactedProps(r[e],n):r[e]===n&&delete r[e];return r},getByPath(e,n,r=null,i=`.`){let a=e||{},o=(n||``).split(i);for(let e of o){if(!_.isObject(a)&&!Array.isArray(a)||a[e]===void 0)return r;a=a[e]}return a},setByPath(e,n,r,i=`.`){if(typeof e!=`object`||!e){console.warn(`setByPath: data not an object or array.`);return}let a=e,o=n.split(i),s=o.pop();for(let e of o)(!_.isObject(a)&&!Array.isArray(a)||!_.isObject(a[e])&&!Array.isArray(a[e]))&&(a[e]={}),a=a[e];a[s]=r},deleteByPath(e,n,r=`.`){let i=e||{},a=(n||``).split(r),o=a.pop();for(let e of a)(!_.isObject(i)&&!Array.isArray(i)||!_.isObject(i[e])&&!Array.isArray(i[e]))&&(i[e]={}),i=i[e];Array.isArray(i)?i.splice(o,1):_.isObject(i)&&delete i[o],a.length>0&&(Array.isArray(i)&&!i.length||_.isObject(i)&&!Object.keys(i).length)&&(Array.isArray(e)&&e.length>0||_.isObject(e)&&Object.keys(e).length>0)&&_.deleteByPath(e,a.join(r),r)},emptyClone(e,n=[]){let r=JSON.parse(JSON.stringify(e));for(let e in r)n.includes(e)||(typeof r[e]==`string`?r[e]=``:typeof r[e]==`number`?r[e]=0:Array.isArray(r[e])?r[e]=[]:app.utils.isObject(r[e])&&(r[e]={}));return r},randomString(e=8,n=m){let r=``;for(let i=0;i<e;i++)r+=n.charAt(Math.floor(Math.random()*n.length));return r},randomSecret(e=30){if(typeof crypto>`u`)return app.utils.randomString(e);let n=new Uint8Array(e);crypto.getRandomValues(n);let r=``;for(let i=0;i<e;i++)r+=`-_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`.charAt(n[i]%64);return r},sentenize(e,n=!0){if(typeof e!=`string`)return``;if(e=e.trim().split(`_`).join(` `),e===``)return e;if(e=e[0].toUpperCase()+e.substring(1),n){let n=e[e.length-1];n!==`.`&&n!==`?`&&n!==`!`&&(e+=`.`)}return e},trimQuotedValue(e){return typeof e==`string`&&(e[0]==`"`||e[0]==`'`||e[0]=="`")&&e[0]==e[e.length-1]?e.slice(1,-1):e},slugify(e,n=`_`,r=[]){if(e===``)return``;let i={a:/а|à|á|å|â/gi,b:/б/gi,c:/ц|ç/gi,d:/д/gi,e:/е|è|é|ê|ẽ|ë/gi,f:/ф/gi,g:/г/gi,h:/х/gi,i:/й|и|ì|í|î/gi,j:/ж/gi,k:/к/gi,l:/л/gi,m:/м/gi,n:/н|ñ/gi,o:/о|ò|ó|ô|ø/gi,p:/п/gi,q:/я/gi,r:/р/gi,Line truncated
|
||||
`),n+=r.map(e=>{let n=``;return e.name.includes(`(`)||e.name.includes(` `)?n+=e.name:n+="`"+e.name+"`",e.collate&&(n+=` COLLATE `+e.collate),e.sort&&(n+=` `+e.sort.toUpperCase()),n}).join(`,
|
||||
`),r.length>1&&(n+=`
|
||||
`),n+=`)`,e.where&&(n+=` WHERE ${e.where}`),n},replaceIndexFields(e,n){let r=app.utils.parseIndex(e);return typeof n==`function`?Object.assign(r,n(r)||{}):Object.assign(r,n||{}),app.utils.buildIndex(r)},replaceIndexColumn(e,n,r){if(n===r)return e;let i=app.utils.parseIndex(e),a=!1;for(let e of i.columns)e.name===n&&(e.name=r,a=!0);return a?app.utils.buildIndex(i):e}};window.app=window.app||{},window.app.utils=_,window.app=window.app||{},window.app.utils=window.app.utils||{},window.app.utils.mimeTypes=[{ext:``,mimeType:`application/octet-stream`},{ext:`.xpm`,mimeType:`image/x-xpixmap`},{ext:`.7z`,mimeType:`application/x-7z-compressed`},{ext:`.zip`,mimeType:`application/zip`},{ext:`.xlsx`,mimeType:`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`},{ext:`.docx`,mimeType:`application/vnd.openxmlformats-officedocument.wordprocessingml.document`},{ext:`.pptx`,mimeType:`application/vnd.openxmlformats-officedocument.presentationml.presentation`},{ext:`.epub`,mimeType:`application/epub+zip`},{ext:`.apk`,mimeType:`application/vnd.android.package-archive`},{ext:`.jar`,mimeType:`application/jar`},{ext:`.odt`,mimeType:`application/vnd.oasis.opendocument.text`},{ext:`.ott`,mimeType:`application/vnd.oasis.opendocument.text-template`},{ext:`.ods`,mimeType:`application/vnd.oasis.opendocument.spreadsheet`},{ext:`.ots`,mimeType:`application/vnd.oasis.opendocument.spreadsheet-template`},{ext:`.odp`,mimeType:`application/vnd.oasis.opendocument.presentation`},{ext:`.otp`,mimeType:`application/vnd.oasis.opendocument.presentation-template`},{ext:`.odg`,mimeType:`application/vnd.oasis.opendocument.graphics`},{ext:`.otg`,mimeType:`application/vnd.oasis.opendocument.graphics-template`},{ext:`.odf`,mimeType:`application/vnd.oasis.opendocument.formula`},{ext:`.odc`,mimeType:`application/vnd.oasis.opendocument.chart`},{ext:`.sxc`,mimeType:`application/vnd.sun.xml.calc`},{ext:`.pdf`,mimeType:`application/pdf`},{ext:`.fdf`,mimeType:`application/vnd.fdf`},{ext:``,mimeType:`application/x-ole-storage`},{ext:`.msi`,mimeType:`application/x-ms-installer`},{ext:`.aaf`,mimeType:`application/octet-stream`},{ext:`.msg`,mimeType:`application/vnd.ms-outlook`},{ext:`.xls`,mimeType:`application/vnd.ms-excel`},{ext:`.pub`,mimeType:`application/vnd.ms-publisher`},{ext:`.ppt`,mimeType:`application/vnd.ms-powerpoint`},{ext:`.doc`,mimeType:`application/msword`},{ext:`.ps`,mimeType:`application/postscript`},{ext:`.psd`,mimeType:`image/vnd.adobe.photoshop`},{ext:`.p7s`,mimeType:`application/pkcs7-signature`},{ext:`.ogg`,mimeType:`application/ogg`},{ext:`.oga`,mimeType:`audio/ogg`},{ext:`.ogv`,mimeType:`video/ogg`},{ext:`.png`,mimeType:`image/png`},{ext:`.png`,mimeType:`image/vnd.mozilla.apng`},{ext:`.jpg`,mimeType:`image/jpeg`},{ext:`.jxl`,mimeType:`image/jxl`},{ext:`.jp2`,mimeType:`image/jp2`},{ext:`.jpf`,mimeType:`image/jpx`},{ext:`.jpm`,mimeType:`image/jpm`},{ext:`.jxs`,mimeType:`image/jxs`},{ext:`.gif`,mimeType:`image/gif`},{ext:`.webp`,mimeType:`image/webp`},{ext:`.exe`,mimeType:`application/vnd.microsoft.portable-executable`},{ext:``,mimeType:`application/x-elf`},{ext:``,mimeType:`application/x-object`},{ext:``,mimeType:`application/x-executable`},{ext:`.so`,mimeType:`application/x-sharedlib`},{ext:``,mimeType:`application/x-coredump`},{ext:`.a`,mimeType:`application/x-archive`},{ext:`.deb`,mimeType:`application/vnd.debian.binary-package`},{ext:`.tar`,mimeType:`application/x-tar`},{ext:`.xar`,mimeType:`application/x-xar`},{ext:`.bz2`,mimeType:`application/x-bzip2`},{ext:`.fits`,mimeType:`application/fits`},{ext:`.tiff`,mimeType:`image/tiff`},{ext:`.bmp`,mimeType:`image/bmp`},{ext:`.ico`,mimeType:`image/x-icon`},{ext:`.mp3`,mimeType:`audio/mpeg`},{ext:`.flac`,mimeType:`audio/flac`},{ext:`.midi`,mimeType:`audio/midi`},{ext:`.ape`,mimeType:`audio/ape`},{ext:`.mpc`,mimeType:`audio/musepack`},{ext:`.amr`,mimeType:`audio/amr`},{ext:`.wav`,mimeType:`audio/wav`},{ext:`.aiff`,mimeType:`audio/aiff`},{ext:`.au`,mimeType:`audio/basic`},{ext:`.mpeg`,mimeType:`video/mpeg`},{ext:`.mov`,mimeType:`video/quicktime`},{ext:`.mp4`,mimeType:`video/mp4`},{ext:`.avif`,mimeType:`image/avif`},{ext:`.3gp`,mimeType:`video/3gpp`},{ext:`.3g2`,mimeType:`video/3gpp2`},{ext:`.mp4`,mimeType:`audio/mp4`},{ext:`.mqv`,mimeType:`video/quicktime`},{ext:`.m4a`,mimeType:`audio/x-m4a`},{ext:`.m4v`,mimeType:`video/x-m4v`},{ext:`.heic`,mimeType:`image/heic`},{ext:`.heic`,mimeType:`image/heic-sequence`},{ext:`.heif`,mimeType:`image/heif`},{ext:`.heif`,mimeType:`image/heif-sequence`},{ext:`.mj2`,mimeType:`video/mj2`},{ext:`.dvb`,mimeType:`video/vnd.dvb.file`},{ext:`.webm`,mimeType:`video/webm`},{ext:`.avi`,mimeType:`video/x-msvideo`},{ext:`.flv`,mimeType:`video/x-flv`},{ext:`.mkv`,mimeType:`video/x-matroska`},{ext:`.asf`,mimeType:`video/x-ms-asf`},{ext:`.asf`,mimeType:`video/x-ms-wmv`},{ext:`.asf`,mimeType:`video/asf`},{ext:`.aac`,mimeType:`audio/aac`},{ext:`.voc`,mimeType:`audio/x-unknown`},{ext:`.m3u`,mimeType:`application/vnd.apple.mpegurl`},{ext:`.rmvb`,mimeType:`application/Line truncated
|
||||
`+Ie:null),"html-class":`formatted-date`,className:()=>`formatted-date ${n.short?`short`:`full`}`,onunmount:()=>{r.forEach(e=>e?.unwatch())}},()=>{if(!n.value)return t.span({className:`missing-value`});if(n.short){let e=n.value.split(` `);return[t.span({className:`primary-date`},e[0]),t.span({className:`secondary-date`},e[1])]}return[t.span({className:`primary-date`},app.utils.toLocalDatetime(n.value)),t.span({className:`secondary-date`},n.value)]})},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.refreshButton=function(e={}){let n=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,tooltip:`Refresh`,className:`btn transparent secondary circle rotate-btn`,disabled:!1,onclick:function(e){}}),r=app.utils.extendStore(n,e),i,a=t.button({rid:n.rid,id:()=>n.id,hidden:()=>n.hidden,inert:()=>n.inert,type:`button`,ariaLabel:app.attrs.tooltip(()=>n.tooltip),disabled:()=>n.disabled,className:()=>n.className,onunmount:()=>{clearTimeout(i),r.forEach(e=>e?.unwatch())},onclick:e=>{e.preventDefault(),n.onclick&&n.onclick(e),a.classList.add(`rotate`),a.addEventListener(`animationend`,()=>{a.classList.remove(`rotate`)}),clearTimeout(i),i=setTimeout(()=>{clearTimeout(i),a.classList.remove(`rotate`)},500)}},t.i({className:`ri-refresh-line`,ariaHidden:!0}));return a},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.searchHistoryButton=function(e={}){let n=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,value:void 0,historyKey:`default`,max:15,openInNewTabParam:`filter`,btnClassName:`btn sm pill secondary transparent p-r-5`,onselect:function(e){}}),r=app.utils.extendStore(n,e),i=store({items:app.utils.getLocalHistory(n.historyKey,[])});function a(e){o(e),i.items.unshift(e)}function o(e){app.utils.removeByValue(i.items,e)}let s=`history_dropdown_`+app.utils.randomString();r.push(watch(()=>n.value,e=>{e&&a(e)})),r.push(watch(()=>{i.items.length>n.max&&(i.items=i.items.slice(0,n.max)),app.utils.saveLocalHistory(n.historyKey,i.items)}));let c=t.div({id:s,className:`dropdown sm left nowrap history-searchbar-dropdown`,popover:`hint`,onclick:e=>(e.stopPropagation(),!1)},t.div({className:`block p-5`},t.small({className:`txt-hint`},`Search history`)),()=>i.items?.length?i.items.slice(0,n.max).map(e=>t.button({type:`button`,className:`dropdown-item txt-code`,onclick:()=>{c.hidePopover(),n.onselect?.(e),a(e)},onauxclick:()=>{if(n.openInNewTabParam){a(e),c.hidePopover();let r=app.utils.replaceHashQueryParams({[n.openInNewTabParam]:e},!1);window.open(r,`_blank`)}}},t.span({className:`txt-ellipsis`,title:e,textContent:e}),t.small({role:`button`,className:`remove-btn link-hint m-l-auto p-l-5 p-r-5`,title:`Clear`,onauxclick:e=>(e.stopPropagation(),!1),onclick:n=>(n.stopPropagation(),o(e),!1)},t.i({className:`ri-close-line`,ariaHidden:!0})))):t.div({rid:`no-history`,className:`block p-5`},t.span(null,`Your recent searches will show up here.`)));return t.button({rid:n.rid,id:()=>n.id,hidden:()=>n.hidden,inert:()=>n.inert,type:`button`,title:`Search history`,className:()=>n.btnClassName,"html-popovertarget":s,onunmount:()=>{r?.forEach(e=>e?.unwatch())}},t.i({className:`ri-search-line`,ariaHidden:!0}),t.i({className:`ri-arrow-drop-down-line`,ariaHidden:!0}),c)},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.s3Test=function(e={}){let n=`s3_test_request`,r=store({rid:void 0,config:null,label:`Use S3 storage`,testFilesystem:`storage`}),i=app.utils.extendStore(r,e),a=store({isTesting:!1,testError:null,get hasError(){return!app.utils.isEmpty(a.testError)}}),o,s;function c(e=150){if(!r.config.enabled){clearTimeout(o);return}a.isTesting=!0,clearTimeout(o),o=setTimeout(()=>{l()},e)}async function l(){if(a.isTesting=!0,!r.config.enabled||!r.testFilesystem){a.testError=null,a.isTesting=!1;return}app.pb.cancelRequest(n),clearTimeout(s),s=setTimeout(()=>{app.pb.cancelRequest(n),a.testError=Error(`S3 test connection timeout.`),a.isTesting=!1},3e4);try{await app.pb.props.testS3(r.testFilesystem,{requestKey:n}),a.testError=null,a.isTesting=!1}catch(e){e?.isAbort||(a.testError=e,a.isTesting=!1,clearTimeout(s))}}return i.push(watch(()=>r.testFilesystem&&r.config,()=>c())),t.div({pbEvent:`s3Test`,rid:r.rid,hidden:()=>!r.testFilesystem,className:()=>`label s3-test-label txt-nowrap ${a.hasError?`warning`:`success`}`,ariaDescription:app.attrs.tooltip(()=>a.testError?.data?.message),onunmount:()=>{clearTimeout(s),clearTimeout(o),i.forEach(e=>e?.unwatch())}},()=>a.isTesting?t.span({className:`loader sm`}):a.hasError?[t.i({className:`ri-error-warning-line txt-warning`,ariaHidden:!0}),t.span({className:`txt`},`Failed to establish S3 connection`)]:[t.i({className:`ri-checkbox-circle-line txt-success`,ariaHidden:!0}),t.span({className:`txt`},`S3 connected successfully`)])},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.s3ConfigFields=function(e={}){let n=store({rid:void 0,id
Line truncated
|
||||
<video controls width="300">
|
||||
<source src="${a}" />
|
||||
<p>Download: <a href="${a}" download="${i}">${i}</a>.</p>
|
||||
</video>
|
||||
`)}})}},{type:`menuitem`,text:`Manual embed`,onAction:()=>{tinymce.activeEditor.execCommand(`mceMedia`)}}])}})}var qe=`DIV.P.A.EM.B.STRONG.H1.H2.H3.H4.H5.H6.TABLE.TR.TD.TH.TBODY.THEAD.TFOOT.BR.HR.Q.SUP.SUB.DEL.IMG.OL.UL.LI.CODE`.split(`.`);function Je(e){if(e){for(let n of e.children)Je(n);qe.includes(e.tagName)?(e.removeAttribute(`style`),e.removeAttribute(`class`)):Ye(e)}}function Ye(e){let n=e.parentNode;for(;e.firstChild;)n.insertBefore(e.firstChild,e);n.removeChild(e)}async function Xe(){if(window.tinymce!==void 0)return;let e=`lazy-tinymce-js`;return document.getElementById(e)?new Promise((e,n)=>{function r(){document.removeEventListener(`tinymceLoadSuccess`,i),document.removeEventListener(`tinymceLoadError`,a)}let i=function(){r(),e()},a=function(e){r(),n(e?.details)};document.addEventListener(`tinymceLoadSuccess`,i),document.addEventListener(`tinymceLoadError`,a)}):new Promise((n,r)=>{document.head.querySelector(`#shablon-script`).after(t.script({id:e,src:`./libs/tinymce/tinymce.min.js`,onload:()=>{n()},onerror:e=>{console.warn(`failed to load tinymce.min.js:`,e),r(e)}}))}).then(()=>{document.dispatchEvent(new CustomEvent(`tinymceLoadSuccess`))}).catch(e=>{document.dispatchEvent(new CustomEvent(`tinymceLoadError`,{detail:e}))})}window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.colorPicker=function(e={}){let n=`picker_`+app.utils.randomString(),r=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,className:``,name:``,required:!1,disabled:!1,value:``,predefinedColors:[],onchange:e=>{},onmount:e=>{},onunmount:e=>{}}),i=app.utils.extendStore(r,e),a=store({inputValue:`#ffffff`});function o(e){e=e?.toLowerCase()||``,r.onchange?.(e)}let s,c=t.input({type:`color`,className:`color-picker-input`,id:()=>r.id,name:()=>r.name,required:()=>r.required,disabled:()=>r.disabled,value:()=>(a.inputValue=r.value||`#ffffff`,r.value||void 0),oninput:e=>{a.inputValue=e.target.value,clearTimeout(s),s=setTimeout(()=>{o(e.target.value)},50)}});return t.div({rid:r.rid,hidden:()=>r.hidden,inert:()=>r.inert,className:()=>`color-picker ${r.className}`,onmount:e=>{r.onmount?.(e)},onunmount:e=>{clearTimeout(s),r.onunmount?.(e),i.forEach(e=>e?.unwatch())}},t.div({className:`color-picker-input-wrapper`},c,t.output({className:`result`,style:()=>`color: lch(from ${a.inputValue||`#ffffff`} calc((49 - l) * infinity) 0 0);`,textContent:()=>a.inputValue})),t.button({hidden:()=>!r.predefinedColors.length,type:`button`,title:`Predefined colors`,className:`link-hint predefined-colors-btn`,"html-popovertarget":n+`predefined-colors-dropdown`},t.i({className:`ri-arrow-down-s-line`,ariaHidden:!0})),t.div({pbEvent:`predefinedColorsDropdown`,id:n+`predefined-colors-dropdown`,className:`dropdown predefined-colors-dropdown`,popover:`auto`},t.div({className:`predefined-colors-list`},()=>r.predefinedColors?.map(e=>t.button({type:`button`,className:()=>`color ${r.value==e?`active`:``}`,style:`background:${e}`,onclick:n=>{c&&(n.target.closest(`.dropdown`)?.hidePopover(),c.value=e||void 0,c.dispatchEvent(new Event(`input`,{bubbles:!0})))}})))))},window.app=window.app||{},window.app.components=window.app.components||{},window.app.components.ruleField=function(e={}){let n=`rule_`+app.utils.randomString(),r=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,className:``,required:!1,disabled:!1,name:void 0,label:void 0,help:void 0,value:null,nullable:!0,placeholder:`Leave empty to grant everyone access...`,autocomplete:e=>[],oninput:e=>{},onmount:e=>{},onunmount:e=>{},get isLocked(){return r.value==null}}),i=app.utils.extendStore(r,e,`isLocked`),a,o=``;function s(e){r.value=e,r.oninput?.(e),a?.dispatchEvent(new CustomEvent(`change`,{detail:e}))}function c(){r.value!==null&&(o=r.value,s(null))}function l(){s(o??``),setTimeout(()=>{document.getElementById(n)?.focus()},0)}return a=t.div({rid:r.rid,id:()=>r.id,hidden:()=>r.hidden,inert:()=>r.inert,"html-name":()=>r.name,className:()=>[`field`,`rule-field`,r.required?`required`:null,r.value===null?`locked`:null,r.disabled?`disabled`:null].filter(Boolean).join(` `),onmount:e=>{r.onmount?.(e)},onunmount:e=>{r.onunmount?.(e),i.forEach(e=>e?.unwatch())}},t.label({htmlFor:n},e=>r.label?typeof r.label==`function`?r.label(e):typeof r.label==`string`?t.span({className:`txt`},r.label):r.label:t.span({className:`txt`},`Rule`),t.span({hidden:()=>!r.isLocked,className:`txt superusers-label`},`(Superusers only)`)),e=>r.isLocked?t.button({type:`button`,className:`unlock-overlay`,disabled:()=>r.disabled,onclick:l},t.span({className:`txt`},`Unlock and set custom rule`),t.i({className:`ri-lock-unlock-line`,ariaHidden:!0})):[app.components.codeEditor({id:n,language:`pbrule`,required:()=>r.required,disabled:()=>r.disabled,value:()=>r.value,oninput:s,placeholder:()=>r.placeholder,autocomplete:r.autocomplete,autocompleteContainer:e}),t.button({hidden:()=>!r.nullable,type:`button`,className:`superuser-toggle`,disabled:()=>r.disabled,onclick:c},t.
Line truncated
|
||||
<defs>
|
||||
<marker id="${s}_arrow1" class="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto" fill="context-stroke">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
</defs>
|
||||
<g class="paths-group" marker-end="url(#${s}_arrow1)"></g>
|
||||
`;let h=m.querySelector(`.paths-group`),g=document.createElementNS(`http://www.w3.org/2000/svg`,`svg`);g.classList.add(`erd-paths`,`front`),g.innerHTML=`
|
||||
<defs>
|
||||
<marker id="${s}_arrow2" class="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto" fill="context-stroke">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" />
|
||||
</marker>
|
||||
</defs>
|
||||
<g class="paths-group" marker-end="url(#${s}_arrow2)"></g>
|
||||
`;let _=g.querySelector(`.paths-group`);return a=t.div({tabIndex:-1,rid:n.rid,id:()=>n.id,hidden:()=>n.hidden,inert:()=>n.inert,className:()=>`erd ${n.className} ${i.isUpdating?`updating`:``} ${i.isPanning?`panning`:``} ${i.activeCollection?`active`:``}`,onkeydown:e=>{(e.ctrlKey||e.metaKey)&&e.key==`0`&&f()},onmount:async e=>{e.addEventListener(`wheel`,r=>{r.preventDefault();let a=e.getBoundingClientRect(),o=r.clientX-a.left,s=r.clientY-a.top,c=(o-i.viewX)/n.scale,l=(s-i.viewY)/n.scale,u=Math.min(Math.max(-r.deltaY*.001+n.scale,$e),Qe);i.viewX=o-c*u,i.viewY=s-l*u,n.scale=u}),e.addEventListener(`pointerdown`,e=>{e.buttons==1&&(i.isPanning=!0,i.panStartX=e.clientX-i.viewX,i.panStartY=e.clientY-i.viewY)}),e._ondragging=function(e){i.isPanning&&(i.viewX=e.clientX-i.panStartX,i.viewY=e.clientY-i.panStartY)},e._ondragstop=function(){i.isPanning=!1},window.addEventListener(`pointermove`,e._ondragging),window.addEventListener(`pointerup`,e._ondragstop),c()},onunmount:e=>{window.removeEventListener(`pointermove`,e._ondragging),window.removeEventListener(`pointerup`,e._ondragstop),r.forEach(e=>e?.unwatch())}},t.div({className:`erd-area`,style:()=>`transform: translate(${i.viewX}px, ${i.viewY}px) scale(${n.scale});`},m,t.div({className:`erd-tables`},()=>n.collections.map(e=>t.div({style:()=>`left:${i.positions[e.id]?.x||0}px;top:${i.positions[e.id]?.y||0}px`,className:()=>`erd-table type-${e.type} ${p(e)?`active`:``} ${e.system?`system`:``}`,"html-data-collection-id":()=>e.id,"html-data-collection-name":()=>e.name,onmouseenter:()=>{h.querySelectorAll(`[data-to="${e.id}"]`)?.forEach(e=>{e.classList.add(`active-to`),_.append(e)}),h.querySelectorAll(`[data-from="${e.id}"]`)?.forEach(e=>{e.classList.add(`active-from`),_.append(e)}),i.activeCollection=e},onmouseleave:()=>{for(let e of _.children)e.classList.remove(`active-from`,`active-to`);h.append(..._.children),i.activeCollection=null}},t.div({className:`erd-table-row header`},()=>e.name),()=>e.fields?.map(e=>t.div({className:`erd-table-row type-${e.type} ${e.primaryKey?`primary-key`:``}`,"html-data-field-id":()=>e.id,"html-data-field-name":()=>e.name},t.i({ariaHidden:!0,title:()=>e.type,className:()=>`field-icon ${app.fieldTypes[e.type].icon||app.utils.fallbackFieldIcon}`}),t.span({className:`field-name`},()=>e.name),()=>{if(e.hidden)return t.span({className:`label danger field-hidden-label`},`Hidden`)},()=>{if(e.maxSelect!==void 0)return t.span({className:`meta`},e.maxSelect>1?`multiple`:`single`)}))))),g),t.nav({className:`erd-nav`,onmousedown:e=>{e.stopImmediatePropagation()}},t.button({type:`button`,className:`btn sm circle secondary`,title:`Zoom in`,onclick:()=>{n.scale+=.05}},t.i({className:`ri-add-line`,ariaHidden:!0})),t.button({type:`button`,className:`btn sm circle secondary`,title:`Zoom out`,onclick:()=>{n.scale-=.05}},t.i({className:`ri-subtract-line`,ariaHidden:!0})))),a};function tt(e,n,r=1,i=``,a=``,o=2){if(!e||!n)return;let s=e.closest(`.erd-area`).getBoundingClientRect(),c=e.getBoundingClientRect(),l=n.getBoundingClientRect();o*=r;let u=c.top-s.top+c.height/2,d=l.top-s.top+l.height/2,f,p;c.left<l.left?(f=c.left-s.left+c.width+o,p=l.left-s.left-o):c.left>l.left?(f=c.left-s.left-o,p=l.left-s.left+l.width+o):(f=c.left-s.left-o,p=l.left-s.left-o),f/=r,p/=r,u/=r,d/=r;let m=f+(p-f)/2;f==p&&(m-=20);let h=`M ${f} ${u}
|
||||
L ${m} ${u}
|
||||
L ${m} ${d}
|
||||
L ${p} ${d}`,g=document.createElementNS(`http://www.w3.org/2000/svg`,`path`);return g.setAttribute(`class`,`relation-path`),g.setAttribute(`data-from`,i||``),g.setAttribute(`data-to`,a||``),g.setAttribute(`d`,h),g}window.app=window.app||{},window.app.components=window.app.components||{};var nt=1e3;window.app.components.pageSidebar=function(e={},...n){let r,i=store({rid:void 0,id:void 0,hidden:void 0,inert:void 0,className:``,widthHistoryKey:`pbPageSidebarWidth`,onmount:void 0,onunmount:void 0}),a=app.utils.extendStore(i,e),o=store({responsiveShow:!1}),s;function c(){if(r){if(window.innerWidth>nt){o.responsiveShow=!1,r.dataset.responsive=!1,s?.remove(),s=null;return}r.dataset.responsive=!0,s||(s=t.button({type:`button`,className:`btn transparent secondary responsive-sidebar-btn`,title:`Toggle sidebar`,onclick:e=>{e.stopPropagation(),o.responsiveShow=!o.responsiveShow}},t.i({className:`ri-menu-2-line`,ariaHidden:!0})),document.body.querySelector(`.page-header .breadcrumbs`).before(s))}}function l(e){if(e.target.closest(`.responsive-close`)){o.responsiveShow=!1;return}if(!(e.target.closest(`.page-sidebar`)||e.target.closest(`.app-header`)||e.target.closest(`.modal`)))return e.preventDefault(),e.stopImmediatePropagation(),o.responsiveShow=!1,!1}return a.push(watch(()=>o.responsiveShow,e=>{e?window.addEventListener(`click`,l,!0):window.removeEventListener(`click`,l,!0)})),r=t.aside({pbEvent:`pageSidebar`,rid:i.rid,id:()=>i.id,hidden:()=>i.hidden,inert:()=>i.inert,className:()=>`page-sidebar ${i.className} ${o.responsiveShow?`active`:``}`,onmount:e=>{c(e),window.addEventListener(`resize`,c),i.onmount?.(e)},onunmount:e=>{i.onunmount?.(e),window.removeEventListener(`click`,l,!0),window.removeEventListener(`resize`,c),s?.remove(),a.forEach(e=>e?.unwatch())}},e=>{let n;return i.widthHistoryKey&&(n=localStorage.getItem(i.widthHistoryKey),n&&(e.style.width=n)),app.components.dragline({ondragstart:n=>{e._startWidth=e.offsetWidth},ondragging:(r,a,o)=>{n=e._startWidth+a+`px`,e.style.width=n,i.widthHistoryKey&&localStorage.setItem(i.widthHistoryKey,n)}})},...n),r};var N=`modulepreload`,rt=function(e,n){return new URL(e,n).href},it={},P=function(e,n,r){let i=Promise.resolve();if(n&&n.length>0){let e=document.getElementsByTagName(`link`),a=document.querySelector(`meta[property=csp-nonce]`),o=a?.nonce||a?.getAttribute(`nonce`);function s(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}i=s(n.map(n=>{if(n=rt(n,r),n in it)return;it[n]=!0;let i=n.endsWith(`.css`),a=i?`[rel="stylesheet"]`:``;if(r)for(let r=e.length-1;r>=0;r--){let a=e[r];if(a.href===n&&(!i||a.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${n}"]${a}`))return;let s=document.createElement(`link`);if(s.rel=i?`stylesheet`:N,i||(s.as=`script`),s.crossOrigin=``,s.href=n,o&&s.setAttribute(`nonce`,o),document.head.appendChild(s),i)return new Promise((e,r)=>{s.addEventListener(`load`,e),s.addEventListener(`error`,()=>r(Error(`Unable to preload CSS for ${n}`)))})}))}function a(e){let n=new Event(`vite:preloadError`,{cancelable:!0});if(n.payload=e,window.dispatchEvent(n),!n.defaultPrevented)throw e}return i.then(n=>{for(let e of n||[])e.status===`rejected`&&a(e.reason);return e().catch(a)})};window.app=window.app||{},window.app.modals=window.app.modals||{},window.app.modals.openApiPreview=function(e,n={onbeforeopen:null,onafteropen:null,onbeforeclose:null,onafterclose:null}){let r=at(e,n);r&&(document.body.appendChild(r),app.modals.open(r))};function at(e,n){if(!e){console.warn(`[apiPreviewModal] missing required collection`);return}let r,i=store({activeTab:`List/Search`,tabEl:null,isLoading:!1}),a={"List/Search":async e=>{let{docsList:n}=await P(async()=>{let{docsList:e}=await import(`./docsList-BAfVNUIM.js`);return{docsList:e}},__vite__mapDeps([0,1,2,3]),import.meta.url);return i.tabEl=n(e)},View:async e=>{let{docsView:n}=await P(async()=>{let{docsView:e}=await import(`./docsView-Be6Uhvcf.js`);return{docsView:e}},__vite__mapDeps([4,1,2]),import.meta.url);return i.tabEl=n(e)}};e.type!=`view`&&(a.Create=async e=>{let{docsCreate:n}=await P(async()=>{let{docsCreate:e}=await import(`./docsCreate-Be3S3y5K.js`);return{docsCreate:e}},__vite__mapDeps([5,1,2]),import.meta.url);return i.tabEl=n(e)},a.Update=async e=>{let{docsUpdate:n}=await P(async()=>{let{docsUpdate:e}=await import(`./docsUpdate-LtD8DbWI.js`);return{docsUpdate:e}},__vite__mapDeps([6,5,1,2]),import.meta.url);return i.tabEl=n(e)},a.Delete=async e=>{let{docsDelete:n}=await P(async()=>{let{docsDelete:e}=await import(`./docsDelete-CybOn5jy.js`);return{docsDelete:e}},[],import.meta.url);return i.tabEl=n(e)},a.Realtime=async e=>{let{docsRealtime:n}=await P(async()=>{let{docsRealtime:e}=await import(`./docsRealtime-PMESvmJN.js`);return{docsRealtime:e}},[],import.meta.url);return i.tabEl=n(e)},a.Batch=async e=>{let{docsBatch:n}=await P(async()=>{let{docsBatch:e}=await import(`./docsBatch-DNJl1NTn.js`);return{docsBatc
Line truncated
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${i}');
|
||||
|
||||
// load the token into the store
|
||||
const token = '...';
|
||||
pb.authStore.save(token, null);
|
||||
`,footnote:t.div({className:`txt-right`},t.a({href:`https://github.com/pocketbase/js-sdk`,target:`_blank`,rel:`noopener noreferrer`,textContent:`JS SDK docs`}))},{title:`Dart SDK`,language:`dart`,value:`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${i}');
|
||||
|
||||
// load the token into the store
|
||||
final token = '...';
|
||||
pb.authStore.save(token, null);
|
||||
`,footnote:t.div({className:`txt-right`},t.a({href:`https://github.com/pocketbase/dart-sdk`,target:`_blank`,rel:`noopener noreferrer`,textContent:`Dart SDK docs`}))}]})),t.footer({className:`modal-footer`},t.button({type:`button`,className:`btn transparent m-r-auto`,disabled:()=>r.isLoading,onclick:()=>app.modals.close()},t.span({className:`txt`},`Close`)),t.button({hidden:()=>r.token,type:`submit`,"html-form":n+`_form`,className:()=>`btn expanded-lg ${r.isLoading?`loading`:``}`,disabled:()=>r.isLoading},t.span({className:`txt`},`Generate token`)),t.button({hidden:()=>!r.token,type:`button`,className:()=>`btn secondary expanded-lg ${r.isLoading?`loading`:``}`,onclick:()=>o()},t.span({className:`txt`},`Generate new one`))))}window.app=window.app||{},window.app.modals=window.app.modals||{},window.app.modals.openRecordUpsert=function(e,n=null,r={onbeforeopen:null,onafteropen:null,onbeforeclose:null,onafterclose:null,onsave:null,ondelete:null,onduplicate:null,ontokensreset:null,onpasswordresetsend:null,onverificationsend:null}){app.store.errors=null;let i=At(e,n,r);i&&(document.body.appendChild(i),app.modals.open(i))};var Et=[`expand`];function Dt(e,n=Et){if(n.find(n=>e[n]!==void 0)){e=Object.assign({},e);for(let r of n)delete e[r]}return e}function Ot(e){e=Dt(e),app.utils.copyToClipboard(JSON.stringify(e,null,2)),app.toasts.success(`Record copied to clipboard!`)}function kt(e){return e?JSON.stringify(e):``}var z=`main`,B=`authProviders`;function At(e,n,r){if(!e?.id){console.warn(`[recordUpsertModal] missing required collection`);return}let i,a=`record_upsert_`+app.utils.randomString(),o=app.utils.getLocalHistory(app.consts.COLUMNS_STORAGE_PREFIX+e.id,{}),s=store({isLoading:!0,isSaving:!1,isLocked:!1,originalRecord:{},record:{},initialDraft:null,activeTab:z,get isNew(){return app.utils.isEmpty(s.originalRecord?.id)},get isAuthCollection(){return e.type==`auth`},get isSuperusersCollection(){return e.name==`_superusers`},get showTabs(){return!s.isNew&&s.isAuthCollection&&!s.isSuperusersCollection},get excludedFields(){let e=[`id`];return s.isAuthCollection&&e.push(`email`,`emailVisibility`,`verified`,`password`,`tokenKey`),e},get initialDraftHash(){return kt(s.initialDraft)},get recordHash(){return kt(s.record)},get originalRecordHash(){return kt(s.originalRecord)},get hasChanges(){return s.originalRecordHash!=s.recordHash},get isFormDisabled(){return s.isLoading||s.isSaving||!s.isNew&&!s.hasChanges}});function c(){return`draft_`+e.id+`_`+(s.originalRecord?.id||``)}function l(){return window.localStorage.getItem(c())||``}function u(){try{let e=l();if(e)return JSON.parse(e)}catch(e){console.warn(`getDraft failure:`,e),f()}return null}function d(e){try{window.localStorage.setItem(c(),e)}catch(e){console.warn(`saveDraft failure:`,e),f()}}function f(){window.localStorage.removeItem(c()),s.initialDraft=null}function p(){if(!s.initialDraft)return;app.store.errors&&(app.store.errors=null);let e=JSON.parse(JSON.stringify(s.initialDraft));f(),s.record=e}async function m(n){s.isLoading=!0;let r=typeof n==`string`?n:n?.id;if(!r){let e=app.utils.isObject(n)?JSON.parse(JSON.stringify(n)):{};s.originalRecord=app.utils.emptyClone(e,[`collectionId`,`collectionName`]),s.initialDraft=u(),s.record=e,s.isLoading=!1,s.isLocked=!1;return}s.isLocked=!!app.store.settings?.meta?.hideControls;try{s.originalRecord={id:r};let n=await app.pb.collection(e.name).getOne(r,{requestKey:`upsert_load_`+r});s.originalRecord=n,s.initialDraft=u(),s.record=JSON.parse(JSON.stringify(n)),s.isLoading=!1}catch(e){e?.isAbort||(app.checkApiError(e),s.isLoading=!1,setTimeout(()=>app.modals.close(i,!0),0))}}async function h(){let n={};for(let e in s.record){if(e==`expand`||e.startsWith(`@@`))continue;let r=s.record[e]?.__raw||s.record[e];r===void 0&&(r=null),n[e]=r}for(let r of e.fields){let i=app.fieldTypes[r.type]?.onrecordsave;i&&await i({collection:e,originalRecord:s.originalRecord,record:s.record,field:r,payload:n})}return n}async function g(n=!0){if(!(s.isLocked||s.isSaving||!s.isNew&&!s.hasChanges)){s.isSaving=!0;try{let a=await h(),o=app.utils.isEmpty(s.originalRecord?.id),c;c=o?await app.pb.collection(e.name).create(a):await app.pb.collection(e.name).update(s.originalRecord.id,a),f(),o?(s.originalRecord=structuredClone(c),s.record=structuredClone(c)):(Object.assign(s.originalRecord,structuredClone(c)),Object.assign(s.record,structuredClone(c))),r.onsave?.(structuredClone(c),o),app.store.errors=null;let l;l=o?`Successfully created ${e.name} "${c.id}".`:`Successfully updated ${e.name} "${c.id}".`,app.toasts.success(l,{key:`recordSave`}),s.isSaving=!1,n&&app.modals.close(i,!0)}catch(e){e?.isAbort||(s.isSaving=!1,app.checkApiError(e,!1),app.toasts.error(e.message||`Failed to save record.`,{key:`recordSave`}))}}}function _(){f(),s.record=JSON.parse(JSON.stringify(s.originalRecord))}async function v(){let n=s.originalRecord?JSON.parse(JSON.stringify(s.originalRecord)):{};n.id=``;for(let r of e.fields){let i=app.fieldTypes[r.type]?.o
Line truncated
|
||||
The field cannot be presentable if hidden.`),n})})))},()=>{if(i.showHidden)return t.div({className:`field prop-hidden`},t.input({type:`checkbox`,id:r+`.hidden`,className:`sm`,name:()=>`fields.${e.fieldIndex}.hidden`,checked:()=>!!e.field.hidden,onchange:n=>e.field.hidden=n.target.checked}),t.label({htmlFor:r+`.hidden`},t.span({className:`txt`},`Hidden`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Hide from the JSON API response and filters.`)})))},t.button({hidden:()=>!i.showDuplicate&&(!i.showRemove||e.field.system),type:`button`,title:`More options`,className:`btn sm circle transparent secondary more-btn m-l-auto`,"html-popovertarget":r+`_options_dropdown`},t.i({className:`ri-more-line`,ariaHidden:!0})),t.div({id:r+`_options_dropdown`,className:`dropdown sm field-options-dropdown`,popover:`auto`},()=>{if(i.showDuplicate)return t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,textContent:`Duplicate`,onclick:e=>{o(),e.target.closest(`.dropdown`).hidePopover()}})},()=>{if(!(!i.showRemove||e.field.system))return t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,textContent:`Remove`,onclick:e=>{s(),e.target.closest(`.dropdown`).hidePopover(),e.target.closest(`details`).open=!1}})})))};function G(e,n=`field`){let r=n,i=2,a=n.match(/\d+$/)?.[0]||``,o=a?n.substring(0,n.length-a.length):n;for(;e?.find(e=>e.name.toLowerCase()==r.toLowerCase());)r=o+((a<<0)+i),i++;return r}function Yt(e,n,r){if(!r||n===void 0||n===r||!e?.passwordAuth?.identityFields?.length)return;let i=e.passwordAuth.identityFields;for(let e=0;e<i.length;e++)i[e]==n&&(i[e]=r)}function Xt(e,n,r){!r||n===void 0||n===r||!e?.indexes?.length||!e?.fields?.length||e.fields.find(e=>!e[`@toDelete`]&&e.name==n)||(e.indexes=e.indexes.map(e=>app.utils.replaceIndexColumn(e,n,r)))}function Zt(e){let n=`text_`+app.utils.randomString(),r=store({get hasAutogenerate(){return!app.utils.isEmpty(e.field.autogeneratePattern)&&app.utils.isEmpty(e.originalRecord?.id)},get isDisabled(){return!app.utils.isEmpty(e.originalRecord?.id)&&e.field.primaryKey},get isRequired(){return e.field.required&&!r.hasAutogenerate&&!r.isDisabled}});return t.div({className:`record-field-input field-type-text`},t.div({className:`fields`},t.div({className:`field`},t.label({htmlFor:n},t.i({ariaHidden:!0,className:()=>e.field.primaryKey?`ri-key-line`:app.fieldTypes.text.icon}),t.span({className:`txt`},()=>e.field.name)),t.div({className:`autoexpand-wrapper`},t.textarea({id:n,rows:1,name:()=>e.field.name,required:()=>r.isRequired,disabled:()=>r.isDisabled,placeholder:()=>r.hasAutogenerate?`Leave empty to autogenerate...`:``,value:()=>e.record[e.field.name]||``,oninput:n=>e.record[e.field.name]=n.target.value||``}),t.div({className:`input`},()=>e.record[e.field.name],` `))),()=>{if(!e.field.primaryKey||!e.originalRecord?.id)return;let n=e.collection?.fields?.filter(e=>e.type==`autodate`)||[];if(!n.length)return;let r=[];for(let i of n)r.push(`${i.name}: ${app.utils.displayValue(e.record[i.name])}`);return t.div({className:`field addon`},t.i({className:`ri-information-line txt-hint link-faded`,ariaDescription:app.attrs.tooltip(r.join(`
|
||||
`),`left`)}))}),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function Qt(e){let n=`f_`+app.utils.randomString();return app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.min`},t.span({className:`txt`},`Min length`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Clear the field or set it to 0 for no limit.`)})),t.input({type:`number`,id:n+`.min`,name:()=>`fields.${e.fieldIndex}.min`,step:1,min:0,max:2**53-1,placeholder:`No min limit`,value:()=>e.field.min||``,oninput:n=>{e.field.min=parseInt(n.target.value,10)}}))),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.max`},t.span({className:`txt`},`Max length`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Clear the field or set it to 0 to fallback to the default limit.`)})),t.input({type:`number`,id:n+`.max`,name:()=>`fields.${e.fieldIndex}.max`,step:1,min:()=>e.field.min||0,max:2**53-1,placeholder:`Default to max 5000 characters`,value:()=>e.field.max||``,oninput:n=>{e.field.max=parseInt(n.target.value,10)}}))),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.pattern`},t.span({className:`txt`},`Validation pattern`),()=>{if(e.field.primaryKey)return t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`All record ids have forbidden characters and unique case-insensitive (ASCII) validations in addition to the user defined regex pattern.`)})}),t.input({type:`text`,id:n+`.pattern`,name:()=>`fields.${e.fieldIndex}.pattern`,value:()=>e.field.pattern||``,oninput:n=>e.field.pattern=n.target.value})),t.div({className:`field-help`},`Ex. `,t.code(null,`^[a-z0-9]+$`))),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.autogeneratePattern`},t.span({className:`txt`},`Autogenerate pattern`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Set and autogenerate text matching the pattern on missing record create value.`)})),t.input({type:`text`,id:n+`.autogeneratePattern`,name:()=>`fields.${e.fieldIndex}.autogeneratePattern`,value:()=>e.field.autogeneratePattern||``,oninput:n=>e.field.autogeneratePattern=n.target.value})),t.div({className:`field-help`},`Ex. `,t.code(null,`[a-z0-9]{30}`))),t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`},`(!='')`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Requires the field value to be nonempty string`)})))]})}function $t(e){return t.div({className:`record-field-view field-type-text`},()=>{let n=e.record[e.field.name]||``;if(n==``)return t.span({className:`missing-value`});if(e.field.primaryKey){let r=null;return e.record?.collectionName==`_superusers`&&app.store.superuser?.id==e.record?.id&&(r=t.strong({className:`txt`,textContent:` (you)`})),t.span({className:`label`},app.components.copyButton(n),t.span({className:`txt-ellipsis`},app.utils.truncate(n),r))}return e.short?t.span({className:`txt txt-ellipsis`,textContent:app.utils.truncate(n)}):n})}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.text={icon:`ri-text`,label:`Plain text`,settings:Qt,input:Zt,view:$t,filterModifiers:e=>[`lower`],dummyData:(e,n=!1)=>e.primaryKey?app.utils.randomString(15):`example text`};function en(e){let n=`editor_`+app.utils.randomString(),r=store({lazyEditor:null});return t.div({className:`record-field-input field-type-editor large-modal`,onmount:()=>{requestAnimationFrame(()=>{r.lazyEditor=app.components.tinymce({id:n,required:()=>e.field.required,convertURLs:()=>e.field.convertURLs,name:()=>e.field.name,value:()=>e.record[e.field.name]||``,onchange:n=>{e.record[e.field.name]=n}})})}},t.div({className:`field`},t.label({htmlFor:n},t.i({className:app.fieldTypes.editor.icon,ariaHidden:!0}),t.span({className:`txt`},()=>e.field.name)),()=>r.lazyEditor),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function tn(e){let n=`f_`+app.utils.randomString();return store({showInfo:!1}),app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.maxSize`},t.span(null,`Max size `),t.small(null,`(bytes)`)),t.input({type:`number`,id:n+`.maxSize`,name:()=>`fields.${e.fieldIndex}.maxSize`,min:0,step:1,max:2**53-1,placeholder:`Default to max ~5MB`,val
Line truncated
|
||||
This field is disabled if "Only domains" is set.`)})),t.input({type:`text`,id:n+`.exceptDomains`,disabled:()=>!app.utils.isEmpty(e.field.onlyDomains),name:()=>`fields.${e.fieldIndex}.exceptDomains`,value:()=>app.utils.joinNonEmpty(e.field.exceptDomains),onchange:n=>e.field.exceptDomains=app.utils.splitNonEmpty(n.target.value,`,`)})),t.div({className:`field-help`},`Use comma as separator.`)),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.onlyDomains`},t.span({className:`txt`},`Only domains`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`List of domains that are ONLY allowed.
|
||||
This field is disabled if "Except domains" is set.`)})),t.input({type:`text`,id:n+`.onlyDomains`,disabled:()=>!app.utils.isEmpty(e.field.exceptDomains),name:()=>`fields.${e.fieldIndex}.onlyDomains`,value:()=>app.utils.joinNonEmpty(e.field.onlyDomains),onchange:n=>e.field.onlyDomains=app.utils.splitNonEmpty(n.target.value,`,`)})),t.div({className:`field-help`},`Use comma as separator.`)),t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`},`(!='')`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Requires the field value to be nonempty string`)})))]})}function q(e){return t.div({className:`record-field-view field-type-email`},()=>{let n=e.record[e.field.name]||``;return n?e.short?t.span({className:`txt txt-ellipsis`,textContent:app.utils.truncate(n)}):n:t.span({className:`missing-value`})})}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.email={icon:`ri-mail-line`,label:`Email`,settings:un,input:ln,view:q,filterModifiers:e=>[`lower`],dummyData:(e,n=!1)=>`test_${app.utils.randomString(3,`123567890`)}@example.com`};function dn(e){let n=`url_`+app.utils.randomString();return t.div({className:`record-field-input field-type-url`},t.div({className:`field`},t.label({htmlFor:n},t.i({className:app.fieldTypes.url.icon,ariaHidden:!0}),t.span({className:`txt`},()=>e.field.name)),t.input({type:`url`,id:n,spellcheck:!1,name:()=>e.field.name,required:()=>e.field.required,value:()=>e.record[e.field.name]||``,oninput:n=>e.record[e.field.name]=n.target.value})),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function J(e){let n=`f_`+app.utils.randomString();return app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.exceptDomains`},t.span({className:`txt`},`Except domains`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`List of domains that are NOT allowed.
|
||||
This field is disabled if "Only domains" is set.`)})),t.input({type:`text`,id:n+`.exceptDomains`,disabled:()=>!app.utils.isEmpty(e.field.onlyDomains),name:()=>`fields.${e.fieldIndex}.exceptDomains`,value:()=>app.utils.joinNonEmpty(e.field.exceptDomains),onchange:n=>e.field.exceptDomains=app.utils.splitNonEmpty(n.target.value,`,`)})),t.div({className:`field-help`},`Use comma as separator.`)),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.onlyDomains`},t.span({className:`txt`},`Only domains`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`List of domains that are ONLY allowed.
|
||||
This field is disabled if "Except domains" is set.`)})),t.input({type:`text`,id:n+`.onlyDomains`,disabled:()=>!app.utils.isEmpty(e.field.exceptDomains),name:()=>`fields.${e.fieldIndex}.onlyDomains`,value:()=>app.utils.joinNonEmpty(e.field.onlyDomains),onchange:n=>e.field.onlyDomains=app.utils.splitNonEmpty(n.target.value,`,`)})),t.div({className:`field-help`},`Use comma as separator.`)),t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`},`(!='')`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Requires the field value to be nonempty string`)})))]})}function fn(e){return t.div({className:`record-field-view field-type-url`},()=>{let n=e.record[e.field.name]||``;return n?t.a({href:()=>n,className:`txt txt-ellipsis`,rel:`noopener noreferrer`,target:`_blank`,textContent:app.utils.truncate(n),ariaDescription:app.attrs.tooltip(`Open in new tab`),onclick:e=>{e.stopPropagation()}}):t.span({className:`missing-value`})})}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.url={icon:`ri-link`,label:`URL`,settings:J,input:dn,view:fn,dummyData:(e,n=!1)=>`https://example.com`};function pn(e){let n=`date_`+app.utils.randomString();return t.div({className:`record-field-input field-type-date`},t.div({className:`field`},t.label({htmlFor:n},t.i({className:app.fieldTypes.date.icon,ariaHidden:!0}),t.span({className:`txt`},()=>e.field.name)),t.input({id:n,step:1,type:`datetime-local`,name:()=>e.field.name,required:()=>e.field.required,value:()=>app.utils.toDatetimeLocalInputValue(e.record[e.field.name]),onchange:n=>{e.record[e.field.name]=app.utils.toRFC3339Datetime(n.target.value)}})),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function mn(e){let n=`f_`+app.utils.randomString();return app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.min`},t.span({className:`txt`},`Min date (Local)`)),t.input({type:`datetime-local`,id:n+`.min`,step:1,name:()=>`fields.${e.fieldIndex}.min`,value:()=>app.utils.toDatetimeLocalInputValue(e.field.min),onchange:n=>{e.field.min=app.utils.toRFC3339Datetime(n.target.value)}}))),t.div({className:`col-sm-6`},t.div({className:`field`},t.label({htmlFor:n+`.max`},t.span({className:`txt`},`Max date (Local)`)),t.input({type:`datetime-local`,id:n+`.max`,step:1,name:()=>`fields.${e.fieldIndex}.max`,value:()=>app.utils.toDatetimeLocalInputValue(e.field.max),onchange:n=>{e.field.max=app.utils.toRFC3339Datetime(n.target.value)}}))),t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`},`(!='')`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`Requires the field value to be nonempty string`)})))]})}function hn(e){return t.div({className:`record-field-view field-type-date`},app.components.formattedDate({value:()=>e.record[e.field.name],short:()=>e.short}))}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.date={icon:`ri-calendar-line`,label:`Datetime`,settings:mn,input:pn,view:hn,dummyData:(e,n=!1)=>new Date().toISOString().replaceAll(`T`,` `)};function gn(e){delete e.clone[e.field.name]}function _n(e){let n=[{label:`Create`,value:1},{label:`Update`,value:2},{label:`Create/Update`,value:3}];function r(e){return e.onCreate&&e.onUpdate?3:e.onUpdate?2:1}function i(n){switch(n){case 1:e.field.onCreate=!0,e.field.onUpdate=!1;break;case 2:e.field.onCreate=!1,e.field.onUpdate=!0;break;case 3:e.field.onCreate=!0,e.field.onUpdate=!0;break}}let a=store({isDropdownOpen:!1});return app.components.fieldSettings(e,{header:t.div({className:`field header-select autodate-select`,ariaDescription:app.attrs.tooltip(`Auto set on`,`left`),onmount:()=>{i(r(e.field))}},app.components.select({required:!0,options:n,disabled:()=>e.originalCollection?.system,value:()=>r(e.field),onchange:e=>i(e?.[0]?.value),ondropdowntoggle:e=>{a.isDropdownOpen=e.newState==`open`}}))})}func
Line truncated
|
||||
Leave empty for no restriction.`)})),app.components.select({max:99,placeholder:`No restriction`,options:app.utils.mimeTypes.map(e=>({value:e.mimeType,label:()=>t.div({className:`inline-flex gap-10`},t.span({className:`txt`},e.ext||`-`),t.small({className:`txt-hint`},e.mimeType))})),name:()=>`fields.${e.fieldIndex}.mimeTypes`,value:()=>app.utils.toArray(e.field.mimeTypes),onchange:n=>e.field.mimeTypes=n.map(e=>e.value)})),t.div({className:`field-help`},t.button({type:`button`,className:`link-hint gap-0`,"html-popovertarget":n+`mimeTypesDropdown`},t.span({className:`txt`},`Choose presets`),t.i({className:`ri-arrow-drop-down-fill`,ariaHidden:!0})),t.div({id:n+`mimeTypesDropdown`,className:`dropdown sm nowrap left p-10`,popover:`auto`},t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,onclick:n=>{e.field.mimeTypes=[`image/jpeg`,`image/png`,`image/svg+xml`,`image/gif`,`image/webp`],n.target.closest(`.dropdown`).hidePopover()},textContent:`Images (jpg, png, svg, gif, webp)`}),t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,onclick:n=>{e.field.mimeTypes=[`application/pdf`,`application/msword`,`application/vnd.openxmlformats-officedocument.wordprocessingml.document`,`application/vnd.ms-excel`,`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`],n.target.closest(`.dropdown`).hidePopover()},textContent:`Documents (pdf, doc/docx, xls/xlsx)`}),t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,onclick:n=>{e.field.mimeTypes=[`video/mp4`,`video/mpeg`,`video/x-msvideo`,`video/quicktime`,`video/3gpp`],n.target.closest(`.dropdown`).hidePopover()},textContent:`Videos (mp4, mpeg, avi, mov, 3gp)`}),t.button({type:`button`,className:`dropdown-item`,role:`menuitem`,onclick:n=>{e.field.mimeTypes=[`application/zip`,`application/x-7z-compressed`,`application/x-rar-compressed`],n.target.closest(`.dropdown`).hidePopover()},textContent:`Archives (zip, 7zip, rar)`})))),t.div({className:()=>e.field.maxSelect>1?`col-sm-6`:`col-sm-9`},t.div({className:`field`},t.label({htmlFor:n+`.thumbs`},t.span({className:`txt`},`Thumb sizes`),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(`List of additional thumb sizes for image files, along with the default thumb size of 100x100. The thumbs are generated lazily on first access.`)})),t.input({type:`text`,id:n+`.thumbs`,placeholder:`e.g. 50x50, 480x720`,name:()=>`fields.${e.fieldIndex}.thumbs`,value:()=>app.utils.joinNonEmpty(e.field.thumbs),onchange:n=>e.field.thumbs=app.utils.splitNonEmpty(n.target.value,`,`)})),t.div({className:`field-help`},t.span({className:`txt m-r-5`},`Use comma as separator.`),t.button({type:`button`,className:`link-hint gap-0`,"html-popovertarget":n+`thumbFormatsDropdown`},t.span({className:`txt`},`Supported formats`),t.i({className:`ri-arrow-drop-down-fill`,ariaHidden:!0})),t.div({id:n+`thumbFormatsDropdown`,className:`dropdown sm nowrap left p-10`,popover:`auto`},t.ul({className:`m-0 p-l-sm`},t.li(null,t.strong(null,`WxH`),t.span(null,` (e.g. 100x50) - crop to WxH viewbox (from center)`)),t.li(null,t.strong(null,`WxHt`),t.span(null,` (e.g. 100x50t) - crop to WxH viewbox (from top)`)),t.li(null,t.strong(null,`WxHb`),t.span(null,` (e.g. 100x50b) - crop to WxH viewbox (from bottom)`)),t.li(null,t.strong(null,`WxHf`),t.span(null,` (e.g. 100x50f) - fit inside a WxH viewbox (without cropping)`)),t.li(null,t.strong(null,`0xH`),t.span(null,` (e.g. 0x50) - resize to H height preserving the aspect ratio`)),t.li(null,t.strong(null,`Wx0`),t.span(null,` (e.g. 100x0) - resize to W width preserving the aspect ratio`)))))),t.div({className:`col-sm-3`},t.div({className:`field`},t.label({htmlFor:n+`.maxSize`},`Max size`),t.input({type:`number`,id:n+`.maxSize`,step:1,min:0,max:2**53-1,placeholder:`~5MB default`,name:()=>`fields.${e.fieldIndex}.maxSize`,value:()=>e.field.maxSize||``,oninput:n=>e.field.maxSize=parseInt(n.target.value,10)})),t.div({className:`field-help`},`In bytes.`)),()=>{if(e.field.maxSelect>1)return t.div({className:`col-sm-3`},t.div({className:`field`},t.label({htmlFor:n+`.maxSelect`},`Max select`),t.input({type:`number`,id:n+`.maxSelect`,placeholder:`Default to single`,step:1,min:2,required:!0,max:2**53-1,name:()=>`fields.${e.fieldIndex}.maxSelect`,value:()=>e.field.maxSelect||``,onchange:e=>{let n=parseInt(e.target.value,10);n>1?props.field.maxSelect=n:props.field.maxSelect=1}})))},t.div({className:`col-sm-12`},t.div({className:`field m-t-5 m-b-5`},t.input({className:`switch`,type:`checkbox`,id:n+`.protected`,name:()=>`fields.${e.fieldIndex}.protected`,checked:()=>!!e.field.protected,onchange:n=>e.field.protected=n.target.checked}),t.label({htmlFor:n+`.protected`},t.span({className:`txt`},`Protected`),t.small({className:`txt-hint`},`Files will require View API rule permissions and file token (`,t.a({href:`https://pocketbase.io/docs/files-handling#protected-files`,target:`_blank`,rel:`noopener noreferrer`,textContent:`Learn more`}),`).`)))),t.div({className:`col-sm-12`},t.di
Line truncated
|
||||
`),oninput:n=>{let r=n.target.value.trimStart().replaceAll(`
|
||||
|
||||
`,`
|
||||
`).split(`
|
||||
`);e.field.values=r,app.utils.deleteByPath(app.store.errors,`fields.${e.fieldIndex}.values`)},onchange:n=>{let r=new Set,i=n.target.value.split(`
|
||||
`);for(let e of i)e!=``&&r.add(e);e.field.values=Array.from(r)},onblur:e=>{(!e.relatedTarget||!i.contains(e.relatedTarget))&&i.hidePopover()}}))),a=[watch(()=>{e.field.values?.length&&e.field.maxSelect>e.field.values.length&&(e.field.maxSelect=e.field.values.length)})];return app.components.fieldSettings(e,{header:[t.div({className:`field header-select field-select-choices-input`,onunmount:()=>{a.forEach(e=>e?.unwatch())}},t.input({type:`text`,placeholder:`Add choices*`,className:`txt-left inline-error`,value:()=>e.field.values?.join(` • `)||``,name:()=>`fields.${e.fieldIndex}.values`,onfocus:e=>(i?.showPopover({source:e.target}),i.querySelector(`textarea`)?.focus(),!1)}),i),t.div({className:`field header-select single-multiple-select`},app.components.select({required:!0,options:r,value:()=>e.field.maxSelect>1,onchange:n=>{n?.[0]?.value?e.field.maxSelect=e.field.values.length||2:e.field.maxSelect=1}}))],content:()=>t.div({className:`grid sm`},()=>{if(e.field.maxSelect>1)return t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.maxSelect`},`Max select`),t.input({type:`number`,id:n+`.maxSelect`,placeholder:`Default to single`,step:1,min:2,max:()=>e.field.values?.length||2,name:()=>`fields.${e.fieldIndex}.maxSelect`,value:()=>e.field.maxSelect||``,onchange:n=>{let r=parseInt(n.target.value,10);r>1?e.field.maxSelect=r:e.field.maxSelect=1}})))},t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.help`},`Help text`),t.input({type:`text`,id:n+`.help`,name:()=>`fields.${e.fieldIndex}.help`,value:()=>e.field.help||``,oninput:n=>e.field.help=n.target.value})))),footer:()=>[t.div({className:`field`},t.input({className:`sm`,type:`checkbox`,id:n+`.required`,name:()=>`fields.${e.fieldIndex}.required`,checked:()=>!!e.field.required,onchange:n=>e.field.required=n.target.checked}),t.label({htmlFor:n+`.required`},t.span({className:`txt`},`Required`),t.small({className:`txt-hint`,textContent:()=>e.field.maxSelect>1?`(!=[])`:`(!='')`}),t.i({className:`ri-information-line link-hint`,ariaDescription:app.attrs.tooltip(()=>`Requires the field value to be nonempty ${e.field.maxSelect>1?`array`:`string`}.`)})))]})}function An(e){return t.div({className:`record-field-view field-type-select`},t.div({className:`inline-flex gap-5`},()=>{let n=app.utils.toArray(e.record[e.field.name],!1);return n.length?n.map(e=>t.span({className:`label`,title:e,textContent:app.utils.truncate(e,100)})):t.span({className:`missing-value`})}))}window.app=window.app||{},window.app.fieldTypes=window.app.fieldTypes||{},window.app.fieldTypes.select={icon:`ri-list-check`,label:`Select`,settings:kn,input:On,view:An,filterModifiers:e=>e.maxSelect>1?[`each`,`length`]:[],dummyData:(e,n=!1)=>e.maxSelect>1?e.values?.slice(0,2)||[]:e.values?.[0]||``};function jn(e){let n=`json_`+app.utils.randomString(),r=store({value:``}),i=[watch(()=>e.record[e.field.name],(n,i)=>{if(!(n!==``&&n===r.value)){if(typeof n==`string`&&!n.startsWith(`"`)&&!n.endsWith(`"`)){r.value=JSON.stringify(n===void 0?null:n),e.record[e.field.name]=r.value;return}typeof n==`string`&&n.startsWith(`"`)&&n.endsWith(`"`)?r.value=n:n===null?r.value=`null`:r.value=JSON.stringify(n===void 0?null:n,null,2)}})];function a(){let n=r.value.trim();if(n===``){e.record[e.field.name]=null;return}try{let r=JSON.parse(n);typeof r==`string`?e.record[e.field.name]=JSON.stringify(r):e.record[e.field.name]=r}catch{e.record[e.field.name]=n}}return t.div({className:`record-field-input field-type-json`},t.div({className:`field`,onunmount:()=>{clearTimeout(void 0),i.forEach(e=>e?.unwatch())}},t.label({htmlFor:n},t.i({className:app.fieldTypes.json.icon,ariaHidden:!0}),t.span({className:`txt`},()=>e.field.name),t.span({hidden:()=>Mn(r.value.trim()),className:`json-state`,ariaDescription:app.attrs.tooltip(`Invalid JSON`,`left`)},t.i({className:`ri-error-warning-fill txt-danger`,ariaHidden:!0})),t.span({hidden:()=>!Mn(r.value.trim()),className:`json-state`,ariaDescription:app.attrs.tooltip(`Valid JSON`,`left`)},t.i({className:`ri-checkbox-circle-fill txt-success`,ariaHidden:!0}))),app.components.codeEditor({language:`js`,id:n,name:()=>e.field.name,required:()=>e.field.required,value:()=>r.value,oninput:e=>r.value=e,onblur:()=>a()})),()=>{if(e.field.help)return t.div({className:`field-help`},e.field.help)})}function Mn(e){if(e===``)return!0;try{return JSON.parse(e),!0}catch{return!1}}function Nn(e){try{let n=e.record[e.field.name];typeof n==`string`&&JSON.parse(n)}catch(r){throw new n({status:400,response:{message:`Invalid JSON data`,data:{[e.field.name]:{code:`invalid_json`,message:r.toString()}}}})}}function Pn(e){let n=`f_`+app.utils.randomString(),r=store({showInfo:!1});return app.components.fieldSettings(e,{content:()=>t.div({className:`grid sm`},t.div({className:`col-sm-12`},t.div({className:`field`},t.label({htmlFor:n+`.maxSize`},t.span(null,`Max size `),t.small(null,`(bytes)`)),t.input({type:`number`,id:n+`.maxSize`,name:()=>`fields.${e.fieldIndex}.maxSize`,min:0,step:1
Line truncated
|
||||
To target the newly submitted ones you can use @request.body.*.`)})],name:`updateRule`,autocomplete:i,disabled:()=>e.originalCollection?.system,value:()=>e.collection.updateRule,oninput:n=>e.collection.updateRule=n})),t.div({className:`col-12`,ariaDescription:r()},app.components.ruleField({label:`Delete rule`,name:`deleteRule`,autocomplete:i,disabled:()=>e.originalCollection?.system,value:()=>e.collection.deleteRule,oninput:n=>e.collection.deleteRule=n}))]}),()=>{if(e.collection.type==`auth`)return[t.hr({className:`m-t-base m-b-base`}),t.button({type:`button`,onmount:()=>{n.showAuthRules=e.collection.manageRule!==null||e.collection.authRule!==``},className:()=>`btn secondary sm ${n.showAuthRules?``:`transparent`}`,onclick:()=>{n.showAuthRules=!n.showAuthRules}},t.span({className:`txt`},`Additional auth collection rules`),t.i({ariaHidden:!0,className:()=>n.showAuthRules?`ri-arrow-drop-up-line`:`ri-arrow-drop-down-line`})),app.components.slide(()=>n.showAuthRules,t.div({className:`grid sm m-t-sm`},t.div({className:`col-12`,ariaDescription:r()},app.components.ruleField({label:`Authentication rule`,name:`authRule`,placeholder:``,autocomplete:i,disabled:()=>e.originalCollection?.system,value:()=>e.collection.authRule,oninput:n=>e.collection.authRule=n}),t.div({className:`field-help`},t.p(null,`This rule is executed every time `,t.strong(null,`before authentication`),` allowing you to restrict who can authenticate.`),t.p(null,`For example, to allow only verified users you can set it to `,t.code(null,`verified = true`),`.`),t.p(null,`Leave it empty to allow anyone with an account to authenticate.`),t.p(null,`To disable authentication entirely you can change it to "Set superusers only".`))),t.div({className:`col-12`,ariaDescription:r()},app.components.ruleField({label:`Manage rule`,name:`manageRule`,autocomplete:i,disabled:()=>e.originalCollection?.system,value:()=>e.collection.manageRule,oninput:n=>e.collection.manageRule=n}),t.div({className:`field-help`},t.p(null,`This rule is executed in addition to the `,t.strong(null,`create`),` and `,t.strong(null,`update`),` API rules.`),t.p(null,`It enables superuser-like permissions to allow fully managing the auth record(s), eg. changing the password without requiring to enter the old one, directly updating the verified state or email, etc.`)))))]})}var Xn=`test_view_query`;function Zn(e){let n=`query_`+app.utils.randomString(),r=[`SELECT`,`FROM`,`WHERE`,`LEFT JOIN`,`INNER JOIN`,`ON`,`GROUP BY`,`HAVING`,`ORDER BY`,`LIMIT`,`OFFSET`,`AS`,`WITH`,`NOT`,`IN`,`EXISTS`,`LIKE`,`CAST`],i=store({testRecords:[],testError:``,isTesting:!1});async function a(n){if(i.isTesting=!0,i.testRecords=[],(app.store.errors?.viewQuery||app.store.errors?.fields)&&(delete app.store.errors.viewQuery,delete app.store.errors.fields),!n){i.testError=``,i.isTesting=!1;return}try{let r=await app.pb.send(`/api/collections/meta/dry-run-view`,{method:`POST`,body:{query:n},requestKey:Xn});e.collection?.id?i.testRecords=r.sample.map(n=>(n.collectionId=e.collection?.id,n.collectionName=e.collection?.name,n)):i.testRecords=r.sample,i.testError=``,i.isTesting=!1}catch(e){e.isAbort||(i.testError=e.message||`Invalid query.`,i.isTesting=!1)}}let o,s=[watch(()=>e.collection?.viewQuery,e=>{clearTimeout(o),o=setTimeout(()=>a(e),200)})];return t.div({pbEvent:`collectionViewQueryTabContent`,className:`collection-tab-content collection-view-query-tab-content`,onunmount:()=>{clearTimeout(o),app.pb.cancelRequest(Xn),s.forEach(e=>e?.unwatch())}},t.div({className:`grid`},t.div({className:`col-12`},t.div({className:`txt-right txt-sm m-b-10`},t.button({type:`button`,className:`txt-bold link-hint`,"html-popovertarget":n+`caveats_dropdown`},()=>`Query caveats`)),t.div({id:n+`caveats_dropdown`,className:`dropdown sm query-caveats-dropdown`,popover:`auto`},t.ul(null,t.li(null,`Wildcard columns (*) are not supported.`),t.li(null,`The query must have a unique `,t.code(null,`id`),` column.`,t.br(),`If your query doesn't have a suitable one, you can use the universal `,t.code(null,`(ROW_NUMBER() OVER()) as id`),`.`),t.li(null,`Expressions must be aliased with a valid formatted field name, e.g. `,t.code(null,`MAX(balance) as maxBalance`),`.`),t.li(null,`Combined/multi-spaced expressions must be wrapped in parenthesis, e.g. `,t.code(null,`(MAX(balance) + 1) as maxBalance`),`.`),t.li(null,`UNION expressions are supported but the entire query must be wrapped in parenthesis.`))),t.div({className:`field`},t.label({htmlFor:n+`.viewQuery`},t.span({className:`txt`},`Select query`),t.span({hidden:()=>!i.testError,className:`query-state`,ariaDescription:app.attrs.tooltip(`Invalid query`,`left`)},t.i({className:`ri-error-warning-fill txt-danger`,ariaHidden:!0})),t.span({hidden:()=>!!i.testError,className:`query-state`,ariaDescription:app.attrs.tooltip(`Valid query`,`left`)},t.i({className:`ri-checkbox-circle-fill txt-success`,ariaHidden:!0}))),app.components.codeEditor({id:n+`.viewQuery`,name:`viewQuery`,language:`sql`,required:!0,autocomplete:
Line truncated
|
||||
`+e)})},t.div({className:`btns`},t.button({type:`button`,className:()=>`btn expanded-lg ${a.isSaving?`loading`:``}`,disabled:()=>!a.canSave,onclick:()=>s(!0)},t.span({className:`txt`},()=>a.isNew?`Create`:`Save changes`)),t.button({type:`button`,title:`Save options`,className:()=>`btn p-5`,disabled:()=>!a.canSave,"html-popovertarget":i+`save_options`},t.i({className:`ri-arrow-up-s-line`,ariaHidden:!0})),t.div({id:i+`save_options`,className:`dropdown nowrap`,popover:`auto`},t.button({type:`button`,className:`dropdown-item`,onclick:e=>{e.target.closest(`.dropdown`).hidePopover(),s(!1)}},t.span({className:`txt`},`Save and continue`),t.small({className:`txt-hint`},`(Ctrl+S)`)),t.hr(),t.button({type:`button`,className:`dropdown-item`,onclick:e=>{e.target.closest(`.dropdown`).hidePopover(),u()}},t.span({className:`txt`},`Reset form`)))))),r}function $n(e){let n=JSON.parse(JSON.stringify(app.store.collectionScaffolds[e.type])),r=JSON.parse(JSON.stringify(e.fields))||[],i=r.filter(e=>!e.system);e.fields=n.fields||[];for(let n of r){if(!n.system)continue;let r=e.fields.find(e=>e.name==n.name);r&&Object.assign(r,n)}for(let n of i)e.fields.push(n);if(e.indexes=e.indexes||[],e.indexes.length){let r=n?.indexes||[];indexesLoop:for(let n=e.indexes.length-1;n>=0;n--){let i=app.utils.parseIndex(e.indexes[n]),a=i.indexName.toLowerCase();for(let i of r)if(a==app.utils.parseIndex(i).indexName.toLowerCase()){e.indexes.splice(n,1);continue indexesLoop}for(let r of i.columns)if(!e.fields.find(e=>e.name.toLowerCase()==r.name.toLowerCase())){e.indexes.splice(n,1);continue indexesLoop}}}app.utils.mergeUnique(e.indexes,n.indexes)}function er(e,n){let r=`truncate_`+app.utils.randomString(),i=store({isSubmitting:!1,nameConfirm:``});async function a(){if(i.isSubmitting||!e.originalCollection?.name||e.originalCollection.name!=i.nameConfirm)return!1;i.isSubmitting=!0;try{return await app.pb.collections.truncate(e.originalCollection.name),n.ontruncate?.(JSON.parse(JSON.stringify(e.originalCollection))),app.toasts.success(`Successfully truncated collection "${e.originalCollection.name}".`),i.isSubmitting=!1,!0}catch(e){i.isSubmitting=!1,app.checkApiError(e)}return!1}return t.button({type:`button`,className:`dropdown-item txt-danger`,disabled:()=>i.isSubmitting,onclick:n=>{n.target.closest(`.dropdown`).hidePopover(),app.modals.confirm(t.div(null,t.h6({className:`block txt-center`},`Do you really want to delete all records of the collection?`),t.div({className:`confirm-collection-label txt-bold m-t-sm m-b-sm`},`Type the collection name `,t.div({className:`label`},()=>e.originalCollection.name,app.components.copyButton(()=>e.originalCollection?.name)),` to confirm:`),t.div({className:`field`},t.label({htmlFor:r+`.confirm_name`},`Collection name`),t.input({id:r+`.confirm_name`,type:`text`,required:!0,pattern:()=>RegExp.escape(e.originalCollection.name),value:()=>i.nameConfirm,oninput:e=>i.nameConfirm=e.target.value}))),async()=>{if(document.getElementById(r+`.confirm_name`)?.reportValidity(),!await a())return!1;app.modals.close(n.target.closest(`.modal`))},()=>{i.nameConfirm=``})}},t.i({className:`ri-eraser-line`,ariaHidden:!0}),t.span({className:`txt`},`Truncate`))}function tr(e,n){let r=`delete_`+app.utils.randomString(),i=store({isSubmitting:!1,nameConfirm:``});async function a(){if(i.isSubmitting||!e.originalCollection?.name||e.originalCollection.name!=i.nameConfirm)return!1;i.isSubmitting=!0;try{return await app.pb.collections.delete(e.originalCollection.name),n.ondelete?.(JSON.parse(JSON.stringify(e.originalCollection))),app.utils.removeByKey(app.store.collections,`id`,e.originalCollection.id),app.toasts.success(`Successfully deleted collection "${e.originalCollection.name}".`),i.isSubmitting=!1,!0}catch(e){i.isSubmitting=!1,app.checkApiError(e)}return!1}return t.button({type:`button`,className:`dropdown-item txt-danger`,disabled:()=>i.isSubmitting,onclick:n=>{n.target.closest(`.dropdown`).hidePopover();let o=n.target.closest(`.modal`);app.modals.confirm(t.div({className:`block`},t.h6({className:`block txt-center`},()=>e.originalCollection.type==`view`?`Do you really want to delete the selected collection?`:`Do you really want to delete the selected collection and all its records`),t.div({className:`confirm-collection-label txt-bold m-t-sm m-b-sm`},`Type the collection name `,t.div({className:`label`},()=>e.originalCollection.name,app.components.copyButton(()=>e.originalCollection?.name)),` to confirm:`),t.div({className:`field`},t.label({htmlFor:r+`.confirm_name`},`Collection name`),t.input({id:r+`.confirm_name`,type:`text`,required:!0,pattern:()=>RegExp.escape(e.originalCollection.name),value:()=>i.nameConfirm,oninput:e=>i.nameConfirm=e.target.value}))),async()=>{if(document.getElementById(r+`.confirm_name`)?.reportValidity(),!await a())return!1;app.modals.close(o)},()=>{i.nameConfirm=``})}},t.i({className:`ri-delete-bin-7-line`,ariaHidden:!0}),t.span({className:`txt`},`Delete`))}window.app=window.app||{},window.app.components=window.a
Line truncated
|
||||
...
|
||||
-----END PRIVATE KEY-----`,value:()=>a.privateKey||``,oninput:e=>a.privateKey=e.target.value})),t.div({className:`field-help`},`The key is not stored on the server and it is used only for generating the signed JWT.`)))),t.footer({className:`modal-footer`},t.button({type:`button`,className:`btn transparent m-r-auto`,onclick:()=>app.modals.close(n)},t.span({className:`txt`},`Close`)),t.button({"html-form":r+`_form`,type:`submit`,className:`btn expanded`},t.i({className:`ri-key-line`,ariaHidden:!0}),t.span({className:`txt`},`Generate secret`)))),n}function pr(){return t.div({className:`inline-flex gap-5`},t.span(null,`Default log levels:`),()=>{let e=[];for(let n in app.utils.logLevels)e.push(t.code(null,`${n}:${app.utils.logLevels[n].label}`));return e})}window.app=window.app||{},window.app.modals=window.app.modals||{},window.app.modals.openLogsSettings=function(e={onbeforeopen:null,onafteropen:null,onbeforeclose:null,onafterclose:null,onsave:null}){let n=mr(e);n&&(document.body.appendChild(n),app.modals.open(n))};function mr(e){let n,r=store({isLoading:!1,isSaving:!1,formSettings:{},initFormSettingsHash:``,get hasChanges(){return r.initFormSettingsHash!=JSON.stringify(r.formSettings)}});function i(e={}){r.formSettings={logs:e?.logs||{}},r.initFormSettingsHash=JSON.stringify(r.formSettings)}async function a(){r.isLoading=!0;try{i(await app.pb.settings.getAll({requestKey:`logsSettings`})),r.isLoading=!1}catch(e){e.isAbort||(r.isLoading=!1,app.checkApiError(e))}}async function o(){if(r.hasChanges){r.isSaving=!0;try{let a=await app.pb.settings.update(app.utils.filterRedactedProps(r.formSettings));e.onsave?.(a),i(a),app.toasts.success(`Successfully saved logs settings.`),r.isSaving=!1,app.modals.close(n)}catch(e){e.isAbort||(r.isSaving=!1,app.checkApiError(e))}}}return n=t.div({pbEvent:`logsSettingsModal`,className:`modal popup sm logs-settings-modal`,onbeforeopen:n=>(a(),e.onbeforeopen?.(n)),onafteropen:n=>{e.onafteropen?.(n)},onbeforeclose:n=>e.onbeforeclose?.(n),onafterclose:n=>{e.onafterclose?.(n),n?.remove()}},t.header({className:`modal-header`},t.h5({className:`m-auto`},`Logs settings`)),()=>r.isLoading?t.div({className:`modal-content flex`,style:`min-height: 200px`},t.span({className:`loader m-auto`})):[t.form({pbEvent:`logsSettingsForm`,id:`logsSettingsForm`,className:`modal-content logs-settings-form`,onsubmit:e=>{e.preventDefault(),o()}},t.div({className:`grid`},t.div({className:`col-lg-12`},t.field({className:`field`},t.label({htmlFor:`logs.maxDays`},`Max days retention`),t.input({type:`number`,id:`logs.maxDays`,name:`logs.maxDays`,min:0,required:!0,value:()=>r.formSettings.logs.maxDays,oninput:e=>r.formSettings.logs.maxDays=e.target.value<<0})),t.div({className:`field-help`},`Set to `,t.code(null,0),` to disable logs persistence.`)),t.div({className:`col-lg-12`},t.field({className:`field`},t.label({htmlFor:`logs.minLevel`},`Min log level`),t.input({type:`number`,id:`logs.minLevel`,name:`logs.minLevel`,min:-100,max:100,required:!0,value:()=>r.formSettings.logs.minLevel,oninput:e=>r.formSettings.logs.minLevel=e.target.value<<0})),t.div({className:`field-help`},t.div(null,`Logs with level below the minimum will be ignored.`),pr())),t.div({className:`col-lg-12`},t.field({className:`field`},t.input({type:`checkbox`,id:`logs.logIP`,name:`logs.logIP`,className:`switch`,checked:()=>r.formSettings.logs.logIP,onchange:e=>r.formSettings.logs.logIP=e.target.checked}),t.label({htmlFor:`logs.logIP`},`Enable IP logging`))),t.div({className:`col-lg-12`},t.field({className:`field`},t.input({type:`checkbox`,id:`logs.logAuthId`,name:`logs.logAuthId`,className:`switch`,checked:()=>r.formSettings.logs.logAuthId,onchange:e=>r.formSettings.logs.logAuthId=e.target.checked}),t.label({htmlFor:`logs.logAuthId`},`Enable Auth Id logging`))))),t.footer({className:`modal-footer`},t.button({type:`button`,className:`btn transparent m-r-auto`,onclick:()=>app.modals.close(n),disabled:()=>r.isSaving},t.span({className:`txt`},`Close`)),t.button({type:`submit`,"html-form":`logsSettingsForm`,className:()=>`btn ${r.isSaving?`loading`:``}`,disabled:()=>!r.hasChanges||r.isSaving},t.span({className:`txt`},`Save changes`)))]),n}function hr(e){return t.div({className:()=>`label log-level-label level-${e.level}`},t.span({className:`txt`},()=>`${app.utils.logLevels[e.level]?.label||`UNKN`} (${e.level})`))}window.app=window.app||{},window.app.modals=window.app.modals||{},window.app.modals.openLogPreview=function(e,n={onbeforeopen:null,onafteropen:null,onbeforeclose:null,onafterclose:null}){let r=yr(e,n);r&&(document.body.appendChild(r),app.modals.open(r))};var gr=[`execTime`,`type`,`auth`,`authId`,`status`,`method`,`url`,`referer`,`remoteIP`,`userIP`,`userAgent`,`error`,`details`];function _r(e){app.utils.downloadJSON(e,`log_`+e.created.replaceAll(/[-:\. ]/gi,``)+`.json`)}function vr(e){app.utils.copyToClipboard(JSON.stringify(e,null,2)),app.toasts.success(`Log copied to clipboard!`)}function yr(e,n){let r,i=store({isLoading:!1,log:null,get isRequ
Line truncated
|
||||
{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:[Hr(),Br(r),Vr(r)]};e._uplot?.destroy(),e._uplot=new uPlot(l,n,e)}function Br(e){let n;return{hooks:{init:r=>{r.over.ondblclick=n=>{e.zoom={}},n=watch(()=>{!e.zoom?.min||!e.zoom?.max?Rr(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 Vr(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 Hr(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} ${Ur(a)}-${Ur(o)}`,n.left=Math.round(e.valToPos(r,`x`)),n.top=Math.round(e.valToPos(i,`y`))}}}}function Ur(e){let n=e.getHours(),r=n>=12?`pm`:`am`;return n=n%12||12,n+r}var Q=50;function Wr(e){let n=store({logs:[],lastLoadCount:0,lastPage:1,bulkSelected:{},get canLoadMore(){return n.lastLoadCount>=Q},get totalSelected(){return Object.keys(n.bulkSelected).length},get areAllSelected(){return n.logs.length&&n.logs.length==n.totalSelected}});async function r(r=!1){e.isListLoading=!0;try{let i=r?1:n.lastPage+1,a=(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 o=app.utils.toRFC3339Datetime(i);a.push(`created >= "${r}" && created <= "${o}"`)}else i>1&&a.push(`created <= "${n.logs[n.logs.length-1].created}"`);let o=await app.pb.logs.getList(i,Q,{skipTotal:1,sort:`-@rowid`,requestKey:`logs_list`,filter:a.filter(Boolean).map(e=>`(`+e+`)`).join(`&&`)});o.page==1&&(n.logs=[],n.bulkSelected={}),n.lastPage=o.page,n.lastLoadCount=o.items.length;for(let e=0;e<o.items.length;e++)app.utils.pushOrReplaceObject(n.logs,o.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 i(e=!0){let r={};if(e)for(let e of n.logs)r[e.id]=e;n.bulkSelected=r}function a(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 o=/[-:\. ]/gi;function s(){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(o,``)+`.json`);let r=e[0].created.replaceAll(o,``),i=e[e.length-1].created.replaceAll(o,``);return app.utils.downloadJSON(e,`${e.length}_logs_${i}_to_${r}.json`)}let c=[];return t.div({pbEvent:`logsList`,className:`page-table-wrapper`,onmount(n){c.push(watch(()=>[e.filter,e.presets?.length],()=>{e.zoom={}}),watch(()=>[e.reset,e.filter,e.presets?.length,e.zoom?.min,e.zoom?.max],()=>{r(!0),n&&(n.scrollTop=0)}))},onunmount(){c.
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({hidden:()=>i.isLoading,className:`alert info m-b-sm`},t.div({className:`flex gap-5`},t.span(null,`Resolved user IP:`),t.strong(null,()=>i.realIP||`N/A`)),t.div({className:`flex gap-5`},t.span(null,`Detected proxy header:`),t.strong(null,()=>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
|
||||
connecting to your PocketBase app).
|
||||
`),t.p(null,`
|
||||
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({type:`button`,className:`label sm link-hint`,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 $r(){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{i(await app.pb.settings.getAll()),e.isLoading=!1}catch(e){e.isAbort||app.checkApiError(e)}}async function r(){if(!(e.isSaving||!e.hasChanges)){e.isSaving=!0,e.formSettings.rateLimits.rules=Xr(e.formSettings.rateLimits.rules);try{let n=app.utils.filterRedactedProps(e.formSettings);i(await app.pb.settings.update(n)),app.toasts.success(`Successfully saved application settings.`)}catch(e){app.checkApiError(e)}e.isSaving=!1}}function i(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={meta:n.meta||{},batch:n.batch||{},trustedProxy:n.trustedProxy||{headers:[]},rateLimits:n.rateLimits||{rules:[]}},Xr(e.originalFormSettings.rateLimits.rules),e.formSettings=JSON.parse(JSON.stringify(e.originalFormSettings))}function a(){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 application-settings-form`,inert:()=>e.isSaving,onsubmit:e=>{e.preventDefault(),r()}},t.div({className:`col-md-5`},t.div({className:`field`},t.label({htmlFor:`meta.appName`},`Application name`),t.input({id:`meta.appName`,name:`meta.appName`,type:`text`,required:!0,value:()=>e.formSettings.meta.appName||``,oninput:n=>e.formSettings.meta.appName=n.target.value}))),t.div({className:`col-md-5`},t.div({className:`field`},t.label({htmlFor:`meta.appURL`},`Application URL`),t.input({id:`meta.appURL`,name:`meta.appURL`,type:`url`,required:!0,value:()=>e.formSettings.meta.appURL||``,oninput:n=>e.formSettings.meta.appURL=n.target.value}))),t.div({className:`col-md-2`},()=>ei(e,e.isSaving)),t.div({className:`col-lg-12`},()=>Qr(e),()=>Zr(e),()=>Kr(e)),t.div({className:`col-lg-12`},t.div({className:`field`},t.input({id:`meta.hideControls`,name:`meta.hideControls`,type:`checkbox`,className:`switch`,checked:()=>e.formSettings.meta.hideControls,onchange:n=>e.formSettings.meta.hideControls=n.target.checke
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:a},t.span({className:`txt`},`Cancel`)),t.button({className:()=>`btn expanded ${e.isSaving?`loading`:``}`,disabled:()=>!e.hasChanges||e.isSaving},t.span({className:`txt`},`Save changes`)))))),t.footer({className:`page-footer`},app.components.credits())))}function ei(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 ti(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.cro
Line truncated
|
||||
@annually
|
||||
@monthly
|
||||
@weekly
|
||||
@daily
|
||||
@midnight
|
||||
@hourly`)},`macros`),`.`,t.br(),`By default the timezone is in UTC.`)),t.div({className:`col-lg-6`},t.div({className:`field`},t.label({htmlFor:`backups.cronMaxKeep`},`Max @auto backups to keep`),t.input({id:`backups.cronMaxKeep`,name:`backups.cronMaxKeep`,type:`number`,required:()=>a.enableAutoBackups,min:1,value:()=>a.formSettings.backups.cronMaxKeep,oninput:e=>{a.formSettings.backups.cronMaxKeep=parseInt(e.target.value,10)}})))))),t.div({className:`col-lg-12`},app.components.s3ConfigFields({toggleLabel:`Store backups in S3 storage`,testFilesystem:`backups`,config:()=>a.formSettings.backups.s3})),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({hidden:()=>!a.hasChanges,type:`button`,className:`btn transparent secondary`,onclick:l},t.span({className:`txt`},`Cancel`)),t.button({className:()=>`btn expanded ${a.isSaving?`loading`:``}`,disabled:()=>!a.hasChanges||a.isSaving},t.span({className:`txt`},`Save changes`))))])))}function ni(e={oncreated:null}){let n=ri(e);n&&(document.body.appendChild(n),app.modals.open(n))}function ri(e){let n,r=`backup_create_`+app.utils.randomString(),i=store({name:``,isSubmitting:!1}),a;async function o(){if(!i.isSubmitting){i.isSubmitting=!0,clearTimeout(a),a=setTimeout(()=>{app.modals.close(n)},1500);try{await app.pb.backups.create(i.name,{requestKey:r}),i.isSubmitting=!1,e.oncreated&&e.oncreated(i.name),app.toasts.success(`Successfully generated new backup.`),app.modals.close(n)}catch(e){e.isAbort||(clearTimeout(a),i.isSubmitting=!1,app.checkApiError(e))}}}return n=t.div({pbEvent:`backupCreateModal`,className:`modal popup backup-create-modal`,onbeforeclose:()=>{i.isSubmitting&&app.toasts.info(`The backup was started but may take a while to complete. You can come back later.`)},onafterclose:e=>{clearTimeout(a),e?.remove()}},t.header({className:`modal-header`},t.h5({className:`m-auto txt-center`},`Initialize new backup`)),t.form({id:r,className:`modal-content backup-restore-form`,autocomplete:`off`,onsubmit:e=>{e.preventDefault(),o()}},t.div({className:`grid`},t.div({className:`col-lg-12`},t.div({className:`alert warning`},t.div({className:`content`},t.p(null,`Please note that during the backup other concurrent write requests may fail since the database will be temporary "locked" (this usually happens only during the ZIP generation).`),t.p({className:`txt-bold`},`If you are using S3 storage for the collections file upload, you'll have to backup them separately since they are not locally stored and they will not be included in the generated backup!`)))),t.div({className:`col-lg-12`},t.div({className:`field`},t.label({htmlFor:r+`_name`},`Backup name`),t.input({id:r+`_name`,name:`name`,type:`text`,pattern:`^[a-z0-9_-]+.zip$`,placeholder:`Leave empty to autogenerate`,value:()=>i.name,oninput:e=>i.name=e.target.value})),t.div({className:`field-help`},`Must be in the format [a-z0-9_-].zip`)))),t.footer({className:`modal-footer`},t.button({type:`button`,className:`btn transparent m-r-auto`,disabled:()=>i.isSubmitting,onclick:()=>app.modals.close(n)},t.span({className:`txt`},`Cancel`)),t.button({"html-form":r,type:`submit`,className:()=>`btn ${i.isSubmitting?`loading`:``}`,disabled:()=>i.isSubmitting},t.span({className:`txt`},`Start backup`)))),n}function ii(e){let n=ai(e);document.body.appendChild(n),app.modals.open(n)}function ai(e){let n=`backup_restore_`+app.utils.randomString(),r=store({key:e,keyConfirm:``,isSubmitting:!1,get canSubmit(){return r.key&&r.key==r.keyConfirm}}),i;async function a(){if(!(r.isSubmitting||!r.canSubmit)){clearTimeout(i),r.isSubmitting=!0;try{await app.pb.backups.restore(r.keyConfirm),i=setTimeout(()=>{window.location.reload(),r.isSubmitting=!1},2e3)}catch(e){clearTimeout(i),e?.isAbort||(r.isSubmitting=!1,app.checkApiError(e))}}}return t.div({pbEvent:`backupRestoreModal`,className:`modal popup backup-restore-modal`,onbeforeclose:()=>!r.isSubmitting,onafterclose:e=>{e?.remove()},onunmount:()=>{clearTimeout(i)}},t.header({className:`modal-header`},t.h5({className:`m-auto txt-center`},`Restore `,t.strong(null,()=>r.key))),t.form({id:n,className:`modal-content backup-restore-form`,autocomplete:`off`,onsubmit:e=>{e.preventDefault(),a()}},t.div({className:`grid`},t.div({className:`col-lg-12`},t.div({className:`alert danger`},t.div({className:`content`},t.p({className:`txt-bold`},`Please proceed with extreme caution and use it only with trusted backups!`),t.p(null,`Backup restore currently works only on UNIX based systems.`),t.p(null,`The restore operation will attempt to replace your existing `,t.code(null,`pb_data`),` with the one from the backup and will restart the application process.`),t.p(null,`This means that on success all of your data (including app settings, users, superusers, etc.) will be replaced with the ones from the backup.`),t.p(null,`The operation will be reverted if the backup is invalid (ex. missing `,t.code(null,`data.db`),` file).`),t.p(null,`Below is an overLine truncated
|
||||
Vendored
+1
@@ -0,0 +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
|
||||
Vendored
+2
-2
@@ -13,9 +13,9 @@
|
||||
|
||||
<!-- prism -->
|
||||
<script src="./libs/prism/prism.js" data-manual></script>
|
||||
<script type="module" crossorigin src="./assets/index-CFYkmcdC.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-5KAeUg7B.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/pocketbase.es-B_4DUNUU.js">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-BsxX9wuy.css">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-DR1PnFnr.css">
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
|
||||
Generated
+6
-6
@@ -224,9 +224,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz",
|
||||
"integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==",
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
||||
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -937,9 +937,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.9",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
|
||||
"integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
|
||||
"version": "8.5.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
||||
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
||||
@@ -59,6 +59,7 @@ export function appHeader() {
|
||||
colorSchemeButton(),
|
||||
t.button(
|
||||
{
|
||||
type: "button",
|
||||
className: "header-link logged-user txt-normal",
|
||||
"html-popovertarget": "logged-user-dropdown",
|
||||
},
|
||||
@@ -108,6 +109,7 @@ function colorSchemeButton() {
|
||||
return [
|
||||
t.button(
|
||||
{
|
||||
type: "button",
|
||||
className: "header-link color-scheme-picker",
|
||||
"html-popovertarget": "color-scheme-dropdown",
|
||||
title: "Color scheme",
|
||||
|
||||
+18
-15
@@ -41,9 +41,9 @@ window.app.components.codeEditor = function(propsArg = {}) {
|
||||
// ]
|
||||
autocomplete: undefined, // Array<string|Object> | function(word): Array<string|Object>,
|
||||
// ---
|
||||
oninput: function(val, e) {},
|
||||
onfocus: function(val, e) {},
|
||||
onblur: function(val, e) {},
|
||||
oninput: function(val) {},
|
||||
onfocus: function(val) {},
|
||||
onblur: function(val) {},
|
||||
});
|
||||
|
||||
const extendWatchers = app.utils.extendStore(props, propsArg, "autocomplete");
|
||||
@@ -121,6 +121,12 @@ window.app.components.codeEditor = function(propsArg = {}) {
|
||||
isFieldVisible = true;
|
||||
}
|
||||
|
||||
function updateValue(newVal) {
|
||||
props.value = newVal;
|
||||
props.oninput?.(newVal);
|
||||
editorContent.dispatchEvent(new CustomEvent("change", { detail: newVal }));
|
||||
}
|
||||
|
||||
let isCtrlOrCmdKey = false;
|
||||
|
||||
let valueWatcher;
|
||||
@@ -155,8 +161,8 @@ window.app.components.codeEditor = function(propsArg = {}) {
|
||||
valueWatcher?.unwatch();
|
||||
closeAutocompleteDropdown();
|
||||
},
|
||||
onfocus: (e) => {
|
||||
props.onfocus?.(props.value, e);
|
||||
onfocus: () => {
|
||||
props.onfocus?.(props.value);
|
||||
},
|
||||
onblur: (e) => {
|
||||
// not blurred because of dropdown click
|
||||
@@ -164,16 +170,12 @@ window.app.components.codeEditor = function(propsArg = {}) {
|
||||
closeAutocompleteDropdown();
|
||||
}
|
||||
|
||||
props.onblur?.(props.value, e);
|
||||
props.onblur?.(props.value);
|
||||
},
|
||||
oninput: (e) => {
|
||||
closeAutocompleteDropdown();
|
||||
|
||||
props.value = editorContent.textContent;
|
||||
|
||||
props.oninput?.(props.value, e);
|
||||
|
||||
editorContent.dispatchEvent(new CustomEvent("change", { detail: props.value }));
|
||||
updateValue(editorContent.textContent);
|
||||
|
||||
if (!props.value?.length) {
|
||||
editorContent.textContent = ""; // ensure that no comments, br, etc. tags are left
|
||||
@@ -235,7 +237,8 @@ window.app.components.codeEditor = function(propsArg = {}) {
|
||||
editorContent.textContent = editorContent.textContent.substring(0, match.start)
|
||||
+ word
|
||||
+ editorContent.textContent.substring(match.end + 1);
|
||||
props.value = editorContent.textContent;
|
||||
|
||||
updateValue(editorContent.textContent);
|
||||
|
||||
try {
|
||||
window
|
||||
@@ -316,13 +319,13 @@ window.app.components.codeEditor = function(propsArg = {}) {
|
||||
selection.modify("extend", "backward", "character");
|
||||
if (selection.toString()[0] == "\t") {
|
||||
selection.deleteFromDocument();
|
||||
props.value = editorContent.textContent;
|
||||
updateValue(editorContent.textContent);
|
||||
} else {
|
||||
// check ahead and restore
|
||||
selection.modify("extend", "forward", "character");
|
||||
if (selection.toString()[0] == "\t") {
|
||||
selection.deleteFromDocument();
|
||||
props.value = editorContent.textContent;
|
||||
updateValue(editorContent.textContent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,7 +338,7 @@ window.app.components.codeEditor = function(propsArg = {}) {
|
||||
range.deleteContents();
|
||||
range.insertNode(document.createTextNode("\t"));
|
||||
range.collapse();
|
||||
props.value = editorContent.textContent;
|
||||
updateValue(editorContent.textContent);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
@@ -247,6 +247,7 @@ function initSearch(selectFunc = null) {
|
||||
if (data.searchTerm.length) {
|
||||
return t.button(
|
||||
{
|
||||
type: "button",
|
||||
className: "link-hint",
|
||||
title: "Clear search",
|
||||
onclick: () => reset(),
|
||||
|
||||
@@ -249,7 +249,7 @@ window.app.components.select = function(propsArg = {}) {
|
||||
tabIndex: -1,
|
||||
role: "button",
|
||||
className: "ri-close-line link-hint btn-option-unset",
|
||||
ariaLabel: app.attrs.tooltip("Unset", "left"),
|
||||
ariaLabel: app.attrs.tooltip("Unset"),
|
||||
onclick: () => {
|
||||
toggle(opt);
|
||||
return false;
|
||||
|
||||
@@ -124,28 +124,28 @@ window.app.components.tinymce = function(propsArg = {}) {
|
||||
|
||||
clearTimeout(changeTimeoutId);
|
||||
|
||||
// workaround for https://github.com/tinymce/tinymce/issues/9377
|
||||
editorRef.dom?.unbind(document);
|
||||
|
||||
catchError(() => {
|
||||
// workaround for https://github.com/tinymce/tinymce/issues/9377
|
||||
editorRef.dom?.unbind(document);
|
||||
|
||||
window.tinymce?.remove(editorRef);
|
||||
});
|
||||
editorRef = null;
|
||||
oldChange = null;
|
||||
}
|
||||
|
||||
async function initEditor(el) {
|
||||
async function initEditor(editorTarget) {
|
||||
await loadTinyMCE();
|
||||
|
||||
destroyEditor();
|
||||
|
||||
// removed while loading
|
||||
if (!el.isConnected) {
|
||||
if (!editorTarget?.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const opts = {
|
||||
target: el,
|
||||
target: editorTarget,
|
||||
content_style: cssVars,
|
||||
branding: false,
|
||||
promotion: false,
|
||||
@@ -203,7 +203,7 @@ window.app.components.tinymce = function(propsArg = {}) {
|
||||
"wordcount",
|
||||
],
|
||||
toolbar:
|
||||
"styles | alignleft aligncenter alignright | bold italic forecolor backcolor | bullist numlist | link table media_picker codesample | direction code",
|
||||
"styles | alignleft aligncenter alignright | bold italic forecolor backcolor | bullist numlist | link media_picker table codesample | direction code",
|
||||
paste_postprocess: (editor, args) => {
|
||||
cleanupPastedNode(args.node);
|
||||
},
|
||||
@@ -254,10 +254,10 @@ window.app.components.tinymce = function(propsArg = {}) {
|
||||
|
||||
// propagate save shortcut to the parent
|
||||
editor.on("keydown", (e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.code == "KeyS" && editor.formElement) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.code == "KeyS" && editorTarget) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
editor.formElement.dispatchEvent(new KeyboardEvent("keydown", e));
|
||||
editorTarget.dispatchEvent(new KeyboardEvent("keydown", e));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -133,6 +133,7 @@ function addToast(textOrElem, options = {}) {
|
||||
textOrElem,
|
||||
t.button(
|
||||
{
|
||||
type: "button",
|
||||
className: "m-l-auto btn circle sm transparent secondary toast-remove",
|
||||
title: "Clear",
|
||||
onclick: () => removeToast(toastRef),
|
||||
|
||||
@@ -832,7 +832,7 @@ function deleteDropdownItem(data, modalSettings) {
|
||||
try {
|
||||
await app.pb.collections.delete(data.originalCollection.name);
|
||||
|
||||
modalSettings.ondelete?.(JSON.parse(JSON.stringify(data.originalCollection)));
|
||||
modalSettings?.ondelete?.(JSON.parse(JSON.stringify(data.originalCollection)));
|
||||
|
||||
app.utils.removeByKey(app.store.collections, "id", data.originalCollection.id);
|
||||
|
||||
|
||||
@@ -6,8 +6,9 @@ export function collectionViewQueryTab(upsertData) {
|
||||
// dprint-ignore
|
||||
const autocomplete = [
|
||||
"SELECT", "FROM", "WHERE", "LEFT JOIN", "INNER JOIN", "ON",
|
||||
"GROUP BY", "HAVING", "ORDER BY", "LIMIT", "OFFSET", "AS",
|
||||
"WITH", "NOT", "IN", "EXISTS", "LIKE", "CAST",
|
||||
"AS", "GROUP BY", "HAVING", "ORDER BY", "ASC", "DESC", "LIMIT",
|
||||
"OFFSET", "WITH", "NOT", "IN", "AND", "OR", "EXISTS", "LIKE",
|
||||
"CAST", "REAL", "DECIMAL", "NUMERIC", "INT", "TEXT", "BOOL",
|
||||
];
|
||||
|
||||
const local = store({
|
||||
|
||||
@@ -6,7 +6,7 @@ export function mfaAccordion(collection) {
|
||||
if (!collection.mfa) {
|
||||
collection.mfa = {
|
||||
enabled: false,
|
||||
duration: 900,
|
||||
duration: 600,
|
||||
rule: "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ export function pageCollections(route) {
|
||||
onclick: () => app.modals.openRecordUpsert(app.store.activeCollection),
|
||||
},
|
||||
t.i({ className: "ri-add-line", ariaHidden: true }),
|
||||
t.span({ className: "txt", textContent: "New Record" }),
|
||||
t.span({ className: "txt", textContent: "New record" }),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
.api-preview-sidebar {
|
||||
width: 190px;
|
||||
width: 195px;
|
||||
flex-shrink: 0;
|
||||
overflow: auto;
|
||||
scrollbar-width: thin;
|
||||
|
||||
+8
-6
@@ -135,7 +135,7 @@ body {
|
||||
height: 100%;
|
||||
|
||||
/* normalizes IBM Plex Sans font rendering between Chrome and Firefox */
|
||||
letter-spacing: 0.1px;
|
||||
letter-spacing: 0.004em;
|
||||
}
|
||||
|
||||
h1,
|
||||
@@ -513,7 +513,9 @@ hr {
|
||||
text-decoration: none;
|
||||
border-radius: var(--borderRadius);
|
||||
color: var(--linkColor);
|
||||
transition: color var(--animationSpeed);
|
||||
transition:
|
||||
color var(--animationSpeed),
|
||||
opacity var(--animationSpeed);
|
||||
user-select: none;
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
@@ -894,14 +896,14 @@ hr {
|
||||
background: var(--surfaceAlt1Color);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--surfaceAlt1Color) 8%,
|
||||
var(--surfaceAlt2Color) 18%,
|
||||
var(--surfaceAlt1Color) 33%
|
||||
var(--surfaceAlt1Color) 10%,
|
||||
var(--surfaceAlt2Color) 25%,
|
||||
var(--surfaceAlt1Color) 60%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation:
|
||||
fadeIn var(--animationSpeed),
|
||||
shine 0.8s linear infinite;
|
||||
shine 1s linear infinite;
|
||||
&.sm {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
+7
-2
@@ -138,7 +138,6 @@
|
||||
width: 2000px; /* something large enough to fit the lines */
|
||||
height: 8000px;
|
||||
pointer-events: none;
|
||||
transition: opacity var(--animationSpeed);
|
||||
}
|
||||
.paths-group {
|
||||
fill: none;
|
||||
@@ -146,7 +145,7 @@
|
||||
stroke: var(--pathColor);
|
||||
}
|
||||
.erd-paths.front {
|
||||
--pathColor: var(--surfaceAlt5Color);
|
||||
--pathColor: var(--surfaceTxtDisabledColor);
|
||||
|
||||
z-index: 2;
|
||||
.active-from {
|
||||
@@ -176,6 +175,12 @@
|
||||
cursor: grabbing;
|
||||
}
|
||||
&.active {
|
||||
.erd-table {
|
||||
opacity: 0.7;
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.erd-paths.back {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ button {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
letter-spacing: inherit;
|
||||
}
|
||||
.btn {
|
||||
--btnGap: 8px;
|
||||
@@ -461,6 +462,7 @@ button {
|
||||
position: relative;
|
||||
left: var(--inputPadding);
|
||||
width: calc(100% - 2 * var(--inputPadding));
|
||||
min-height: 0;
|
||||
height: 6px;
|
||||
padding: 0;
|
||||
margin-top: 16px;
|
||||
@@ -472,6 +474,7 @@ button {
|
||||
input[type="color"] {
|
||||
-webkit-appearance: none;
|
||||
border: 0;
|
||||
min-height: 0;
|
||||
height: 36px;
|
||||
margin: 1px 0;
|
||||
&::-webkit-color-swatch-wrapper {
|
||||
|
||||
@@ -23,11 +23,13 @@
|
||||
gap: var(--headerItemsGap);
|
||||
font-size: var(--smFontSize);
|
||||
line-height: var(--smLineHeight);
|
||||
|
||||
/* for when the page is opened in "app" mode */
|
||||
@media (display-mode: minimal-ui) or (display-mode: standalone) {
|
||||
border-top: 1px solid var(--accentAlt1Color);
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: inline-flex;
|
||||
vertical-align: top;
|
||||
|
||||
@@ -15,6 +15,14 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.list-content {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-height: 425px;
|
||||
overflow: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
word-break: break-word;
|
||||
position: relative;
|
||||
|
||||
+1
-1
@@ -157,7 +157,7 @@
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 6px;
|
||||
background: var(--surfaceAlt4Color);
|
||||
background: var(--surfaceAlt5Color);
|
||||
}
|
||||
|
||||
/* levels */
|
||||
|
||||
@@ -26,15 +26,15 @@
|
||||
border-radius: 0;
|
||||
}
|
||||
td {
|
||||
background: var(--surfaceAlt2Color);
|
||||
border-left: 1px solid var(--surfaceAlt3Color);
|
||||
border-top: 1px solid var(--surfaceAlt3Color);
|
||||
background: var(--inputColor);
|
||||
border-left: 1px solid var(--inputBorderColor);
|
||||
border-top: 1px solid var(--inputBorderColor);
|
||||
transition: background var(--animationSpeed);
|
||||
&:first-child {
|
||||
border-left: 0px;
|
||||
}
|
||||
&:focus-within {
|
||||
background: var(--surfaceAlt3Color);
|
||||
background: var(--inputFocusColor);
|
||||
}
|
||||
&:has(.error:not(.hidden)) {
|
||||
background: var(--surfaceDangerColor);
|
||||
@@ -61,6 +61,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
td.col-new-btn {
|
||||
padding: 3px;
|
||||
&:focus-within {
|
||||
background: var(--inputColor);
|
||||
}
|
||||
}
|
||||
.col-label {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
@@ -196,12 +196,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.record-field-input {
|
||||
.record-summary {
|
||||
animation: fadeIn var(--animationSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
.field-type-bool {
|
||||
&.record-field-view .label {
|
||||
min-width: 48px;
|
||||
|
||||
Whitespace-only changes.
+3
-2
@@ -139,13 +139,13 @@
|
||||
[data-color-scheme="dark"],
|
||||
[data-color-scheme="dark"] .dropdown {
|
||||
--surfaceColor: #1f1f1f;
|
||||
--surfaceAlt1Color: color-mix(in srgb, var(--surfaceColor), white 3%);
|
||||
--surfaceAlt1Color: color-mix(in srgb, var(--surfaceColor), white 2%);
|
||||
--surfaceAlt2Color: color-mix(in srgb, var(--surfaceColor), white 7%);
|
||||
--surfaceAlt3Color: color-mix(in srgb, var(--surfaceColor), white 12%);
|
||||
--surfaceAlt4Color: color-mix(in srgb, var(--surfaceColor), white 14%);
|
||||
--surfaceAlt5Color: color-mix(in srgb, var(--surfaceColor), white 16%);
|
||||
|
||||
--surfaceTxtColor: #dedede;
|
||||
--surfaceTxtColor: #ebebeb;
|
||||
--tooltipTxtColor: var(--surfaceTxtColor);
|
||||
--surfaceTxtHintColor: color-mix(in srgb, var(--surfaceTxtColor), transparent 45%);
|
||||
--surfaceTxtDisabledColor: color-mix(in srgb, var(--surfaceTxtColor), transparent 65%);
|
||||
@@ -193,6 +193,7 @@
|
||||
scrollbar-color: var(--surfaceAlt5Color) transparent;
|
||||
|
||||
--modalOverlayColor: rgb(0,0,0, 0.45);
|
||||
--tooltipSurfaceColor: rgb(22,22,22, 0.9);
|
||||
|
||||
--boxShadow: 0px 8px 5px -5px rgba(0,0,0, 0.3);
|
||||
--leftBoxShadow: -1px 0px 5px 0 rgba(0,0,0, 0.3);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const tzName = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
||||
// {
|
||||
// collection: undefined,
|
||||
// originalRecord: undefined,
|
||||
@@ -14,7 +16,7 @@ export function input(props) {
|
||||
t.label(
|
||||
{ htmlFor: uniqueId },
|
||||
t.i({ className: app.fieldTypes.date.icon, ariaHidden: true }),
|
||||
t.span({ className: "txt" }, () => props.field.name),
|
||||
t.span({ className: "txt" }, () => props.field.name, " (", tzName, ")"),
|
||||
),
|
||||
t.input({
|
||||
id: uniqueId,
|
||||
|
||||
@@ -7,26 +7,31 @@
|
||||
export function input(props) {
|
||||
const uniqueId = "editor_" + app.utils.randomString();
|
||||
|
||||
const local = store({
|
||||
lazyEditor: null,
|
||||
const data = store({
|
||||
lazyEditor: "",
|
||||
});
|
||||
|
||||
let lazyEditorTimeoutId;
|
||||
|
||||
return t.div(
|
||||
{
|
||||
className: "record-field-input field-type-editor large-modal",
|
||||
onmount: () => {
|
||||
requestAnimationFrame(() => {
|
||||
local.lazyEditor = app.components.tinymce({
|
||||
lazyEditorTimeoutId = setTimeout(() => {
|
||||
data.lazyEditor = app.components.tinymce({
|
||||
id: uniqueId,
|
||||
name: () => props.field.name,
|
||||
required: () => props.field.required,
|
||||
convertURLs: () => props.field.convertURLs,
|
||||
name: () => props.field.name,
|
||||
value: () => props.record[props.field.name] || "",
|
||||
onchange: (val) => {
|
||||
props.record[props.field.name] = val;
|
||||
},
|
||||
});
|
||||
});
|
||||
}, 0);
|
||||
},
|
||||
onunmount: () => {
|
||||
clearTimeout(lazyEditorTimeoutId);
|
||||
},
|
||||
},
|
||||
t.div(
|
||||
@@ -36,7 +41,7 @@ export function input(props) {
|
||||
t.i({ className: app.fieldTypes.editor.icon, ariaHidden: true }),
|
||||
t.span({ className: "txt" }, () => props.field.name),
|
||||
),
|
||||
() => local.lazyEditor,
|
||||
() => data.lazyEditor,
|
||||
),
|
||||
() => {
|
||||
if (props.field.help) {
|
||||
|
||||
@@ -304,7 +304,7 @@ export function settings(data) {
|
||||
t.span({ className: "txt" }, "Protected"),
|
||||
t.small(
|
||||
{ className: "txt-hint" },
|
||||
"Files will require View API rule permissions and file token (",
|
||||
"File download requests will need to satisfy the View API rule (",
|
||||
t.a({
|
||||
href: import.meta.env.PB_PROTECTED_FILE_DOCS,
|
||||
target: "_blank",
|
||||
|
||||
@@ -36,34 +36,51 @@ export function input(props) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const fieldCollection = app.store.collections.find((c) => c.id == props.field.collectionId);
|
||||
const resultRecords = [];
|
||||
const idsToLoad = [];
|
||||
|
||||
// eagerly expand first level presentable relations (if any and the collections are loaded)
|
||||
const relExpands = [];
|
||||
const presentableRelationFields = fieldCollection?.fields?.filter(
|
||||
(f) => !f.hidden && f.presentable && f.type == "relation",
|
||||
) || [];
|
||||
for (const field of presentableRelationFields) {
|
||||
relExpands.push(field.name);
|
||||
// check for preloaded expand
|
||||
const expanded = app.utils.toArray(props.record.expand?.[props.field.name]);
|
||||
for (const id of ids) {
|
||||
const found = expanded.find((r) => r.id == id);
|
||||
if (found) {
|
||||
resultRecords.push(found);
|
||||
} else {
|
||||
idsToLoad.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
const records = await app.pb.collection(props.field.collectionId).getFullList({
|
||||
requestKey: null,
|
||||
filter: ids.map((id) => app.pb.filter("id={:id}", { id })).join("||"),
|
||||
expand: relExpands.join(",") || undefined,
|
||||
});
|
||||
try {
|
||||
if (idsToLoad.length) {
|
||||
const fieldCollection = app.store.collections.find((c) => c.id == props.field.collectionId);
|
||||
|
||||
// preserve the original order
|
||||
const orderedRecords = [];
|
||||
for (let id of ids) {
|
||||
const record = records.find((r) => r.id == id);
|
||||
if (record) {
|
||||
orderedRecords.push(record);
|
||||
// eagerly expand first level presentable relations (if any and the collections are loaded)
|
||||
const relExpands = [];
|
||||
const presentableRelationFields = fieldCollection?.fields?.filter(
|
||||
(f) => !f.hidden && f.presentable && f.type == "relation",
|
||||
) || [];
|
||||
for (const field of presentableRelationFields) {
|
||||
relExpands.push(field.name);
|
||||
}
|
||||
|
||||
const records = await app.pb.collection(props.field.collectionId).getFullList({
|
||||
requestKey: null,
|
||||
filter: idsToLoad.map((id) => app.pb.filter("id={:id}", { id })).join("||"),
|
||||
expand: relExpands.join(",") || undefined,
|
||||
});
|
||||
|
||||
// preserve the original order
|
||||
for (const id of idsToLoad) {
|
||||
const found = records.find((r) => r.id == id);
|
||||
if (found) {
|
||||
resultRecords.push(found);
|
||||
} else {
|
||||
console.warn("missing relation id:", id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
local.selected = orderedRecords;
|
||||
local.selected = resultRecords;
|
||||
local.isLoading = false;
|
||||
} catch (err) {
|
||||
if (!err.isAbort) {
|
||||
@@ -135,6 +152,7 @@ export function input(props) {
|
||||
{ className: "actions" },
|
||||
t.button(
|
||||
{
|
||||
type: "button",
|
||||
className: "btn sm secondary transparent circle",
|
||||
ariaLabel: app.attrs.tooltip("Remove"),
|
||||
onclick: () => remove(record.id),
|
||||
|
||||
+16
-9
@@ -19,11 +19,20 @@ export function logsList(logsSettings) {
|
||||
},
|
||||
});
|
||||
|
||||
// used as loose guard to prevent new logs to constantly push the old ones to later pages
|
||||
let loadStartDate;
|
||||
|
||||
async function load(reset = false) {
|
||||
logsSettings.isListLoading = true;
|
||||
|
||||
try {
|
||||
const page = reset ? 1 : data.lastPage + 1;
|
||||
let page;
|
||||
if (reset) {
|
||||
page = 1;
|
||||
loadStartDate = new Date().toISOString().replace("T", " ");
|
||||
} else {
|
||||
page = data.lastPage + 1;
|
||||
}
|
||||
|
||||
const normalizedFilter = (logsSettings.presets || []).concat(
|
||||
app.utils.normalizeSearchFilter(logsSettings.filter, ["level", "message", "data"]),
|
||||
@@ -41,9 +50,8 @@ export function logsList(logsSettings) {
|
||||
const max = app.utils.toRFC3339Datetime(maxDate);
|
||||
|
||||
normalizedFilter.push(`created >= "${min}" && created <= "${max}"`);
|
||||
} else if (page > 1) {
|
||||
// minimize duplicates in case there were new logs that push the old ones to later pages
|
||||
normalizedFilter.push(`created <= "${data.logs[data.logs.length - 1].created}"`);
|
||||
} else {
|
||||
normalizedFilter.push(`created <= "${loadStartDate}"`);
|
||||
}
|
||||
|
||||
const result = await app.pb.logs.getList(page, perPage, {
|
||||
@@ -275,14 +283,13 @@ export function logsList(logsSettings) {
|
||||
|
||||
return t.div(
|
||||
{ className: "sticky-content txt-center txt-hint" },
|
||||
t.p({ className: "txt-bold" }, "No logs found."),
|
||||
t.div({ className: "txt-bold" }, "No logs found."),
|
||||
t.button(
|
||||
{
|
||||
hidden: () =>
|
||||
logsSettings.filter?.length
|
||||
|| app.utils.isEmpty(logsSettings.zoom),
|
||||
logsSettings.filter?.length || app.utils.isEmpty(logsSettings.zoom),
|
||||
type: "button",
|
||||
className: "btn secondary expanded-lg",
|
||||
className: "btn secondary expanded-lg m-t-10",
|
||||
onclick() {
|
||||
logsSettings.zoom = {};
|
||||
},
|
||||
@@ -293,7 +300,7 @@ export function logsList(logsSettings) {
|
||||
{
|
||||
hidden: () => !logsSettings.filter?.length,
|
||||
type: "button",
|
||||
className: "btn secondary expanded-lg",
|
||||
className: "btn secondary expanded-lg m-t-10",
|
||||
onclick() {
|
||||
logsSettings.filter = "";
|
||||
},
|
||||
|
||||
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user