Compare commits

..
98 Commits
Author SHA1 Message Date
Gani Georgiev 3616b9d667 updated backport changelog 2026-05-22 07:20:20 +03:00
Gani Georgiev efd9604aa7 updated jsvm types 2026-05-22 07:14:50 +03:00
Gani Georgiev 1ab6bba61b updated golang.org/x dependencies 2026-05-22 06:24:21 +03:00
Gani Georgiev 9e6b4eeda8 updated reload tooltip and bumped app version 2026-05-21 19:00:58 +03:00
Gani Georgiev ea1a537a60 allow the select empty and placeholder props to be reactive functions 2026-05-20 16:39:59 +03:00
Gani Georgiev 23a989118c updated shablon 2026-05-20 16:27:19 +03:00
Gani Georgiev d0b2551e78 [#7694] don't reset the records list pagination on record update 2026-05-18 19:26:17 +03:00
Gani Georgiev b9b0e5ae80 added extra IP checks for the connected realtime client 2026-05-18 19:13:25 +03:00
Gani Georgiev f7fbc6c2c3 added RealtimeConnectRequestEvent.MaxTimeout field 2026-05-18 11:40:46 +03:00
Gani Georgiev a286d28bf9 fixed changelog typo 2026-05-15 07:34:50 +03:00
Gani Georgiev 5cff808438 updated error message and regenerated jsvm types 2026-05-15 07:23:25 +03:00
Gani Georgiev 45e3ca6ea6 normalized scaffold indexes 2026-05-15 07:06:56 +03:00
Gani Georgiev 9c8557a124 updated changelog 2026-05-15 06:32:58 +03:00
Gani Georgiev 8d7e3abbd6 [#7689] fixed indexes update collection error 2026-05-14 21:32:21 +03:00
Gani Georgiev 9d50e20880 updated api preview examples 2026-05-14 10:02:42 +03:00
Gani Georgiev fbf4f3e5fb updated modernc.org/sqlite 2026-05-14 09:26:13 +03:00
Gani Georgiev 820b9afe98 added error marker for each collection tab and fixed the styles of the raw errors tooltip 2026-05-14 09:15:05 +03:00
Gani Georgiev b061673d9a updated test to trigger the entire save hook chain 2026-05-13 23:11:10 +03:00
Gani Georgiev c9c47dc75c removed duplicated entry from the backport changelog 2026-05-13 22:49:25 +03:00
Gani Georgiev cbb7c061a3 updated backport changelog 2026-05-13 22:48:35 +03:00
Gani Georgiev caad25bb61 fixed grammar 2026-05-13 22:37:28 +03:00
Gani Georgiev 3b98059a8a force unset realtime connections auth state 2026-05-13 22:34:50 +03:00
Gani Georgiev a8c236a54d fixed ui extensions test 2026-05-10 14:41:53 +03:00
Gani Georgiev 81a5672498 updated ui extensions api to allow top-level await calls 2026-05-10 14:36:09 +03:00
Gani Georgiev 1b5ea9a1fa silenced the superuser ips confirmation if there is no change 2026-05-07 22:57:45 +03:00
Gani Georgiev d438c6a96a bumped app version 2026-05-07 19:51:35 +03:00
Gani Georgiev 6828ca1d4e bumped min go github action version to 1.26.3 2026-05-07 19:49:22 +03:00
Gani Georgiev 88193b9396 adjusted noitems alignment 2026-05-07 19:39:33 +03:00
Gani Georgiev 3cedf032ca [#7681] changed settings app url input to type=text 2026-05-06 19:40:44 +03:00
Gani Georgiev 4396369bb9 removed unused css file 2026-05-06 10:30:11 +03:00
Gani Georgiev 4a4f8ad9db use input css vars and updated changelog 2026-05-06 00:56:12 +03:00
Gani Georgiev 5a144e1342 [#6410] added rate limit option to exclude IPs/CIDR subnets 2026-05-05 17:29:25 +03:00
Gani Georgiev 24d72877e9 updated default log level marker color 2026-05-05 14:12:51 +03:00
Gani Georgiev c40f519b7b bump dev version for the demo 2026-05-05 14:02:10 +03:00
Gani Georgiev b026c6844b updated changelog 2026-05-05 14:01:18 +03:00
Gani Georgiev 21a5524fed added superuser ips whitelist 2026-05-05 13:04:06 +03:00
Gani Georgiev fe2d90641c added content-type serving exception tests 2026-05-05 11:40:07 +03:00
Gani Georgiev 8091a756e8 rollback plex-sans since the feedback from the user wasn't ok (inter was still 'fuzzy') 2026-05-04 21:47:57 +03:00
Gani Georgiev 7542cd14e2 [#7677] fixed default ui csp to allow iframe/object previews 2026-05-04 13:39:21 +03:00
Gani Georgiev 7da023f345 [#7467] forced serving conten-type for xlsx, docx and pptx 2026-05-04 13:14:56 +03:00
Gani Georgiev a104de5b9f fixed minor alignment issues after the font replacement 2026-05-04 11:40:40 +03:00
Gani Georgiev 97c5f3fa02 replaced ibm plex sans with inter 2026-05-04 10:50:41 +03:00
Gani Georgiev fccfa20653 added logs list start date guard 2026-05-04 09:49:32 +03:00
Gani Georgiev 29c6dacbfc updated changelog 2026-05-04 09:35:42 +03:00
Gani Georgiev d33927718b removed unnecessery logs filter normalization breaking the pagination 2026-05-04 09:33:47 +03:00
Gani Georgiev d153553d52 added eager alg error check to minimize misuse 2026-05-02 23:50:05 +03:00
Gani Georgiev db88253aac adjusted default button css 2026-05-02 10:05:43 +03:00
Gani Georgiev 0cf34c475b updated changelog 2026-05-02 07:34:50 +03:00
Gani Georgiev 547ee715c0 slightly adjusted the dark text color 2026-05-01 20:55:17 +03:00
Gani Georgiev 4850da6f56 adjusted flaky test 2026-05-01 19:32:00 +03:00
Gani Georgiev 53ac0d29da reordered change email validations to make enumerations slightly harder 2026-05-01 19:31:50 +03:00
Gani Georgiev d90aaedc00 skip duplicated records ids from the IN expand 2026-05-01 19:16:53 +03:00
Gani Georgiev 74defc48b9 fixed editor keydown propagation outside of form 2026-05-01 18:11:06 +03:00
Gani Georgiev 9205b11dc3 bumped app version 2026-05-01 17:51:40 +03:00
Gani Georgiev 8d0881db3f reload trusted proxy info UI after settings save 2026-04-29 11:06:26 +03:00
Gani Georgiev dbcd95eb62 updated the security policy 2026-04-28 12:10:24 +03:00
Gani Georgiev 905256b0c8 added the local time zone name next to the date field label 2026-04-27 22:38:27 +03:00
Gani Georgiev 602f3a4442 added auth collection specific check in the replacer 2026-04-27 22:38:03 +03:00
Gani Georgiev 5c9bcfaf8e [#7670] fixed password fields not being detected as changed 2026-04-27 17:41:19 +03:00
Gani Georgiev 44bf55097a updated changelogs 2026-04-27 09:30:32 +03:00
Gani Georgiev 338d672bee updated ui/dist 2026-04-27 09:04:38 +03:00
Gani Georgiev 5bd9d87bad reorder editor buttons to avoid dropdowns text wrapping 2026-04-27 08:47:14 +03:00
Gani Georgiev 6ba78d5218 updated gitlab userinfo doc reference 2026-04-27 08:03:20 +03:00
Gani Georgiev 260bd59c5b updated jstypes 2026-04-27 07:57:43 +03:00
Gani Georgiev 006566478a added explicit gitlab confirmed_at check 2026-04-27 07:55:51 +03:00
Gani Georgiev 419f335f5b various minor ui fixes 2026-04-27 01:13:08 +03:00
Gani Georgiev 326f150db2 added more tests for internal record hooks 2026-04-26 20:47:47 +03:00
Gani Georgiev 1c86addc4c [#7665] added BaseURL to the ghupdate plugin configuration 2026-04-26 20:47:02 +03:00
Gani Georgiev 494f47efb8 bumped go deps 2026-04-26 16:50:24 +03:00
Gani Georgiev 555a4f1a1e lowered the default mfa duration and reorganized internal record pre/post handling 2026-04-26 16:46:16 +03:00
Gani Georgiev 37b258810a updated gitea displayName 2026-04-26 14:23:23 +03:00
Gani Georgiev ca7cf1162f added App.DeleteAllExternalAuthsByRecord 2026-04-26 11:40:09 +03:00
Gani Georgiev dddb0a029f updated bitbucket,github and gitea oauth2 providers 2026-04-25 17:51:28 +03:00
Gani Georgiev 5d55fc18ee added dummy bcrypt check 2026-04-25 16:16:23 +03:00
Gani Georgiev 449e5af590 adjust dark text color 2026-04-25 11:41:09 +03:00
Gani Georgiev 1e460d3f96 updated changelog and rebuild ui/dist 2026-04-24 22:27:26 +03:00
Gani Georgiev b5030ddfa1 [#7664] fixed codeEditor not firing the change and input events on autocomplete selection 2026-04-24 22:08:58 +03:00
Gani Georgiev fbeb09c40b [#7660] added missing type:button attribute and replaced form tag with div to minimize future regressions 2026-04-24 15:42:47 +03:00
Gani Georgiev 69cdda4bf3 [#7659] fixed SMTP IPv6 format 2026-04-23 21:25:58 +03:00
Gani Georgiev e708f39e1b updated erd styles 2026-04-23 21:18:32 +03:00
Gani Georgiev 52eccb3aac fade non-focused erd tables 2026-04-23 17:43:15 +03:00
Gani Georgiev 1d593476b0 updated view autocomplete keywords 2026-04-23 17:32:07 +03:00
Gani Georgiev 8a04904de1 fixed godoc example typo 2026-04-23 16:57:26 +03:00
Gani Georgiev a3ac674f36 removed title loader to minimize layout jumps 2026-04-23 14:12:59 +03:00
Gani Georgiev ae7041a889 preload the record preview to minimize content jumps 2026-04-23 00:37:05 +03:00
Gani Georgiev 257f03e1fa removed lazy tinymce mount since the relation are now preloaded 2026-04-23 00:01:43 +03:00
Gani Georgiev 3566ba3729 exclude expand from the record draft 2026-04-22 23:15:46 +03:00
Gani Georgiev a6002c4622 optimized record upsert panel loading to minimize layout jumps 2026-04-22 23:02:56 +03:00
Gani Georgiev 2ddf161314 renamed list-group to list-content for consistency with the others 2026-04-22 17:59:11 +03:00
Gani Georgiev b15f358fc9 [#7655] added backups list scroll container 2026-04-22 17:22:22 +03:00
Gani Georgiev 83e44a7cfb added view query sample loading indicator 2026-04-22 16:04:57 +03:00
Gani Georgiev 866b8b8029 added missing name attribute, fixed initial collections page load routing params persistence and removed unnecessery sub label required mark 2026-04-22 15:42:17 +03:00
Gani Georgiev 857214e10d reordered number settings for consistency with the other fields 2026-04-22 14:46:19 +03:00
Gani Georgiev 592b13913f added min-height to the page tables and adjusted light surface colors 2026-04-22 07:00:36 +03:00
Gani Georgiev 84b50c4869 minor color and styles improvements 2026-04-21 17:40:11 +03:00
Gani Georgiev 3c33868ea8 [#7653] updated tinymce options to fix its dialogs position 2026-04-21 14:16:23 +03:00
Gani Georgiev 223ac7a64a enabled text wrapping for the API rule fileds 2026-04-21 11:52:45 +03:00
Gani Georgiev 0cee0662f6 fixed 0 total count on page back/forward navigation 2026-04-21 10:26:37 +03:00
151 changed files with 7945 additions and 4363 deletions

No files matched your search

+12
View File
@@ -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>
+1 -1
View File
@@ -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.
+110
View File
@@ -1,3 +1,113 @@
## v0.38.2
- Added `RealtimeConnectRequestEvent.MaxTimeout` field to specify the absolute max duration a realtime connection can remain open (default to 30mins).
_This is in addition to the `IdeTimeout` of 5mins in order to prevent misuse and to allow the GC to run more regularly._
- Added extra checks for the connected user IP in the realtime APIs to prevent bruteforce guest subscription update attempts and to serve as an extra protection for the "all-in-one" OAuth2 realtime handler.
- Don't reset the records list pagination on record update ([#7694](https://github.com/pocketbase/pocketbase/issues/7694)).
- Updated all `golang.org/x/` packages to cover the recent [security fixes](https://groups.google.com/g/golang-announce/c/PdiGK3xulk4) _(none of them should be a critical issue in PocketBase but nonetheless it is advised to update)_.
## v0.38.1
- Silenced the superuser IPs confirmation if there is no change.
- Updated the _experimental_ UI extensions APIs to allow top-level `await` in the initialization script.
- Force unset the auth state of existing realtime connections on user password, collection secret, etc. changes.
_This is not strictly necessary because the realtime connections have short-lived idle timeout by design but nonetheless it was implemented to minimize the attack vectors._
- Added error marker for each collection tab and fixed the styles of the raw errors tooltip.
- Fixed indexes collection update error ([#7689](https://github.com/pocketbase/pocketbase/issues/7689)).
_⚠️ The fix comes with a system migration that resaves all collections with indexes to ensure that all indexes are normalized and available in the `Collection.Indexes` field (it will also include indexes created manually via the sqlite3 cli or other external tool)._
_If you are using a test `pb_data` for your Go automation tests you may want to apply the migration to it too so that it runs only once and not for each execution of your tests, aka. you could run once `go run main.go migrate up --dir="/path/to/test_pb_data"`._
- Updated `modernc.org/sqlite` to v1.50.1 (SQLite 3.53.1).
- Other minor fixes (_updated API preview examples, fixed code comment typos, etc._).
## 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.
- Fixed `editor` floating dialogs position when scrolling ([#7653](https://github.com/pocketbase/pocketbase/issues/7653)).
- Enabled text wrapping for the API rule fields.
- Added view query sample loading indicator.
- Other minor light UI contrast and styles improvements.
## v0.37.2
- Fixed autoexpandable input in Firefox ([#7648](https://github.com/pocketbase/pocketbase/discussions/7648)).
+23
View File
@@ -2,6 +2,29 @@
> For the most recent versions, please refer to [CHANGELOG.md](./CHANGELOG.md)
---
## v0.22.45
- (_Backported from v0.38.2_) Updated all `golang.org/x/` packages to cover the recent [security fixes](https://groups.google.com/g/golang-announce/c/PdiGK3xulk4) _(none of them should be a critical issue in PocketBase but nonetheless it is advised to update)_.
## v0.22.44
- (_Backported from v0.38.1_) Force unset the auth state of existing realtime connections on user tokenKey change.
## 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
View File
@@ -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")
+52
View File
@@ -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 {
+1
View File
@@ -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
View File
@@ -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,
+2 -1
View File
@@ -81,7 +81,8 @@ func copyExtensionMainjs(buf *bytes.Buffer, ext core.UIExtension) error {
defer f.Close()
// wrap in a self-executing function to avoid scope and concatenation issues
_, _ = buf.WriteString("(function(){")
// (the await/async is for top-level await)
_, _ = buf.WriteString("await (async function(){")
_, err = io.Copy(buf, f)
if err != nil {
+1 -1
View File
@@ -72,7 +72,7 @@ func TestUIExtensions_Mainjs(t *testing.T) {
},
AfterTestFunc: successAfterTestFunc,
ExpectedStatus: 200,
ExpectedContent: []string{"(function(){ext1_main})();(function(){ext3_main})();"},
ExpectedContent: []string{"await (async function(){ext1_main})();await (async function(){ext3_main})();"},
ExpectedEvents: map[string]int{"*": 0},
},
}
+10
View File
@@ -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
+44
View File
@@ -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,
+25
View File
@@ -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] {
+39 -1
View File
@@ -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}
+163 -3
View File
@@ -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})
})
}
+93
View File
@@ -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)
}
}
+118 -11
View File
@@ -28,6 +28,9 @@ const clientsChunkSize = 150
// RealtimeClientAuthKey is the name of the realtime client store key that holds its auth state.
const RealtimeClientAuthKey = "auth"
// RealtimeClientIPKey is the name of the realtime client store key that holds the IP of the connected client.
const RealtimeClientIPKey = "pbRealtimeClientIP"
// bindRealtimeApi registers the realtime api endpoints.
func bindRealtimeApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) {
sub := rg.Group("/realtime")
@@ -63,8 +66,12 @@ func realtimeConnect(e *core.RequestEvent) error {
connectEvent := new(core.RealtimeConnectRequestEvent)
connectEvent.RequestEvent = e
connectEvent.Client = subscriptions.NewDefaultClient()
connectEvent.IdleTimeout = 5 * time.Minute
connectEvent.MaxTimeout = 30 * time.Minute
connectEvent.Client = subscriptions.NewDefaultClient()
// could be used as an optional cross-reference check in other API endpoints
connectEvent.Client.Set(RealtimeClientIPKey, e.RealIP())
return e.App.OnRealtimeConnectRequest().Trigger(connectEvent, func(ce *core.RealtimeConnectRequestEvent) error {
// register new subscription client
@@ -73,7 +80,7 @@ func realtimeConnect(e *core.RequestEvent) error {
e.App.SubscriptionsBroker().Unregister(ce.Client.Id())
}()
ce.App.Logger().Debug("Realtime connection established.", slog.String("clientId", ce.Client.Id()))
ce.App.Logger().Debug("Realtime connection established", slog.String("clientId", ce.Client.Id()))
// signalize established connection (aka. fire "connect" message)
connectMsgEvent := new(core.RealtimeMessageEvent)
@@ -99,12 +106,19 @@ func realtimeConnect(e *core.RequestEvent) error {
return nil
}
// start a max lifetime timer to prevent accumulating too much
// connection resources and to allow the GC to run more regularly
maxTimer := time.NewTimer(ce.MaxTimeout)
defer maxTimer.Stop()
// start an idle timer to keep track of inactive/forgotten connections
idleTimer := time.NewTimer(ce.IdleTimeout)
defer idleTimer.Stop()
for {
select {
case <-maxTimer.C:
cancelRequest()
case <-idleTimer.C:
cancelRequest()
case msg, ok := <-ce.Client.Channel():
@@ -186,6 +200,21 @@ func realtimeSetSubscriptions(e *core.RequestEvent) error {
return e.NotFoundError("Missing or invalid client id.", err)
}
// for just in case to prevent someone changing a guest subscription
//
// note1: this is an extra precaution against clientId bruteforce attempts
// for installations allowing longer realtime connections duration
//
// note2: custom registered clients (aka. those without IP in the store)
// are excluded from the check for backward compatibility
clientIP, _ := client.Get(RealtimeClientIPKey).(string)
if clientIP != "" && clientIP != e.RealIP() {
return e.BadRequestError(
"Invalid realtime client.",
errors.New("the subscription request IP doesn't match with the realtime client IP"),
)
}
// for now allow only guest->auth upgrades and any other auth change is forbidden
clientAuth, _ := client.Get(RealtimeClientAuthKey).(*core.Record)
if clientAuth != nil && !isSameAuth(clientAuth, e.Auth) {
@@ -208,7 +237,7 @@ func realtimeSetSubscriptions(e *core.RequestEvent) error {
e.Client.Subscribe(e.Subscriptions...)
e.App.Logger().Debug(
"Realtime subscriptions updated.",
"Realtime subscriptions updated",
slog.String("clientId", e.Client.Id()),
slog.Any("subscriptions", e.Subscriptions),
)
@@ -219,8 +248,13 @@ func realtimeSetSubscriptions(e *core.RequestEvent) error {
})
}
// updateClientsAuth updates the existing clients auth record with the new one (matched by ID).
func realtimeUpdateClientsAuth(app core.App, newAuthRecord *core.Record) error {
// realtimeUpdateClientsAuth updates the auth state of all clients related to the provided authRecord.
//
// Realtime connections has short lifetime by design, but to minimize abuse
// if the new record has a different tokenKey (e.g. in case of password reset)
// the auth state of the related realtime connections is also cleared
// (aka. they remain active but unauthenticated, allowing to reauthenicate with the next subscription).
func realtimeUpdateClientsAuth(app core.App, authRecord *core.Record) error {
chunks := app.SubscriptionsBroker().ChunkedClients(clientsChunkSize)
group := new(errgroup.Group)
@@ -230,9 +264,13 @@ func realtimeUpdateClientsAuth(app core.App, newAuthRecord *core.Record) error {
for _, client := range chunk {
clientAuth, _ := client.Get(RealtimeClientAuthKey).(*core.Record)
if clientAuth != nil &&
clientAuth.Id == newAuthRecord.Id &&
clientAuth.Collection().Name == newAuthRecord.Collection().Name {
client.Set(RealtimeClientAuthKey, newAuthRecord)
clientAuth.Id == authRecord.Id &&
clientAuth.Collection().Name == authRecord.Collection().Name {
if clientAuth.TokenKey() != authRecord.TokenKey() {
client.Unset(RealtimeClientAuthKey)
} else {
client.Set(RealtimeClientAuthKey, authRecord)
}
}
}
@@ -243,8 +281,8 @@ func realtimeUpdateClientsAuth(app core.App, newAuthRecord *core.Record) error {
return group.Wait()
}
// realtimeUnsetClientsAuthState unsets the auth state of all clients that have the provided auth model.
func realtimeUnsetClientsAuthState(app core.App, authModel core.Model) error {
// realtimeUnsetClientsAuthByRecordModelOrProxy unsets the auth state of all clients that have the provided auth model.
func realtimeUnsetClientsAuthByRecordModelOrProxy(app core.App, authModel core.Model) error {
chunks := app.SubscriptionsBroker().ChunkedClients(clientsChunkSize)
group := new(errgroup.Group)
@@ -267,7 +305,76 @@ func realtimeUnsetClientsAuthState(app core.App, authModel core.Model) error {
return group.Wait()
}
// realtimeUnsetClientsAuthByCollection unsets the auth state of all authenticated clients related to the collection.
func realtimeUnsetClientsAuthByCollection(app core.App, collection *core.Collection) error {
chunks := app.SubscriptionsBroker().ChunkedClients(clientsChunkSize)
group := new(errgroup.Group)
for _, chunk := range chunks {
group.Go(func() error {
for _, client := range chunk {
clientAuth, _ := client.Get(RealtimeClientAuthKey).(*core.Record)
if clientAuth != nil && clientAuth.Collection().Name == collection.Name {
client.Unset(RealtimeClientAuthKey)
}
}
return nil
})
}
return group.Wait()
}
func bindRealtimeEvents(app core.App) {
// reset the clients auth on collection secret change
// (@todo with the future tracking of old collections data consider replacing with *AfterUpdateSuccess to account for transaction rollback)
app.OnCollectionUpdate().Bind(&hook.Handler[*core.CollectionEvent]{
Func: func(e *core.CollectionEvent) error {
if !e.Collection.IsAuth() {
return e.Next()
}
cached, _ := e.App.FindCachedCollectionByNameOrId(e.Collection.Id)
if err := e.Next(); err != nil {
return err
}
if cached != nil && cached.AuthToken.Secret != e.Collection.AuthToken.Secret {
if err := realtimeUnsetClientsAuthByCollection(e.App, e.Collection); err != nil {
app.Logger().Warn(
"Failed to remove client(s) associated to the changed auth collection",
slog.String("collectionName", e.Collection.Name),
slog.String("error", err.Error()),
)
}
}
return nil
},
Priority: -99,
})
// unset the clients auth on auth collection delete
app.OnCollectionAfterDeleteSuccess().Bind(&hook.Handler[*core.CollectionEvent]{
Func: func(e *core.CollectionEvent) error {
if e.Collection.IsAuth() {
if err := realtimeUnsetClientsAuthByCollection(e.App, e.Collection); err != nil {
app.Logger().Warn(
"Failed to remove client(s) associated to the deleted auth collection",
slog.String("collectionName", e.Collection.Name),
slog.String("error", err.Error()),
)
}
}
return e.Next()
},
Priority: -99,
})
// update the clients that has auth record association
app.OnModelAfterUpdateSuccess().Bind(&hook.Handler[*core.ModelEvent]{
Func: func(e *core.ModelEvent) error {
@@ -294,7 +401,7 @@ func bindRealtimeEvents(app core.App) {
Func: func(e *core.ModelEvent) error {
collection := realtimeResolveRecordCollection(e.App, e.Model)
if collection != nil && collection.IsAuth() {
if err := realtimeUnsetClientsAuthState(e.App, e.Model); err != nil {
if err := realtimeUnsetClientsAuthByRecordModelOrProxy(e.App, e.Model); err != nil {
app.Logger().Warn(
"Failed to remove client(s) associated to the deleted auth model",
slog.Any("id", e.Model.PK()),
+231 -13
View File
@@ -26,6 +26,7 @@ func TestRealtimeConnect(t *testing.T) {
Method: http.MethodGet,
URL: "/api/realtime",
Timeout: 100 * time.Millisecond,
Headers: map[string]string{"x-test-ip": "127.0.0.2"},
ExpectedStatus: 200,
ExpectedContent: []string{
`id:`,
@@ -37,6 +38,17 @@ func TestRealtimeConnect(t *testing.T) {
"OnRealtimeConnectRequest": 1,
"OnRealtimeMessageSend": 1,
},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
app.Settings().TrustedProxy.Headers = []string{"x-test-ip"}
app.OnRealtimeConnectRequest().BindFunc(func(e *core.RealtimeConnectRequestEvent) error {
if ip, _ := e.Client.Get(apis.RealtimeClientIPKey).(string); ip != "127.0.0.2" {
t.Fatalf("Expected IP %q, got %q", "127.0.0.2", ip)
}
return e.Next()
})
},
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
if len(app.SubscriptionsBroker().Clients()) != 0 {
t.Errorf("Expected the subscribers to be removed after connection close, found %d", len(app.SubscriptionsBroker().Clients()))
@@ -102,7 +114,8 @@ func TestRealtimeSubscribe(t *testing.T) {
resetClient := func() {
client.Unsubscribe()
client.Set(apis.RealtimeClientAuthKey, nil)
client.Unset(apis.RealtimeClientAuthKey)
client.Unset(apis.RealtimeClientIPKey)
}
validSubscriptionsLimit := make([]string, 1000)
@@ -208,6 +221,26 @@ func TestRealtimeSubscribe(t *testing.T) {
},
ExpectedEvents: map[string]int{"*": 0},
},
{
Name: "existing client with different IP",
Method: http.MethodPost,
URL: "/api/realtime",
Body: strings.NewReader(`{"clientId":"` + client.Id() + `","subscriptions":["test"]}`),
Headers: map[string]string{"x-test-ip": "127.0.0.2"},
ExpectedStatus: 400,
ExpectedContent: []string{`"data":{}`},
ExpectedEvents: map[string]int{"*": 0},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
app.Settings().TrustedProxy.Headers = []string{"x-test-ip"}
client.Set(apis.RealtimeClientIPKey, "127.0.0.1")
app.SubscriptionsBroker().Register(client)
},
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
resetClient()
},
},
{
Name: "existing client with valid topic length",
Method: http.MethodPost,
@@ -460,7 +493,10 @@ func TestRealtimeAuthRecordDeleteEvent(t *testing.T) {
e.Context = context.Background()
e.Model = authRecord1
testApp.OnModelAfterDeleteSuccess().Trigger(e)
err = testApp.OnModelAfterDeleteSuccess().Trigger(e)
if err != nil {
t.Fatal(err)
}
if total := len(testApp.SubscriptionsBroker().Clients()); total != 3 {
t.Fatalf("Expected %d subscription clients, found %d", 3, total)
@@ -495,25 +531,207 @@ func TestRealtimeAuthRecordUpdateEvent(t *testing.T) {
client.Set(apis.RealtimeClientAuthKey, authRecord1)
testApp.SubscriptionsBroker().Register(client)
// refetch the authRecord and change its email
// refetch the authRecord and change its name
authRecord2, err := testApp.FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatal(err)
}
authRecord2.SetEmail("new@example.com")
// mock update event
e := new(core.ModelEvent)
e.App = testApp
e.Type = core.ModelEventTypeUpdate
e.Context = context.Background()
e.Model = authRecord2
newName := "test_new_name"
authRecord2.Set("name", newName)
testApp.OnModelAfterUpdateSuccess().Trigger(e)
err = testApp.Save(authRecord2)
if err != nil {
t.Fatal(err)
}
clientAuthRecord, _ := client.Get(apis.RealtimeClientAuthKey).(*core.Record)
if clientAuthRecord.Email() != authRecord2.Email() {
t.Fatalf("Expected authRecord with email %q, got %q", authRecord2.Email(), clientAuthRecord.Email())
if clientAuthRecord.Get("name") != newName {
t.Fatalf("Expected authRecord with email %q, got %q", newName, clientAuthRecord.Email())
}
}
func TestRealtimeAuthRecordUnsetOnTokenKeyRefresh(t *testing.T) {
testApp, _ := tests.NewTestApp()
defer testApp.Cleanup()
// init realtime handlers
apis.NewRouter(testApp)
authRecord1, err := testApp.FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatal(err)
}
client := subscriptions.NewDefaultClient()
client.Set(apis.RealtimeClientAuthKey, authRecord1)
testApp.SubscriptionsBroker().Register(client)
// refetch the authRecord and refresh its tokenKey
authRecord2, err := testApp.FindAuthRecordByEmail("users", "test@example.com")
if err != nil {
t.Fatal(err)
}
authRecord2.RefreshTokenKey()
err = testApp.Save(authRecord2)
if err != nil {
t.Fatal(err)
}
clientAuthRecord, _ := client.Get(apis.RealtimeClientAuthKey).(*core.Record)
if clientAuthRecord != nil {
t.Fatalf("Expected authRecord to be unset, got %q", clientAuthRecord.Email())
}
}
func TestRealtimeAuthRecordUnsetOnCollectionSecretChange(t *testing.T) {
testApp, _ := tests.NewTestApp()
defer testApp.Cleanup()
// init realtime handlers
apis.NewRouter(testApp)
usersCollection, err := testApp.FindCollectionByNameOrId("users")
if err != nil {
t.Fatal(err)
}
clientsCollection, err := testApp.FindCollectionByNameOrId("clients")
if err != nil {
t.Fatal(err)
}
authRecord1, err := testApp.FindAuthRecordByEmail(usersCollection, "test@example.com")
if err != nil {
t.Fatal(err)
}
client1 := subscriptions.NewDefaultClient()
client1.Set(apis.RealtimeClientAuthKey, authRecord1)
authRecord2, err := testApp.FindAuthRecordByEmail(usersCollection, "test@example.com")
if err != nil {
t.Fatal(err)
}
client2 := subscriptions.NewDefaultClient()
client2.Set(apis.RealtimeClientAuthKey, authRecord2)
authRecord3, err := testApp.FindAuthRecordByEmail(clientsCollection, "test@example.com")
if err != nil {
t.Fatal(err)
}
client3 := subscriptions.NewDefaultClient()
client3.Set(apis.RealtimeClientAuthKey, authRecord3)
clientMocks := map[*core.Record]subscriptions.Client{
authRecord1: client1,
authRecord2: client2,
authRecord3: client3,
}
for _, client := range clientMocks {
testApp.SubscriptionsBroker().Register(client)
}
// change the secret of the users collection (should trigger unset)
usersCollection.AuthToken.Secret = strings.Repeat("a", 30)
err = testApp.Save(usersCollection)
if err != nil {
t.Fatal(err)
}
// change something else of the clients collection (shouldn't trigger unset)
clientsCollection.ListRule = nil
err = testApp.Save(clientsCollection)
if err != nil {
t.Fatal(err)
}
expectations := map[*core.Record]bool{
// record -> unset
authRecord1: true,
authRecord2: true,
authRecord3: false,
}
for record, expectedUnset := range expectations {
clientAuthRecord, _ := clientMocks[record].Get(apis.RealtimeClientAuthKey).(*core.Record)
unset := clientAuthRecord == nil
if unset != expectedUnset {
t.Fatalf("Expected unset state %v, got %v (%v)", expectedUnset, unset, clientAuthRecord)
}
}
}
func TestRealtimeAuthRecordUnsetOnCollectionDelete(t *testing.T) {
testApp, _ := tests.NewTestApp()
defer testApp.Cleanup()
// init realtime handlers
apis.NewRouter(testApp)
usersCollection, err := testApp.FindCollectionByNameOrId("users")
if err != nil {
t.Fatal(err)
}
clientsCollection, err := testApp.FindCollectionByNameOrId("clients")
if err != nil {
t.Fatal(err)
}
authRecord1, err := testApp.FindAuthRecordByEmail(usersCollection, "test@example.com")
if err != nil {
t.Fatal(err)
}
client1 := subscriptions.NewDefaultClient()
client1.Set(apis.RealtimeClientAuthKey, authRecord1)
authRecord2, err := testApp.FindAuthRecordByEmail(usersCollection, "test@example.com")
if err != nil {
t.Fatal(err)
}
client2 := subscriptions.NewDefaultClient()
client2.Set(apis.RealtimeClientAuthKey, authRecord2)
authRecord3, err := testApp.FindAuthRecordByEmail(clientsCollection, "test@example.com")
if err != nil {
t.Fatal(err)
}
client3 := subscriptions.NewDefaultClient()
client3.Set(apis.RealtimeClientAuthKey, authRecord3)
clientMocks := map[*core.Record]subscriptions.Client{
authRecord1: client1,
authRecord2: client2,
authRecord3: client3,
}
for _, client := range clientMocks {
testApp.SubscriptionsBroker().Register(client)
}
// mock users collection delete event to avoid triggering constraints check
e := new(core.ModelEvent)
e.App = testApp
e.Type = core.ModelEventTypeDelete
e.Context = context.Background()
e.Model = usersCollection
err = testApp.OnModelAfterDeleteSuccess().Trigger(e)
if err != nil {
t.Fatal(err)
}
expectations := map[*core.Record]bool{
// record -> unset
authRecord1: true,
authRecord2: true,
authRecord3: false,
}
for record, expectedUnset := range expectations {
clientAuthRecord, _ := clientMocks[record].Get(apis.RealtimeClientAuthKey).(*core.Record)
unset := clientAuthRecord == nil
if unset != expectedUnset {
t.Fatalf("Expected unset state %v, got %v (%v)", expectedUnset, unset, clientAuthRecord)
}
}
}
+6 -6
View File
@@ -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
}
+40 -1
View File
@@ -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))
}
},
},
{
+23 -6
View File
@@ -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 {
+10
View File
@@ -55,6 +55,16 @@ func oauth2SubscriptionRedirect(e *core.RequestEvent) error {
}
defer client.Unsubscribe(oauth2SubscriptionTopic)
// additional check to minimize the risk of XSRF attack vectors
//
// note: custom registered clients (aka. those without IP in the store)
// are excluded from the check for backward compatibility
clientIP, _ := client.Get(RealtimeClientIPKey).(string)
if clientIP != "" && clientIP != e.RealIP() {
e.App.Logger().Debug("The client IP that completed the authentication is different from the one that initialized the OAuth2 realtime connection")
return failureRedirect(e)
}
// temporary store the Apple user's name so that it can be later retrieved with the authWithOAuth2 call
// (see https://github.com/pocketbase/pocketbase/issues/7090)
if data.AppleUser != "" && data.Error == "" && data.Code != "" {
+25 -2
View File
@@ -8,6 +8,7 @@ import (
"testing"
"time"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tests"
"github.com/pocketbase/pocketbase/tools/subscriptions"
@@ -16,9 +17,9 @@ import (
func TestRecordAuthWithOAuth2Redirect(t *testing.T) {
t.Parallel()
clientStubs := make([]map[string]subscriptions.Client, 0, 10)
clientStubs := make([]map[string]subscriptions.Client, 0, 11)
for i := 0; i < 10; i++ {
for i := 0; i < 11; i++ {
c1 := subscriptions.NewDefaultClient()
c2 := subscriptions.NewDefaultClient()
@@ -335,6 +336,28 @@ func TestRecordAuthWithOAuth2Redirect(t *testing.T) {
}
},
},
{
Name: "client with different IP",
Method: http.MethodGet,
URL: "/api/oauth2-redirect?code=123&state=" + clientStubs[10]["c3"].Id(),
Headers: map[string]string{"x-test-ip": "127.0.0.2"},
BeforeTestFunc: func(t testing.TB, app *tests.TestApp, e *core.ServeEvent) {
app.Settings().TrustedProxy.Headers = []string{"x-test-ip"}
clientStubs[10]["c3"].Set(apis.RealtimeClientIPKey, "127.0.0.1")
beforeTestFunc(clientStubs[10], map[string][]string{
"c3": {`"state":"` + clientStubs[10]["c3"].Id(), `"code":"123"`},
})(t, app, e)
},
ExpectedStatus: http.StatusTemporaryRedirect,
ExpectedEvents: map[string]int{"*": 0},
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
app.Store().Get("cancelFunc").(context.CancelFunc)()
checkFailureRedirect(t, app, res)
},
},
}
for _, scenario := range scenarios {
+233 -13
View File
@@ -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)
+20 -10
View File
@@ -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)
})
}
+43 -7
View File
@@ -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))
}
},
},
{
+20
View File
@@ -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)
+7
View File
@@ -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
+36
View File
@@ -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
View File
@@ -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 {
+36
View File
@@ -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
}
+61
View File
@@ -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)
}
}
})
}
}
+5
View File
@@ -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
View File
@@ -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
View File
@@ -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
+20
View File
@@ -348,6 +348,7 @@ func (app *BaseApp) registerCollectionHooks() {
}
// @todo experiment eventually replacing the rules *string with a struct?
// @todo consider changing the Indexes field to a "getter" for the sqlite_master table?
type baseCollection struct {
BaseModel
@@ -820,6 +821,25 @@ func onCollectionSave(e *CollectionEvent) error {
e.Collection.updateGeneratedIdIfExists(e.App)
// normalize indexes table name
for i, raw := range e.Collection.Indexes {
parsed := dbutils.ParseIndex(raw)
// no need to normalize
if parsed.TableName == e.Collection.Name {
continue
}
parsed.TableName = e.Collection.Name
normalized := parsed.Build()
if normalized == "" {
continue // leave to the model validator to decide whether to return an error
}
e.Collection.Indexes[i] = normalized
}
return e.Next()
}
+1 -1
View File
@@ -60,7 +60,7 @@ func (m *Collection) setDefaultAuthOptions() {
},
MFA: MFAConfig{
Enabled: false,
Duration: 1800, // 30min
Duration: 600, // 10min
},
OTP: OTPConfig{
Enabled: false,
+37
View File
@@ -1678,3 +1678,40 @@ func TestCollectionSaveViewWrapping(t *testing.T) {
})
}
}
func TestCollectionSaveIndexesTableNameNormalization(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
dummyCollection := core.NewBaseCollection("new_test")
dummyCollection.Fields.Add(&core.TextField{Name: "test"})
dummyCollection.Indexes = []string{
"create index `new_test_idx1` on `` (`test`) where 1=1",
"create index `new_test_idx2` on `test` (`test`) where 1=2",
"create index `new_test_idx3` on `someting_else` (`test`) where 1=3",
}
err := app.Save(dummyCollection)
if err != nil {
t.Fatal(err)
}
// refetch a clean state
dummyCollection, err = app.FindCollectionByNameOrId(dummyCollection.Name)
if err != nil {
t.Fatal(err)
}
if len(dummyCollection.Indexes) != 3 {
t.Fatalf("Expected 3 indexes, got %v", dummyCollection.Indexes)
}
for _, raw := range dummyCollection.Indexes {
parsed := dbutils.ParseIndex(raw)
if parsed.TableName != dummyCollection.Name {
t.Fatalf("Expected all indexes to have tableName %q, found %q:\n%s", dummyCollection.Name, parsed.TableName, raw)
}
}
}
+4 -2
View File
@@ -306,8 +306,10 @@ func dropCollectionIndexes(app App, collection *Collection) error {
for _, raw := range collection.Indexes {
parsed := dbutils.ParseIndex(raw)
if !parsed.IsValid() {
continue
// note: don't check IsValid because the index table name may not be populated
// (https://github.com/pocketbase/pocketbase/issues/7689)
if parsed.IndexName == "" {
return fmt.Errorf("failed to dop index - missing index name: %s", raw)
}
_, err := txApp.DB().NewQuery(fmt.Sprintf("DROP INDEX IF EXISTS [[%s]]", parsed.IndexName)).Execute()
+46
View File
@@ -294,3 +294,49 @@ func TestSingleVsMultipleValuesNormalization(t *testing.T) {
})
}
}
func TestDropIndexWithoutTableName(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
properIndex := "CREATE INDEX `new_test_idx2` ON `new_test` (`test`)"
indexWithoutTableName := "CREATE INDEX `new_test_idx2` ON `` (`test`)"
dummyCollection := core.NewBaseCollection("new_test")
dummyCollection.Fields.Add(&core.TextField{Name: "test"})
dummyCollection.Indexes = []string{properIndex}
err := app.Save(dummyCollection)
if err != nil {
t.Fatal(err)
}
// resave without table name but without hooks to avoid the normalizations
dummyCollection.Indexes[0] = indexWithoutTableName
err = app.UnsafeWithoutHooks().Save(dummyCollection)
if err != nil {
t.Fatal(err)
}
dummyCollection, err = app.FindCollectionByNameOrId(dummyCollection.Name)
if err != nil {
t.Fatal(err)
}
// resave should normalize the index
err = app.Save(dummyCollection)
if err != nil {
t.Fatal(err)
}
dummyCollection, err = app.FindCollectionByNameOrId(dummyCollection.Name)
if err != nil {
t.Fatal(err)
}
if len(dummyCollection.Indexes) != 1 || dummyCollection.Indexes[0] != properIndex {
t.Fatalf("Expected exactly 1 index\n%s\ngot\n%v", properIndex, dummyCollection.Indexes)
}
}
+17 -1
View File
@@ -448,8 +448,24 @@ type RealtimeConnectRequestEvent struct {
Client subscriptions.Client
// note: modifying it after the connect has no effect
// IdleTimeout specifies the max duration to wait for a new message
// before closing the connection.
//
// Modifying the value after the connection has been established has no effect.
//
// Defaults to 5 minutes.
IdleTimeout time.Duration
// MaxTimeout specifies the maximum duration a realtime connection
// can remain open (including even if there are ongoing messages).
//
// Once the specified duration expires, the current connection will
// be terminated, until a client reconnect is issued (if the client is still active).
//
// Modifying the value after the connection has been established has no effect.
//
// Defaults to 30 minutes.
MaxTimeout time.Duration
}
type RealtimeMessageEvent struct {
+38
View File
@@ -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,
})
}
+101
View File
@@ -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)
}
})
}
+24
View File
@@ -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
}
+66
View File
@@ -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
View File
@@ -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,
)
}
}
+61
View File
@@ -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))
}
}
}
+210
View File
@@ -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
}
+185
View File
@@ -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])
}
}
+27
View File
@@ -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,
})
}
+61
View File
@@ -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))
}
}
}
+3 -2
View File
@@ -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)
+11 -2
View File
@@ -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)
+42
View File
@@ -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
View File
@@ -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)),
),
)
}
+9 -5
View File
@@ -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",
+2 -2
View File
@@ -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 {
+28
View File
@@ -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")
}
+29
View File
@@ -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)
}
})
}
}
+7
View File
@@ -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
}
+10 -10
View File
@@ -17,12 +17,12 @@ require (
github.com/pocketbase/tygoja v0.0.0-20250812183945-97ffe055281f
github.com/spf13/cast v1.10.0
github.com/spf13/cobra v1.10.2
golang.org/x/crypto v0.50.0
golang.org/x/image v0.39.0
golang.org/x/net v0.53.0
golang.org/x/crypto v0.52.0
golang.org/x/image v0.41.0
golang.org/x/net v0.55.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.1
)
require (
@@ -35,15 +35,15 @@ 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
golang.org/x/mod v0.34.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/tools v0.43.0 // indirect
modernc.org/libc v1.72.0 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/tools v0.44.0 // indirect
modernc.org/libc v1.72.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
+26 -26
View File
@@ -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=
@@ -82,40 +82,40 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
@@ -124,18 +124,18 @@ modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/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.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w=
modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
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=
+120
View File
@@ -0,0 +1,120 @@
package migrations
import (
"fmt"
"strings"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/dbutils"
)
// see https://github.com/pocketbase/pocketbase/issues/7689
func init() {
core.SystemMigrations.Register(func(txApp core.App) error {
collections, err := txApp.FindAllCollections()
if err != nil {
return err
}
for _, collection := range collections {
// existing system collection indexes can't be modified and view don't have indexes
if collection.System || collection.IsView() {
continue
}
masterIndexes := []struct {
Name string `db:"name"`
SQL string `db:"sql"`
}{}
err := txApp.DB().Select("name", "sql").
From("sqlite_master").
AndWhere(dbx.HashExp{
"type": "index",
"tbl_name": collection.Name,
}).
AndWhere(dbx.NewExp("sql IS NOT NULL AND name NOT LIKE 'sqlite_autoindex_%'")).
All(&masterIndexes)
if err != nil {
return err
}
// no indexes
if len(masterIndexes) == 0 && len(collection.Indexes) == 0 {
continue
}
missingParsedIndexes := map[string]dbutils.Index{}
// find missing master indexes
masterLoop:
for _, masterIndex := range masterIndexes {
mParsed := dbutils.ParseIndex(masterIndex.SQL)
mParsed.SchemaName = ""
mParsed.TableName = collection.Name
for _, raw := range collection.Indexes {
cParsed := dbutils.ParseIndex(raw)
// index already exists (if needed it will be normalized on resave)
if cParsed.IndexName != "" && strings.EqualFold(cParsed.IndexName, mParsed.IndexName) {
continue masterLoop
}
}
missingParsedIndexes[masterIndex.Name] = mParsed
}
missingIndexesLoop:
for _, missing := range missingParsedIndexes {
missingSQL := missing.Build()
// it shouldn't be possible but for just in case if there is an edge case the regex doesn't cover
if missingSQL == "" {
return fmt.Errorf("failed to build sqlite_master index: %v", missing)
}
// drop the missing index to recreate later
_, err := txApp.DB().DropIndex(missing.TableName, missing.IndexName).Execute()
if err != nil {
return fmt.Errorf("failed to drop index %s: %w", missing.IndexName, err)
}
// no recreate: duplicated single unique tokenKey or email
// (auth collections are guaranteed to have them)
if collection.IsAuth() && missing.Unique && len(missing.Columns) == 1 &&
(strings.EqualFold(missing.Columns[0].Name, core.FieldNameTokenKey) || strings.EqualFold(missing.Columns[0].Name, core.FieldNameEmail)) {
continue missingIndexesLoop
}
// no recreate: the same index definition alreay exists
// in the collection but with different name
for _, raw := range collection.Indexes {
cParsed := dbutils.ParseIndex(raw)
cParsed.IndexName = missing.IndexName
cParsed.SchemaName = missing.SchemaName
cParsed.TableName = missing.TableName
cSQL := cParsed.Build()
if missingSQL == cSQL {
continue missingIndexesLoop
}
}
// recreate: add the missing index to the collection list and
// leave the user to decide whether they want to keep it or not
// (the index could have been previously created externally, e.g. via the sqlite3 cli)
collection.Indexes = append(collection.Indexes, missingSQL)
}
// resave to trigger indexes normalization
err = txApp.Save(collection)
if err != nil {
return err
}
}
return nil
}, nil)
}
+2 -2
View File
@@ -10,8 +10,8 @@ import (
)
const (
expectedDriverVersion = "v1.49.1"
expectedLibcVersion = "v1.72.0"
expectedDriverVersion = "v1.50.1"
expectedLibcVersion = "v1.72.3"
// ModerncDepsCheckHookId is the id of the hook that performs the modernc.org/* deps checks.
// It could be used for removing/unbinding the hook if you don't want the checks.
+16 -10
View File
@@ -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
File diff suppressed because it is too large. Load diff
+4 -4
View File
@@ -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": ""
},
Binary file not shown.
Binary file not shown.
+4 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+2 -2
View File
@@ -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
+6
View File
@@ -168,6 +168,12 @@ func TestFetch(t *testing.T) {
true,
nil,
},
{
"matching kid (no alg)",
"abc",
true,
nil,
},
{
"matching kid",
"def",
+14 -4
View File
@@ -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
}
+83 -8
View File
@@ -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
View File
@@ -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>",
+1 -1
View File
@@ -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 {
+14 -13
View File
@@ -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.2"
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.2"
@@ -1,4 +1,4 @@
import{t as e}from"./expandInfo-DGS0CLSa.js";import{t as n}from"./fieldsInfo-Bz62125-.js";function r(e){let n=app.utils.getApiExampleURL(),r=[{title:`OTP request`,content:i},{title:`OTP auth`,content:a}],o=store({activeActionIndex:0});return t.div({pbEvent:`apiPreviewAuthWithOTP`,className:`content`},t.p(null,`Authenticate with an one-time/short-lived password (OTP).`),t.p(null,`Note that when requesting an OTP we return an `,t.code(null,`otpId`),` even if a user with the provided email doesn't exist as a very basic enumeration protection.`),app.components.codeBlockTabs({className:`sdk-examples m-t-sm`,historyKey:`pbLastSDK`,tabs:[{title:`JS SDK`,language:`js`,value:`
import{t as e}from"./expandInfo-DGS0CLSa.js";import{t as n}from"./fieldsInfo-Bz62125-.js";function r(e){let n=app.utils.getApiExampleURL(),r=[{title:`OTP request`,content:i},{title:`OTP auth`,content:a}],o=store({activeActionIndex:0});return t.div({pbEvent:`apiPreviewAuthWithOTP`,className:`content`},t.p(null,`Authenticate with an one-time/short-lived password (OTP).`),t.p(null,`On successful authentication the user will be also marked as verified (if the OTP source is email and the user is not verified already).`),t.p(null,`Note that when requesting an OTP we return an `,t.code(null,`otpId`),` even if a user with the provided email doesn't exist as a very basic enumeration protection.`),app.components.codeBlockTabs({className:`sdk-examples m-t-sm`,historyKey:`pbLastSDK`,tabs:[{title:`JS SDK`,language:`js`,value:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${n}');
@@ -1,4 +1,4 @@
function e(e){let i=app.utils.getApiExampleURL(),a=[{title:`Request email change`,content:n},{title:`Confirm email change`,content:r}],o=store({activeActionIndex:0});return t.div({pbEvent:`apiPreviewEmailChange`,className:`content`},t.p(null,`Sends ${e.name} email change request.`),t.p(null,`On successful email change all previously issued auth tokens for the specific record will be automatically invalidated.`),app.components.codeBlockTabs({className:`sdk-examples m-t-sm`,historyKey:`pbLastSDK`,tabs:[{title:`JS SDK`,language:`js`,value:`
function e(e){let i=app.utils.getApiExampleURL(),a=[{title:`Request email change`,content:n},{title:`Confirm email change`,content:r}],o=store({activeActionIndex:0});return t.div({pbEvent:`apiPreviewEmailChange`,className:`content`},t.p(null,`Sends ${e.name} email change request.`),t.p(null,`On successful email change all previously issued auth tokens for the specific record will be invalidated (and the user will be marked as verified if not already).`),app.components.codeBlockTabs({className:`sdk-examples m-t-sm`,historyKey:`pbLastSDK`,tabs:[{title:`JS SDK`,language:`js`,value:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${i}');
@@ -16,7 +16,8 @@ function e(e){let i=app.utils.getApiExampleURL(),a=[{title:`Request email change
// (optional) in your custom confirmation page:
// ---
// note: after this call all previously issued auth tokens are invalidated
// note: all previous user auth tokens will be invalidated
// (and the user will be marked as verified if not already)
await pb.collection('${e.name}').confirmEmailChange(
'EMAIL_CHANGE_TOKEN',
'YOUR_PASSWORD',
@@ -39,7 +40,8 @@ function e(e){let i=app.utils.getApiExampleURL(),a=[{title:`Request email change
// (optional) in your custom confirmation page:
// ---
// note: after this call all previously issued auth tokens are invalidated
// note: all previous user auth tokens will be invalidated
// (and the user will be marked as verified if not already)
await pb.collection('${e.name}').confirmEmailChange(
'EMAIL_CHANGE_TOKEN',
'YOUR_PASSWORD',
@@ -53,6 +55,9 @@ function e(e){let i=app.utils.getApiExampleURL(),a=[{title:`Request email change
'${i}/api/collections/${e.name}/request-email-change'
# Confirm email change
#
# note: all previous user auth tokens will be invalidated
# (and the user will be marked as verified if not already)
curl -X POST \\
-H 'Content-Type:application/json' \\
-d '{ "token":"...", "password":"" }' \\
@@ -1,4 +1,4 @@
function e(e){let i=app.utils.getApiExampleURL(),a=[{title:`Request password reset`,content:n},{title:`Confirm password reset`,content:r}],o=store({activeActionIndex:0});return t.div({pbEvent:`apiPreviewPasswordReset`,className:`content`},t.p(null,`Sends ${e.name} password reset email request.`),t.p(null,`On successful password reset all previously issued auth tokens for the specific record will be automatically invalidated.`),app.components.codeBlockTabs({className:`sdk-examples m-t-sm`,historyKey:`pbLastSDK`,tabs:[{title:`JS SDK`,language:`js`,value:`
function e(e){let i=app.utils.getApiExampleURL(),a=[{title:`Request password reset`,content:n},{title:`Confirm password reset`,content:r}],o=store({activeActionIndex:0});return t.div({pbEvent:`apiPreviewPasswordReset`,className:`content`},t.p(null,`Sends ${e.name} password reset email request.`),t.p(null,`On successful password reset all previously issued auth tokens for the specific record will be invalidated (and the user will be marked as verified if not already).`),app.components.codeBlockTabs({className:`sdk-examples m-t-sm`,historyKey:`pbLastSDK`,tabs:[{title:`JS SDK`,language:`js`,value:`
import PocketBase from 'pocketbase';
const pb = new PocketBase('${i}');
@@ -11,7 +11,8 @@ function e(e){let i=app.utils.getApiExampleURL(),a=[{title:`Request password res
// (optional) in your custom confirmation page:
// ---
// note: after this call all previously issued auth tokens are invalidated
// note: all previous user auth tokens will be invalidated
// (and the user will be marked as verified if not already)
await pb.collection('${e.name}').confirmPasswordReset(
'RESET_TOKEN',
'NEW_PASSWORD',
@@ -30,7 +31,8 @@ function e(e){let i=app.utils.getApiExampleURL(),a=[{title:`Request password res
// (optional) in your custom confirmation page:
// ---
// note: after this call all previously issued auth tokens are invalidated
// note: all previous user auth tokens will be invalidated
// (and the user will be marked as verified if not already)
await pb.collection('${e.name}').confirmPasswordReset(
'RESET_TOKEN',
'NEW_PASSWORD',
@@ -44,6 +46,9 @@ function e(e){let i=app.utils.getApiExampleURL(),a=[{title:`Request password res
'${i}/api/collections/${e.name}/request-password-reset'
# Confirm password reset
#
# note: all previous user auth tokens will be invalidated
# (and the user will be marked as verified if not already)
curl -X POST \\
-H 'Content-Type:application/json' \\
-d '{ "token":"...", "password":"", "passwordConfirm":"" }' \\
+83
View File
@@ -0,0 +1,83 @@
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-CJKph96j.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.dataset.rotate=!0,a.addEventListener(`animationend`,()=>{a.dataset.rotate=!1}),clearTimeout(i),i=setTimeout(()=>{clearTimeout(i),a.dataset.rotate=!1},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:void 0,hidden:void 0,in 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,`top`,`code`)})},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),e.indexes=e.indexes?.map(n=>app.utils.replaceIndexFields(n,{tableName:e.name||``}))}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 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
(list changed)`:``}`})),t.div({hidden:()=>!app.store.activeCollection?.id,pbEvent:`pageHeaderPrimaryBtns`,className:`page-header-primary-btns`},t.button({type:`button`,className:`btn outline api-preview-btn`,onclick:()=>app.modals.openApiPreview(app.store.activeCollection)},t.i({className:`ri-code-s-slash-line`,ariaHidden:!0}),t.span({className:`txt`,textContent:`API preview`})),()=>{if(app.store.activeCollection?.type!=`view`)return t.button({type:`button`,className:`btn new-record-btn`,onclick:()=>app.modals.openRecordUpsert(app.store.activeCollection)},t.i({className:`ri-add-line`,ariaHidden:!0}),t.span({className:`txt`,textContent:`New record`}))})),t.div({hidden:()=>!app.store.isLoadingCollections||app.store.activeCollection?.id,className:`block txt-center p-base`},t.span({className:`loader lg`})),t.div({hidden:()=>app.store.isLoadingCollections||app.store.activeCollection?.id,className:`block txt-center p-base`},t.h6({className:`txt`},()=>app.store.collections?.length?`Select collection from the sidebar.`:`No collections found.`)),app.components.recordsSearchbar({hidden:()=>!app.store.activeCollection?.id,collection:()=>app.store.activeCollection,value:()=>r.filter,onsubmit:e=>r.filter=e}),app.components.recordsList({className:`m-t-sm`,reset:()=>r.reset,hidden:()=>!app.store.activeCollection?.id,collection:()=>app.store.activeCollection,filter:()=>r.filter,sort:()=>r.sort,onselect:e=>{r.activeRecordIdOrModel=e},onchange:(e,n)=>{r.filter=e,r.sort=n},suggestReset:()=>r.suggestReset,onSuggestResetChange:e=>{r.suggestReset=!!e}}),t.footer({className:`page-footer`},t.span({className:()=>`total-count ${r.isTotalCountLoading?`faded`:``}`},`Total: `,()=>r.totalCount),app.components.credits())))}function Mr(){return document.body.clientWidth>600&&document.body.clientHeight>=720?150:100}function Nr(e){let n=store({stats:[]});async function r(r){if(r){e.isChartLoading=!0;try{let i=(e.presets||[]).concat(app.utils.normalizeSearchFilter(e.filter,[`level`,`message`,`data`])),a=await app.pb.logs.getStats({filter:i.filter(Boolean).map(e=>`(`+e+`)`).join(`&&`)}),o=a.length,s=[],c=[];e.totalFound=0;for(let n=0;n<o;n++){let r=new Date(a[n].date.replace(` `,`T`)).getTime()/1e3;s.push(r),c.push(a[n].total),e.totalFound+=a[n].total,a[n+1]?.date?new Date(a[n+1].date.replace(` `,`T`)).getTime()/1e3-r>3600&&(s.push(r+3600),c.push(0)):n+1==o&&(s.push(r+3600),c.push(0))}n.stats=a,e.isChartLoading=!1,Fr(r,[s,c],e)}catch(n){n?.isAbort||(e.isChartLoading=!1,console.warn(`failed to load logs chart:`,n))}}}let i=[];return t.div({pbEvent:`logsChart`,className:()=>[`logs-chart`,e.isChartLoading?`loading`:null,e.zoom?.min&&e.zoom?.max?`zoomed`:``,!n.stats.length||!e.isFirstLoadReady?`nodata`:null].filter(Boolean).join(` `),onmount:n=>{i.push(watch(()=>[e.reset,e.filter,e.presets?.length],()=>r(n))),n._resizeChartFunc=()=>{clearTimeout(n._resizeTimeoutId),n._resizeTimeoutId=setTimeout(()=>{n?._uplot&&n._uplot.setSize({width:n.clientWidth,height:Mr()})},100)},window.addEventListener(`resize`,n._resizeChartFunc)},onunmount:e=>{i.forEach(e=>e?.unwatch()),e._uplot?.destroy(),e._resizeChartFunc&&(clearTimeout(e._resizeTimeoutId),window.removeEventListener(`resize`,e._resizeChartFunc),e._resizeChartFunc=null,e._resizeTimeoutId=null)}},t.button({type:`button`,className:()=>`logs-reset-zoom-ctrl ${e.zoom?.min&&e.zoom?.max?``:`hidden`}`,onclick(){e.zoom={}}},t.div({className:`content-primary`},`Reset zoom`),t.div({className:`content-secondary`},`(drag the timeline to pan)`)),t.span({hidden:()=>!e.isChartLoading,className:()=>`loader logs-chart-loader`}))}function Pr(e){let n=e?.data?.[0];n&&e.setScale(`x`,{min:n[0],max:n[n.length-1]})}function Fr(e,n,r){let i=window.getComputedStyle(e),a=i.getPropertyValue(`--surfaceTxtHintColor`),o=i.getPropertyValue(`--surfaceAlt1Color`),s=i.getPropertyValue(`--surfaceAlt4Color`),c=i.getPropertyValue(`--surfaceAlt2Color`),l={width:e.clientWidth,height:Mr(),legend:{show:!1},cursor:{x:!1,y:!1},scales:{x:{range:(e,r,i)=>r<n[0][0]||i>n[0][n[0].length-1]?[e.scales.x.min,e.scales.x.max]:[r,i]},y:{range:{min:{pad:.2,soft:0,mode:1},max:{pad:.2,soft:0,mode:2}}}},series:[{},{paths:uPlot.paths.stepped({align:1}),points:{show:!1,size:1},width:2,fill:c,stroke:s}],axes:[{show:!0,size:35,lineGap:1,space:60,stroke:a,incrs:[300,600,900,1800,3600,3600*2,3600*3,3600*4,3600*6,3600*12,86400,86400*2],values:[[3600,`{h}{aa}`,`
{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.originalFormSettings?.superuserIPs),r=app.utils.toArray(e.formSettings?.superuserIPs);return!r.length||JSON.stringify(n)==JSON.stringify(r)?a():app.modals.confirm(t.div({className:`txt-center`},t.h6(null,`The ONLY allowed superuser IPs will change to: `,t.br(),t.strong(null,r.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`})}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:`wrap 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
+1
View File
@@ -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
-1
View File
@@ -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
-81
View File
@@ -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 N=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>N){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{docsBatch: 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=kt(e,n,r);i&&(document.body.appendChild(i),app.modals.open(i))};var Tt=[`expand`];function Et(e,n=Tt){if(n.find(n=>e[n]!==void 0)){e=Object.assign({},e);for(let r of n)delete e[r]}return e}function Dt(e){e=Et(e),app.utils.copyToClipboard(JSON.stringify(e,null,2)),app.toasts.success(`Record copied to clipboard!`)}function Ot(e){return e?JSON.stringify(e):``}var B=`main`,V=`authProviders`;function kt(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:B,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 Ot(s.initialDraft)},get recordHash(){return Ot(s.record)},get originalRecordHash(){return Ot(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 K(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 Jt(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 Yt(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 Xt(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 Zt(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 Qt(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:Zt,input:Xt,view:Qt,filterModifiers:e=>[`lower`],dummyData:(e,n=!1)=>e.primaryKey?app.utils.randomString(15):`example text`};function $t(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 en(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 J(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:ln,input:cn,view:J,filterModifiers:e=>[`lower`],dummyData:(e,n=!1)=>`test_${app.utils.randomString(3,`123567890`)}@example.com`};function un(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 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+`.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 dn(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:Y,input:un,view:dn,dummyData:(e,n=!1)=>`https://example.com`};function fn(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 pn(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 mn(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:pn,input:fn,view:mn,dummyData:(e,n=!1)=>new Date().toISOString().replaceAll(`T`,` `)};function hn(e){delete e.clone[e.field.name]}function gn(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 On(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:Dn,input:En,view:On,filterModifiers:e=>e.maxSelect>1?[`each`,`length`]:[],dummyData:(e,n=!1)=>e.maxSelect>1?e.values?.slice(0,2)||[]:e.values?.[0]||``};function kn(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:()=>An(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:()=>!An(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 An(e){if(e===``)return!0;try{return JSON.parse(e),!0}catch{return!1}}function jn(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 Mn(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 Jn=`test_view_query`;function Yn(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:Jn});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(Jn),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 Zn(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 Qn(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 $n(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 dr(){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=fr(e);n&&(document.body.appendChild(n),app.modals.open(n))};function fr(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.`),dr())),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 pr(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=_r(e,n);r&&(document.body.appendChild(r),app.modals.open(r))};var mr=[`execTime`,`type`,`auth`,`authId`,`status`,`method`,`url`,`referer`,`remoteIP`,`userIP`,`userAgent`,`error`,`details`];function hr(e){app.utils.downloadJSON(e,`log_`+e.created.replaceAll(/[-:\. ]/gi,``)+`.json`)}function gr(e){app.utils.copyToClipboard(JSON.stringify(e,null,2)),app.toasts.success(`Log copied to clipboard!`)}function _r(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:[Vr(),zr(r),Br(r)]};e._uplot?.destroy(),e._uplot=new uPlot(l,n,e)}function zr(e){let n;return{hooks:{init:r=>{r.over.ondblclick=n=>{e.zoom={}},n=watch(()=>{!e.zoom?.min||!e.zoom?.max?Lr(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 Br(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 Vr(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} ${Hr(a)}-${Hr(o)}`,n.left=Math.round(e.valToPos(r,`x`)),n.top=Math.round(e.valToPos(i,`y`))}}}}function Hr(e){let n=e.getHours(),r=n>=12?`pm`:`am`;return n=n%12||12,n+r}var Ur=50;function Wr(e){let n=store({logs:[],lastLoadCount:0,lastPage:1,bulkSelected:{},get canLoadMore(){return n.lastLoadCount>=Ur},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,Ur,{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() 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 ei(){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=Zr(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:[]}},Zr(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`},Kr(),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`},()=>ti(e,e.isSaving)),t.div({className:`col-lg-12`},()=>$r(e),()=>Qr(e),()=>qr(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.check 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 ti(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 ni(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 ri(e={oncreated:null}){let n=ii(e);n&&(document.body.appendChild(n),app.modals.open(n))}function ii(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 ai(e){let n=oi(e);document.body.appendChild(n),app.modals.open(n)}function oi(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
+2 -2
View File
@@ -13,9 +13,9 @@
<!-- prism -->
<script src="./libs/prism/prism.js" data-manual></script>
<script type="module" crossorigin src="./assets/index-Un_20Bdf.js"></script>
<script type="module" crossorigin src="./assets/index-B2xHaK_e.js"></script>
<link rel="modulepreload" crossorigin href="./assets/pocketbase.es-B_4DUNUU.js">
<link rel="stylesheet" crossorigin href="./assets/index-Cdd8TSHO.css">
<link rel="stylesheet" crossorigin href="./assets/index-Cbzik3Gc.css">
</head>
<body>
</body>
+1 -1
View File
@@ -1,3 +1,3 @@
(function(e){let t,n=new Set,r=new Map,i=[],a,o=Symbol(),s=Symbol(),c=Symbol(),l=Symbol(),u=Symbol(),d=Symbol(),f=Symbol(),p=Symbol(),m=Symbol(),h=Symbol();function g(e,n){let d={[o]:`_`+Math.random(),[p]:e,[m]:n};return r.set(d[o],d),d.run=()=>{let e;t&&(e=t,d[s]=t[o],t[c]=t[c]||[],t[c].push(d[o])),d[l]?.forEach(e=>{e.delete(d[o])}),t=d;let n=d[p](d[h],d);d[m]&&(t=null,d[m](n,d[h],d)),d[h]=n,t=e},d.unwatch=function(){d[u]=1,i.push(d[o]),a&&clearTimeout(a),a=setTimeout(()=>{for(let e of i)_(e);i=[],a=null},50)},d.run(),d}function _(e){let t=r.get(e);if(t?.[d]?.(),t?.[c]){for(let e of t[c])_(e);t[s]=null,t[c]=null}if(t?.[l]){for(let n of t[l])n.delete(e);t[l]=null}r.delete(e)}function v(e){return y(e,new Map)}function y(e,n){let r=typeof e==`object`&&!Array.isArray(e)?Object.getOwnPropertyDescriptors(e):{};return new Proxy(e,{get(e,i,a){if(typeof i==`symbol`)return e[i];if(i==`__raw`)return e;if(r[i]?.get&&!e[s]){let n=i;if(i=`@@`+i,!t){let t=r[n].get.call(e);return r[n]._watcher&&(a[i]=t),t}let s=t[o];if(r[n]._refs=r[n]._refs||new Set,!r[n]._refs.has(s)){r[n]._refs.add(s);let e=t[d];t[d]=()=>{e?.(),r[n]._refs.delete(s),!r[n]._refs.size&&r[n]._watcher&&_(r[n]._watcher[o])}}if(!r[n]._watcher){let o=t;t=null;let s=g(r[n].get.bind(e),t=>{e.hasOwnProperty(i)?a[i]=t:Object.defineProperty(e,i,{writable:!0,enumerable:!1,value:t})});r[n]._watcher=s,s[d]=()=>{r[n]._watcher=null},t=o}}let c=e?.[i];if(typeof c==`function`)return c;if(typeof c==`object`&&c&&!c[s]&&(c.constructor?.name==`Object`||c.constructor?.name==`Array`||c.constructor?.name==null)&&(c[s]=[a,i],c=y(c,n),e[i]=c),t){let r=b(e,i),a=t[o],s=[r];t[l]=t[l]||new Set;for(let e of s){let r=n.get(e);r||(r=new Set,n.set(e,r)),r.add(a),t[l].add(r)}}return c},set(e,t,r){if(typeof t==`symbol`)return e[t]=r,!0;let i=e[t];i?.[s]&&(i[f]=!0),r?.[s]&&Array.isArray(e)&&!isNaN(t)&&(r[s][1]=t);let a=!1;return i===void 0&&!e.hasOwnProperty(t)&&(a=!0),e[t]=r,a&&x(e,`toJSON`,n),(r!==i||t===`length`)&&x(e,t,n),!0},deleteProperty(e,t){if(typeof t!=`symbol`){x(e,t,n);let r=b(e,t);for(let e of n)(e[0]==r||e[0].startsWith(r+`/`))&&n.delete(e[0])}return delete e[t]}})}function b(e,t){let n=t,r=e?.[s];for(;r;)n=r[1]+`/`+n,r=r[0][s];return n}function x(e,t,i){let a=b(e,t),o=i.get(a);if(o)for(let e of o)n.delete(e),n.add(e),n.size==1&&queueMicrotask(()=>{let e={},t;for(let i of n)if(t=r.get(i),!(!t||t[u])&&!(t[s]&&n.has(t[s]))){if(e[i]=(e[i]||0)+1,e[i]>250){console.warn(`Possible infinite loop for watcher `+i+`:`,`
watch(`+t[p]?.toString()+(t[m]?`, `+t[m].toString():``)+`)`);continue}t.run()}n.clear()})}let S=new Proxy({},{get(e,t){return function(){return w(),O.call(void 0,t,...arguments)}}}),C=!1;function w(){if(C)return;C=!0;function e(t,n){for(let r of n)r[t]&&r[t](r),r.childNodes&&e(t,r.childNodes)}new MutationObserver(t=>{for(let n of t)e(`onmount`,n.addedNodes),e(`onunmount`,n.removedNodes)}).observe(document,{childList:!0,subtree:!0})}let T=Symbol(),E=Symbol(),D=Symbol();function O(e,t={},...n){let r=document.createElement(e);if(t)for(let e in t){let n=t[e],i=!1;e.length>5&&e.startsWith(`html-`)&&(i=!0,e=e.substring(5)),n===void 0?r.removeAttribute(e):typeof n!=`function`||e.length>2&&e.startsWith(`on`)?i?r.setAttribute(e,n):r[e]=n:(e==`rid`&&console.warn(`rid is provided as reactive function and will not have effect for `+r+`.
Consider using a plain number, string or object/array reference instead.`),r[T]=r[T]||[],r[T].push(()=>{if(!r)return;let t=n(r,e);t===void 0?(r[e]!==void 0&&(r[e]=void 0),r.removeAttribute(e)):i?r.setAttribute(e,t):r[e]=t}))}let i=r.onmount;r.onmount=()=>{if(!r[D]){if(r[D]=!0,r[T]){r[E]=r[E]||[];for(let e of r[T])r[E].push(g(e))}i?.(r)}};let a=r.onunmount;return r.onunmount=()=>{if(r[D]){if(r[D]=!1,r[E])for(let e of r[E])e.unwatch();r[E]=null,a?.(r)}},k(r,n),r}function k(e,t){t=N(t);for(let n of t)if(typeof n==`function`)A(e,n);else{let t=P(n);Array.isArray(t)?k(e,t):t&&e.appendChild(t)}}function A(e,t){let n=document.createComment(``);e.appendChild(n);let r=[],i=new Map,a=e.moveBefore||e.insertBefore;e[T]=e[T]||[],e[T].push(()=>{if(!e)return;let o=N(t(e)),s=o.length,c=new Map;if(!r?.length){let t=document.createDocumentFragment();for(let e=0;e<s;e++){o[e]=P(o[e]),t.appendChild(o[e]);let n=o[e].rid;n!==void 0&&(c.has(n)?console.warn(`Duplicated rid:`,n,o[e]):c.set(n,e))}e.insertBefore(t,n),t=null,r=o,i=c;return}let l=[],u=[],d=new Set,f=[];for(let e=0;e<s;e++){o[e]=P(o[e]);let t=o[e].rid;if(t!==void 0){c.has(t)?console.warn(`Duplicated rid:`,t,o[e]):c.set(t,e);let n=i.get(t);if(n>=0){d.add(r[n]),o[e]=r[n],f.push(n);continue}}u.push({child:o[e],prev:o[e-1]})}let p=j(f);f.length!=p.length&&f.forEach((e,t)=>{p.has(e)||l.push({child:r[e],targetPos:t})});let m,h,g;for(let t of l)m=r.findIndex(e=>e===t.child),h=t.targetPos,m<t.targetPos&&(h=t.targetPos+1),g=r[h]||n,M(r,m,t.targetPos),a.call(e,t.child,g);for(let e of u)e.prev?e.prev.after(e.child):(r[0]||n).before(e.child);for(let e=0;e<r.length;e++)d.has(r[e])||r[e].remove?.();r=o,i=c,o=null,c=null,d=null,l=null,u=null})}function j(e){let t=[],n=[];for(let r=0;r<e.length;r++){let i=e[r],a=0,o=0,s=t.length;for(;a<s;)o=Math.floor((a+s)/2),e[t[o]]>=i?s=o:a=o+1;a>0&&(n[r]=t[a-1]),t[a]=r}let r=new Set,i=t[t.length-1];for(;i!==void 0;)r.add(e[i]),i=n[i];return r}function M(e,t,n){if(t==n)return e;let r=t>n?-1:1,i=e[t];for(let i=t;i!=n;i+=r)e[i]=e[i+r];e[n]=i}function N(e){return e==null?[]:Array.isArray(e)?e:[e]}function P(e){return typeof e==`string`||typeof e==`number`||typeof e==`boolean`?document.createTextNode(e):e?.__raw===void 0?e:e.__raw}function F(e,t={fallbackPath:`#/`,transition:!0}){let n=L(e),r,i=()=>{let e=window.location.hash,i=I(n,e);if(!i){if(t.fallbackPath!=e){window.location.hash=t.fallbackPath;return}console.warn(`missing route:`,e);return}let a=async()=>{try{await r?.(),r=await i.handler(i)}catch(e){console.warn(`route navigation failed:`,e)}};t.transition&&document.startViewTransition?document.startViewTransition(a):a()};return window.addEventListener(`hashchange`,i),i(),()=>{window.removeEventListener(`hashchange`,i)}}function I(e,t){for(let n of e){let e=t.match(n.regex);if(!e)continue;let r={},i=t.split(`?`)?.[1];if(i){let e=new URLSearchParams(i);for(let[t,n]of e.entries())Array.isArray(r[t])||(r[t]=r[t]?[r[t]]:[]),r[t].push(n)}return Object.assign({path:t,query:r,params:e.groups||{}},n)}}function L(e){let t=[];for(let n in e){let r=n.split(`/`);for(let e in r)r[e].length>2&&r[e].startsWith(`{`)&&r[e].endsWith(`}`)?r[e]=`(?<`+r[e].substring(1,r[e].length-1)+`>[^\\/#?]+)`:r[e]=RegExp.escape(r[e]);t.push({regex:RegExp(`^`+r.join(`\\/`)+`(?:[?#].*)?$`),pattern:n,handler:e[n]})}return t}e.router=F,e.store=v,e.t=S,e.watch=g})(this.window=this.window||{});
Consider using a plain number, string or object/array reference instead.`),r[T]=r[T]||[],r[T].push(()=>{if(!r)return;let t=n(r,e);t===void 0?(r[e]!==void 0&&(r[e]=void 0),r.removeAttribute(e)):i?r.setAttribute(e,t):r[e]=t}))}let i=r.onmount;r.onmount=()=>{if(!r[D]){if(r[D]=!0,r[T]){r[E]=r[E]||[];for(let e of r[T])r[E].push(g(e))}i?.(r)}};let a=r.onunmount;return r.onunmount=()=>{if(r[D]){if(r[D]=!1,r[E])for(let e of r[E])e.unwatch();r[E]=null,a?.(r)}},k(r,n),r}function k(e,t){t=N(t);for(let n of t)if(typeof n==`function`)A(e,n);else{let t=P(n);Array.isArray(t)?k(e,t):t&&e.appendChild(t)}}function A(e,t){let n=document.createComment(``);e.appendChild(n);let r=[],i=new Map,a=e.moveBefore||e.insertBefore;e[T]=e[T]||[],e[T].push(()=>{if(!e)return;let o=N(t(e)),s=o.length,c=new Map;if(!r?.length){let t=document.createDocumentFragment();for(let e=0;e<s;e++){o[e]=P(o[e],!0),t.appendChild(o[e]);let n=o[e].rid;n!==void 0&&(c.has(n)?console.warn(`Duplicated rid:`,n,o[e]):c.set(n,e))}e.insertBefore(t,n),t=null,r=o,i=c;return}let l=[],u=[],d=new Set,f=[];for(let e=0;e<s;e++){o[e]=P(o[e],!0);let t=o[e].rid;if(t!==void 0){c.has(t)?console.warn(`Duplicated rid:`,t,o[e]):c.set(t,e);let n=i.get(t);if(n>=0){d.add(r[n]),o[e]=r[n],f.push(n);continue}}u.push({child:o[e],prev:o[e-1]})}let p=j(f);f.length!=p.length&&f.forEach((e,t)=>{p.has(e)||l.push({child:r[e],targetPos:t})});let m,h,g;for(let t of l)m=r.findIndex(e=>e===t.child),h=t.targetPos,m<t.targetPos&&(h=t.targetPos+1),g=r[h]||n,M(r,m,t.targetPos),a.call(e,t.child,g);for(let e of u)e.prev?e.prev.after(e.child):(r[0]||n).before(e.child);for(let e=0;e<r.length;e++)d.has(r[e])||r[e].remove?.();r=o,i=c,o=null,c=null,d=null,l=null,u=null})}function j(e){let t=[],n=[];for(let r=0;r<e.length;r++){let i=e[r],a=0,o=0,s=t.length;for(;a<s;)o=Math.floor((a+s)/2),e[t[o]]>=i?s=o:a=o+1;a>0&&(n[r]=t[a-1]),t[a]=r}let r=new Set,i=t[t.length-1];for(;i!==void 0;)r.add(e[i]),i=n[i];return r}function M(e,t,n){if(t==n)return e;let r=t>n?-1:1,i=e[t];for(let i=t;i!=n;i+=r)e[i]=e[i+r];e[n]=i}function N(e){return e==null?[]:Array.isArray(e)?e:[e]}function P(e,t=!1){return t&&e==null&&(e=``),typeof e==`string`||typeof e==`number`||typeof e==`boolean`?document.createTextNode(e):e?.__raw===void 0?e:e.__raw}function F(e,t={fallbackPath:`#/`,transition:!0}){let n=L(e),r,i=()=>{let e=window.location.hash,i=I(n,e);if(!i){if(t.fallbackPath!=e){window.location.hash=t.fallbackPath;return}console.warn(`missing route:`,e);return}let a=async()=>{try{await r?.(),r=await i.handler(i)}catch(e){console.warn(`route navigation failed:`,e)}};t.transition&&document.startViewTransition?document.startViewTransition(a):a()};return window.addEventListener(`hashchange`,i),i(),()=>{window.removeEventListener(`hashchange`,i)}}function I(e,t){for(let n of e){let e=t.match(n.regex);if(!e)continue;let r={},i=t.split(`?`)?.[1];if(i){let e=new URLSearchParams(i);for(let[t,n]of e.entries())Array.isArray(r[t])||(r[t]=r[t]?[r[t]]:[]),r[t].push(n)}return Object.assign({path:t,query:r,params:e.groups||{}},n)}}function L(e){let t=[];for(let n in e){let r=n.split(`/`);for(let e in r)r[e].length>2&&r[e].startsWith(`{`)&&r[e].endsWith(`}`)?r[e]=`(?<`+r[e].substring(1,r[e].length-1)+`>[^\\/#?]+)`:r[e]=RegExp.escape(r[e]);t.push({regex:RegExp(`^`+r.join(`\\/`)+`(?:[?#].*)?$`),pattern:n,handler:e[n]})}return t}e.router=F,e.store=v,e.t=S,e.watch=g})(this.window=this.window||{});
+1 -1
View File
@@ -1 +1 @@
.tox,.tox-tinymce,.tox-tinymce-aux{font-family:var(--baseFontFamily)}.tox,.tox .tox-dialog__body-content .accessibility-issue--info .tox-form__group h2{color:var(--surfaceTxtColor)}.tox .tox-button,.tox-tinymce{box-shadow:none;position:relative}.tox .tox-button,.tox .tox-button--secondary{background-position:0 0;background-repeat:repeat;font-weight:700;letter-spacing:normal;outline:0;text-decoration:none}.tox,.tox .tox-button,.tox .tox-button--secondary,.tox .tox-collection__group-heading{text-transform:none;font-style:normal}.tox .tox-dialog__table td:first-child,.tox .tox-dialog__table thead th:first-child{padding-right:8px}.tox{--mceIconScale:0.83;--headerToolbarColor:var(--surfaceColor);box-shadow:none;box-sizing:content-box;cursor:auto;font-size:var(--fontSize);font-weight:400;line-height:normal;-webkit-tap-highlight-color:transparent;text-decoration:none;text-shadow:none;vertical-align:initial;white-space:normal}.tox :not(svg):not(rect){box-sizing:inherit;color:inherit;cursor:inherit;direction:inherit;font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;line-height:inherit;-webkit-tap-highlight-color:inherit;text-align:inherit;text-decoration:inherit;text-shadow:inherit;text-transform:inherit;vertical-align:inherit;white-space:inherit;background:0 0;border:0;box-shadow:none;float:none;height:auto;margin:0;max-width:none;outline:0;padding:0;position:static;width:auto}.tox:not([dir=rtl]){direction:ltr;text-align:left}.tox[dir=rtl]{direction:rtl;text-align:right}.tox-tinymce{border:1px solid var(--surfaceAlt3Color);border-radius:var(--borderRadius);box-sizing:border-box;display:flex;flex-direction:column;overflow:hidden;visibility:inherit!important}.tox.tox-tinymce-inline{border:none;box-shadow:none;overflow:initial}.tox.tox-tinymce-inline .tox-editor-container{overflow:initial}.tox.tox-tinymce-inline .tox-editor-header{background-color:var(--surfaceColor);border:2px solid var(--surfaceAlt3Color);border-radius:var(--borderRadius);box-shadow:none;overflow:hidden}.tox .tox-button,.tox .tox-button[disabled]{border-color:var(--primaryColor);background-image:none}.tox-tinymce-aux{z-index:1300}.tox-tinymce :focus,.tox-tinymce-aux :focus{outline:0}button::-moz-focus-inner{border:0}.tox[dir=rtl] .tox-icon--flip svg{transform:rotateY(180deg)}.tox .accessibility-issue__header{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description{align-items:stretch;border-radius:var(--borderRadius);display:flex;justify-content:space-between}.tox .accessibility-issue__description>div{padding-bottom:4px}.tox .accessibility-issue__description>div>div{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description>div>div .tox-icon svg,.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__checked svg{display:block}.tox .accessibility-issue__repair{margin-top:16px}.tox .tox-dialog__body-content .accessibility-issue--info .accessibility-issue__description{background-color:rgba(0,101,216,.1);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--info .tox-icon svg{fill:var(--primaryColor)}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon{background-color:var(--primaryColor);color:var(--primaryTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:hover{background-color:#0060ce}.tox .tox-button,.tox .tox-button[disabled],.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:active{background-color:var(--primaryColor)}.tox .tox-dialog__body-content .accessibility-issue--warn .accessibility-issue__description{background-color:rgba(255,165,0,.08);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-form__group h2{color:#8f5d00}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-icon svg{fill:#8f5d00}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon{background-color:#ffe89d;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:hover{background-color:#f2d574;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:active{background-color:#e8c657;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--error .accessibility-issue__description{background-color:rgba(204,0,0,.1);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--error .tox-form__group h2{color:#c00}.tox .tox-control-wrap__status-icon-invalid svg,.tox .tox-dialog__body-content .accessibility-issue--error .tox-icon svg{fill:#c00}.tox Line truncated
.tox,.tox .tox-button,.tox .tox-button--secondary,.tox .tox-button-link,.tox .tox-button-link--sm{font-size:var(--fontSize)}.tox,.tox .tox-button,.tox .tox-button-link,.tox .tox-dialog__title,.tox-tinymce,.tox-tinymce-aux{font-family:var(--baseFontFamily)}.tox,.tox .tox-dialog__body-content .accessibility-issue--info .tox-form__group h2{color:var(--surfaceTxtColor)}.tox .tox-button,.tox-tinymce{box-shadow:none;position:relative}.tox .tox-button,.tox .tox-button--secondary{background-position:0 0;background-repeat:repeat;font-weight:700;letter-spacing:normal;outline:0;text-decoration:none}.tox,.tox .tox-button,.tox .tox-button--secondary,.tox .tox-collection__group-heading{text-transform:none;font-style:normal}.tox .tox-dialog__table td:first-child,.tox .tox-dialog__table thead th:first-child{padding-right:8px}.tox{--mceIconScale:0.83;--headerToolbarColor:var(--surfaceColor);box-shadow:none;box-sizing:content-box;cursor:auto;font-weight:400;line-height:normal;-webkit-tap-highlight-color:transparent;text-decoration:none;text-shadow:none;vertical-align:initial;white-space:normal}.tox :not(svg):not(rect){box-sizing:inherit;color:inherit;cursor:inherit;direction:inherit;font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;line-height:inherit;-webkit-tap-highlight-color:inherit;text-align:inherit;text-decoration:inherit;text-shadow:inherit;text-transform:inherit;vertical-align:inherit;white-space:inherit;background:0 0;border:0;box-shadow:none;float:none;height:auto;margin:0;max-width:none;outline:0;padding:0;position:static;width:auto}.tox:not([dir=rtl]){direction:ltr;text-align:left}.tox[dir=rtl]{direction:rtl;text-align:right}.tox-tinymce{border:1px solid var(--surfaceAlt3Color);border-radius:var(--borderRadius);box-sizing:border-box;display:flex;flex-direction:column;overflow:hidden;visibility:inherit!important}.tox.tox-tinymce-inline{border:none;box-shadow:none;overflow:initial}.tox.tox-tinymce-inline .tox-editor-container{overflow:initial}.tox.tox-tinymce-inline .tox-editor-header{background-color:var(--surfaceColor);border:2px solid var(--surfaceAlt3Color);border-radius:var(--borderRadius);box-shadow:none;overflow:hidden}.tox .tox-button,.tox .tox-button[disabled]{border-color:var(--primaryColor);background-image:none}.tox-tinymce-aux{z-index:1300}.tox-tinymce :focus,.tox-tinymce-aux :focus{outline:0}button::-moz-focus-inner{border:0}.tox[dir=rtl] .tox-icon--flip svg{transform:rotateY(180deg)}.tox .accessibility-issue__header{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description{align-items:stretch;border-radius:var(--borderRadius);display:flex;justify-content:space-between}.tox .accessibility-issue__description>div{padding-bottom:4px}.tox .accessibility-issue__description>div>div{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description>div>div .tox-icon svg,.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__checked svg{display:block}.tox .accessibility-issue__repair{margin-top:16px}.tox .tox-dialog__body-content .accessibility-issue--info .accessibility-issue__description{background-color:rgba(0,101,216,.1);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--info .tox-icon svg{fill:var(--primaryColor)}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon{background-color:var(--primaryColor);color:var(--primaryTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:hover{background-color:#0060ce}.tox .tox-button,.tox .tox-button[disabled],.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:active{background-color:var(--primaryColor)}.tox .tox-dialog__body-content .accessibility-issue--warn .accessibility-issue__description{background-color:rgba(255,165,0,.08);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-form__group h2{color:#8f5d00}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-icon svg{fill:#8f5d00}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon{background-color:#ffe89d;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:hover{background-color:#f2d574;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:active{background-color:#e8c657;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--error .accessibility-issue__description{background-color:rgba(204,0,0,.1);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--error .tox-fLine truncated
+6 -6
View File
@@ -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": [
{
+1 -1
View File
@@ -1,3 +1,3 @@
(function(e){let t,n=new Set,r=new Map,i=[],a,o=Symbol(),s=Symbol(),c=Symbol(),l=Symbol(),u=Symbol(),d=Symbol(),f=Symbol(),p=Symbol(),m=Symbol(),h=Symbol();function g(e,n){let d={[o]:`_`+Math.random(),[p]:e,[m]:n};return r.set(d[o],d),d.run=()=>{let e;t&&(e=t,d[s]=t[o],t[c]=t[c]||[],t[c].push(d[o])),d[l]?.forEach(e=>{e.delete(d[o])}),t=d;let n=d[p](d[h],d);d[m]&&(t=null,d[m](n,d[h],d)),d[h]=n,t=e},d.unwatch=function(){d[u]=1,i.push(d[o]),a&&clearTimeout(a),a=setTimeout(()=>{for(let e of i)_(e);i=[],a=null},50)},d.run(),d}function _(e){let t=r.get(e);if(t?.[d]?.(),t?.[c]){for(let e of t[c])_(e);t[s]=null,t[c]=null}if(t?.[l]){for(let n of t[l])n.delete(e);t[l]=null}r.delete(e)}function v(e){return y(e,new Map)}function y(e,n){let r=typeof e==`object`&&!Array.isArray(e)?Object.getOwnPropertyDescriptors(e):{};return new Proxy(e,{get(e,i,a){if(typeof i==`symbol`)return e[i];if(i==`__raw`)return e;if(r[i]?.get&&!e[s]){let n=i;if(i=`@@`+i,!t){let t=r[n].get.call(e);return r[n]._watcher&&(a[i]=t),t}let s=t[o];if(r[n]._refs=r[n]._refs||new Set,!r[n]._refs.has(s)){r[n]._refs.add(s);let e=t[d];t[d]=()=>{e?.(),r[n]._refs.delete(s),!r[n]._refs.size&&r[n]._watcher&&_(r[n]._watcher[o])}}if(!r[n]._watcher){let o=t;t=null;let s=g(r[n].get.bind(e),t=>{e.hasOwnProperty(i)?a[i]=t:Object.defineProperty(e,i,{writable:!0,enumerable:!1,value:t})});r[n]._watcher=s,s[d]=()=>{r[n]._watcher=null},t=o}}let c=e?.[i];if(typeof c==`function`)return c;if(typeof c==`object`&&c&&!c[s]&&(c.constructor?.name==`Object`||c.constructor?.name==`Array`||c.constructor?.name==null)&&(c[s]=[a,i],c=y(c,n),e[i]=c),t){let r=b(e,i),a=t[o],s=[r];t[l]=t[l]||new Set;for(let e of s){let r=n.get(e);r||(r=new Set,n.set(e,r)),r.add(a),t[l].add(r)}}return c},set(e,t,r){if(typeof t==`symbol`)return e[t]=r,!0;let i=e[t];i?.[s]&&(i[f]=!0),r?.[s]&&Array.isArray(e)&&!isNaN(t)&&(r[s][1]=t);let a=!1;return i===void 0&&!e.hasOwnProperty(t)&&(a=!0),e[t]=r,a&&x(e,`toJSON`,n),(r!==i||t===`length`)&&x(e,t,n),!0},deleteProperty(e,t){if(typeof t!=`symbol`){x(e,t,n);let r=b(e,t);for(let e of n)(e[0]==r||e[0].startsWith(r+`/`))&&n.delete(e[0])}return delete e[t]}})}function b(e,t){let n=t,r=e?.[s];for(;r;)n=r[1]+`/`+n,r=r[0][s];return n}function x(e,t,i){let a=b(e,t),o=i.get(a);if(o)for(let e of o)n.delete(e),n.add(e),n.size==1&&queueMicrotask(()=>{let e={},t;for(let i of n)if(t=r.get(i),!(!t||t[u])&&!(t[s]&&n.has(t[s]))){if(e[i]=(e[i]||0)+1,e[i]>250){console.warn(`Possible infinite loop for watcher `+i+`:`,`
watch(`+t[p]?.toString()+(t[m]?`, `+t[m].toString():``)+`)`);continue}t.run()}n.clear()})}let S=new Proxy({},{get(e,t){return function(){return w(),O.call(void 0,t,...arguments)}}}),C=!1;function w(){if(C)return;C=!0;function e(t,n){for(let r of n)r[t]&&r[t](r),r.childNodes&&e(t,r.childNodes)}new MutationObserver(t=>{for(let n of t)e(`onmount`,n.addedNodes),e(`onunmount`,n.removedNodes)}).observe(document,{childList:!0,subtree:!0})}let T=Symbol(),E=Symbol(),D=Symbol();function O(e,t={},...n){let r=document.createElement(e);if(t)for(let e in t){let n=t[e],i=!1;e.length>5&&e.startsWith(`html-`)&&(i=!0,e=e.substring(5)),n===void 0?r.removeAttribute(e):typeof n!=`function`||e.length>2&&e.startsWith(`on`)?i?r.setAttribute(e,n):r[e]=n:(e==`rid`&&console.warn(`rid is provided as reactive function and will not have effect for `+r+`.
Consider using a plain number, string or object/array reference instead.`),r[T]=r[T]||[],r[T].push(()=>{if(!r)return;let t=n(r,e);t===void 0?(r[e]!==void 0&&(r[e]=void 0),r.removeAttribute(e)):i?r.setAttribute(e,t):r[e]=t}))}let i=r.onmount;r.onmount=()=>{if(!r[D]){if(r[D]=!0,r[T]){r[E]=r[E]||[];for(let e of r[T])r[E].push(g(e))}i?.(r)}};let a=r.onunmount;return r.onunmount=()=>{if(r[D]){if(r[D]=!1,r[E])for(let e of r[E])e.unwatch();r[E]=null,a?.(r)}},k(r,n),r}function k(e,t){t=N(t);for(let n of t)if(typeof n==`function`)A(e,n);else{let t=P(n);Array.isArray(t)?k(e,t):t&&e.appendChild(t)}}function A(e,t){let n=document.createComment(``);e.appendChild(n);let r=[],i=new Map,a=e.moveBefore||e.insertBefore;e[T]=e[T]||[],e[T].push(()=>{if(!e)return;let o=N(t(e)),s=o.length,c=new Map;if(!r?.length){let t=document.createDocumentFragment();for(let e=0;e<s;e++){o[e]=P(o[e]),t.appendChild(o[e]);let n=o[e].rid;n!==void 0&&(c.has(n)?console.warn(`Duplicated rid:`,n,o[e]):c.set(n,e))}e.insertBefore(t,n),t=null,r=o,i=c;return}let l=[],u=[],d=new Set,f=[];for(let e=0;e<s;e++){o[e]=P(o[e]);let t=o[e].rid;if(t!==void 0){c.has(t)?console.warn(`Duplicated rid:`,t,o[e]):c.set(t,e);let n=i.get(t);if(n>=0){d.add(r[n]),o[e]=r[n],f.push(n);continue}}u.push({child:o[e],prev:o[e-1]})}let p=j(f);f.length!=p.length&&f.forEach((e,t)=>{p.has(e)||l.push({child:r[e],targetPos:t})});let m,h,g;for(let t of l)m=r.findIndex(e=>e===t.child),h=t.targetPos,m<t.targetPos&&(h=t.targetPos+1),g=r[h]||n,M(r,m,t.targetPos),a.call(e,t.child,g);for(let e of u)e.prev?e.prev.after(e.child):(r[0]||n).before(e.child);for(let e=0;e<r.length;e++)d.has(r[e])||r[e].remove?.();r=o,i=c,o=null,c=null,d=null,l=null,u=null})}function j(e){let t=[],n=[];for(let r=0;r<e.length;r++){let i=e[r],a=0,o=0,s=t.length;for(;a<s;)o=Math.floor((a+s)/2),e[t[o]]>=i?s=o:a=o+1;a>0&&(n[r]=t[a-1]),t[a]=r}let r=new Set,i=t[t.length-1];for(;i!==void 0;)r.add(e[i]),i=n[i];return r}function M(e,t,n){if(t==n)return e;let r=t>n?-1:1,i=e[t];for(let i=t;i!=n;i+=r)e[i]=e[i+r];e[n]=i}function N(e){return e==null?[]:Array.isArray(e)?e:[e]}function P(e){return typeof e==`string`||typeof e==`number`||typeof e==`boolean`?document.createTextNode(e):e?.__raw===void 0?e:e.__raw}function F(e,t={fallbackPath:`#/`,transition:!0}){let n=L(e),r,i=()=>{let e=window.location.hash,i=I(n,e);if(!i){if(t.fallbackPath!=e){window.location.hash=t.fallbackPath;return}console.warn(`missing route:`,e);return}let a=async()=>{try{await r?.(),r=await i.handler(i)}catch(e){console.warn(`route navigation failed:`,e)}};t.transition&&document.startViewTransition?document.startViewTransition(a):a()};return window.addEventListener(`hashchange`,i),i(),()=>{window.removeEventListener(`hashchange`,i)}}function I(e,t){for(let n of e){let e=t.match(n.regex);if(!e)continue;let r={},i=t.split(`?`)?.[1];if(i){let e=new URLSearchParams(i);for(let[t,n]of e.entries())Array.isArray(r[t])||(r[t]=r[t]?[r[t]]:[]),r[t].push(n)}return Object.assign({path:t,query:r,params:e.groups||{}},n)}}function L(e){let t=[];for(let n in e){let r=n.split(`/`);for(let e in r)r[e].length>2&&r[e].startsWith(`{`)&&r[e].endsWith(`}`)?r[e]=`(?<`+r[e].substring(1,r[e].length-1)+`>[^\\/#?]+)`:r[e]=RegExp.escape(r[e]);t.push({regex:RegExp(`^`+r.join(`\\/`)+`(?:[?#].*)?$`),pattern:n,handler:e[n]})}return t}e.router=F,e.store=v,e.t=S,e.watch=g})(this.window=this.window||{});
Consider using a plain number, string or object/array reference instead.`),r[T]=r[T]||[],r[T].push(()=>{if(!r)return;let t=n(r,e);t===void 0?(r[e]!==void 0&&(r[e]=void 0),r.removeAttribute(e)):i?r.setAttribute(e,t):r[e]=t}))}let i=r.onmount;r.onmount=()=>{if(!r[D]){if(r[D]=!0,r[T]){r[E]=r[E]||[];for(let e of r[T])r[E].push(g(e))}i?.(r)}};let a=r.onunmount;return r.onunmount=()=>{if(r[D]){if(r[D]=!1,r[E])for(let e of r[E])e.unwatch();r[E]=null,a?.(r)}},k(r,n),r}function k(e,t){t=N(t);for(let n of t)if(typeof n==`function`)A(e,n);else{let t=P(n);Array.isArray(t)?k(e,t):t&&e.appendChild(t)}}function A(e,t){let n=document.createComment(``);e.appendChild(n);let r=[],i=new Map,a=e.moveBefore||e.insertBefore;e[T]=e[T]||[],e[T].push(()=>{if(!e)return;let o=N(t(e)),s=o.length,c=new Map;if(!r?.length){let t=document.createDocumentFragment();for(let e=0;e<s;e++){o[e]=P(o[e],!0),t.appendChild(o[e]);let n=o[e].rid;n!==void 0&&(c.has(n)?console.warn(`Duplicated rid:`,n,o[e]):c.set(n,e))}e.insertBefore(t,n),t=null,r=o,i=c;return}let l=[],u=[],d=new Set,f=[];for(let e=0;e<s;e++){o[e]=P(o[e],!0);let t=o[e].rid;if(t!==void 0){c.has(t)?console.warn(`Duplicated rid:`,t,o[e]):c.set(t,e);let n=i.get(t);if(n>=0){d.add(r[n]),o[e]=r[n],f.push(n);continue}}u.push({child:o[e],prev:o[e-1]})}let p=j(f);f.length!=p.length&&f.forEach((e,t)=>{p.has(e)||l.push({child:r[e],targetPos:t})});let m,h,g;for(let t of l)m=r.findIndex(e=>e===t.child),h=t.targetPos,m<t.targetPos&&(h=t.targetPos+1),g=r[h]||n,M(r,m,t.targetPos),a.call(e,t.child,g);for(let e of u)e.prev?e.prev.after(e.child):(r[0]||n).before(e.child);for(let e=0;e<r.length;e++)d.has(r[e])||r[e].remove?.();r=o,i=c,o=null,c=null,d=null,l=null,u=null})}function j(e){let t=[],n=[];for(let r=0;r<e.length;r++){let i=e[r],a=0,o=0,s=t.length;for(;a<s;)o=Math.floor((a+s)/2),e[t[o]]>=i?s=o:a=o+1;a>0&&(n[r]=t[a-1]),t[a]=r}let r=new Set,i=t[t.length-1];for(;i!==void 0;)r.add(e[i]),i=n[i];return r}function M(e,t,n){if(t==n)return e;let r=t>n?-1:1,i=e[t];for(let i=t;i!=n;i+=r)e[i]=e[i+r];e[n]=i}function N(e){return e==null?[]:Array.isArray(e)?e:[e]}function P(e,t=!1){return t&&e==null&&(e=``),typeof e==`string`||typeof e==`number`||typeof e==`boolean`?document.createTextNode(e):e?.__raw===void 0?e:e.__raw}function F(e,t={fallbackPath:`#/`,transition:!0}){let n=L(e),r,i=()=>{let e=window.location.hash,i=I(n,e);if(!i){if(t.fallbackPath!=e){window.location.hash=t.fallbackPath;return}console.warn(`missing route:`,e);return}let a=async()=>{try{await r?.(),r=await i.handler(i)}catch(e){console.warn(`route navigation failed:`,e)}};t.transition&&document.startViewTransition?document.startViewTransition(a):a()};return window.addEventListener(`hashchange`,i),i(),()=>{window.removeEventListener(`hashchange`,i)}}function I(e,t){for(let n of e){let e=t.match(n.regex);if(!e)continue;let r={},i=t.split(`?`)?.[1];if(i){let e=new URLSearchParams(i);for(let[t,n]of e.entries())Array.isArray(r[t])||(r[t]=r[t]?[r[t]]:[]),r[t].push(n)}return Object.assign({path:t,query:r,params:e.groups||{}},n)}}function L(e){let t=[];for(let n in e){let r=n.split(`/`);for(let e in r)r[e].length>2&&r[e].startsWith(`{`)&&r[e].endsWith(`}`)?r[e]=`(?<`+r[e].substring(1,r[e].length-1)+`>[^\\/#?]+)`:r[e]=RegExp.escape(r[e]);t.push({regex:RegExp(`^`+r.join(`\\/`)+`(?:[?#].*)?$`),pattern:n,handler:e[n]})}return t}e.router=F,e.store=v,e.t=S,e.watch=g})(this.window=this.window||{});
+1 -1
View File
@@ -1 +1 @@
.tox,.tox-tinymce,.tox-tinymce-aux{font-family:var(--baseFontFamily)}.tox,.tox .tox-dialog__body-content .accessibility-issue--info .tox-form__group h2{color:var(--surfaceTxtColor)}.tox .tox-button,.tox-tinymce{box-shadow:none;position:relative}.tox .tox-button,.tox .tox-button--secondary{background-position:0 0;background-repeat:repeat;font-weight:700;letter-spacing:normal;outline:0;text-decoration:none}.tox,.tox .tox-button,.tox .tox-button--secondary,.tox .tox-collection__group-heading{text-transform:none;font-style:normal}.tox .tox-dialog__table td:first-child,.tox .tox-dialog__table thead th:first-child{padding-right:8px}.tox{--mceIconScale:0.83;--headerToolbarColor:var(--surfaceColor);box-shadow:none;box-sizing:content-box;cursor:auto;font-size:var(--fontSize);font-weight:400;line-height:normal;-webkit-tap-highlight-color:transparent;text-decoration:none;text-shadow:none;vertical-align:initial;white-space:normal}.tox :not(svg):not(rect){box-sizing:inherit;color:inherit;cursor:inherit;direction:inherit;font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;line-height:inherit;-webkit-tap-highlight-color:inherit;text-align:inherit;text-decoration:inherit;text-shadow:inherit;text-transform:inherit;vertical-align:inherit;white-space:inherit;background:0 0;border:0;box-shadow:none;float:none;height:auto;margin:0;max-width:none;outline:0;padding:0;position:static;width:auto}.tox:not([dir=rtl]){direction:ltr;text-align:left}.tox[dir=rtl]{direction:rtl;text-align:right}.tox-tinymce{border:1px solid var(--surfaceAlt3Color);border-radius:var(--borderRadius);box-sizing:border-box;display:flex;flex-direction:column;overflow:hidden;visibility:inherit!important}.tox.tox-tinymce-inline{border:none;box-shadow:none;overflow:initial}.tox.tox-tinymce-inline .tox-editor-container{overflow:initial}.tox.tox-tinymce-inline .tox-editor-header{background-color:var(--surfaceColor);border:2px solid var(--surfaceAlt3Color);border-radius:var(--borderRadius);box-shadow:none;overflow:hidden}.tox .tox-button,.tox .tox-button[disabled]{border-color:var(--primaryColor);background-image:none}.tox-tinymce-aux{z-index:1300}.tox-tinymce :focus,.tox-tinymce-aux :focus{outline:0}button::-moz-focus-inner{border:0}.tox[dir=rtl] .tox-icon--flip svg{transform:rotateY(180deg)}.tox .accessibility-issue__header{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description{align-items:stretch;border-radius:var(--borderRadius);display:flex;justify-content:space-between}.tox .accessibility-issue__description>div{padding-bottom:4px}.tox .accessibility-issue__description>div>div{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description>div>div .tox-icon svg,.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__checked svg{display:block}.tox .accessibility-issue__repair{margin-top:16px}.tox .tox-dialog__body-content .accessibility-issue--info .accessibility-issue__description{background-color:rgba(0,101,216,.1);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--info .tox-icon svg{fill:var(--primaryColor)}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon{background-color:var(--primaryColor);color:var(--primaryTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:hover{background-color:#0060ce}.tox .tox-button,.tox .tox-button[disabled],.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:active{background-color:var(--primaryColor)}.tox .tox-dialog__body-content .accessibility-issue--warn .accessibility-issue__description{background-color:rgba(255,165,0,.08);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-form__group h2{color:#8f5d00}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-icon svg{fill:#8f5d00}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon{background-color:#ffe89d;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:hover{background-color:#f2d574;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:active{background-color:#e8c657;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--error .accessibility-issue__description{background-color:rgba(204,0,0,.1);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--error .tox-form__group h2{color:#c00}.tox .tox-control-wrap__status-icon-invalid svg,.tox .tox-dialog__body-content .accessibility-issue--error .tox-icon svg{fill:#c00}.tox Line truncated
.tox,.tox .tox-button,.tox .tox-button--secondary,.tox .tox-button-link,.tox .tox-button-link--sm{font-size:var(--fontSize)}.tox,.tox .tox-button,.tox .tox-button-link,.tox .tox-dialog__title,.tox-tinymce,.tox-tinymce-aux{font-family:var(--baseFontFamily)}.tox,.tox .tox-dialog__body-content .accessibility-issue--info .tox-form__group h2{color:var(--surfaceTxtColor)}.tox .tox-button,.tox-tinymce{box-shadow:none;position:relative}.tox .tox-button,.tox .tox-button--secondary{background-position:0 0;background-repeat:repeat;font-weight:700;letter-spacing:normal;outline:0;text-decoration:none}.tox,.tox .tox-button,.tox .tox-button--secondary,.tox .tox-collection__group-heading{text-transform:none;font-style:normal}.tox .tox-dialog__table td:first-child,.tox .tox-dialog__table thead th:first-child{padding-right:8px}.tox{--mceIconScale:0.83;--headerToolbarColor:var(--surfaceColor);box-shadow:none;box-sizing:content-box;cursor:auto;font-weight:400;line-height:normal;-webkit-tap-highlight-color:transparent;text-decoration:none;text-shadow:none;vertical-align:initial;white-space:normal}.tox :not(svg):not(rect){box-sizing:inherit;color:inherit;cursor:inherit;direction:inherit;font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;line-height:inherit;-webkit-tap-highlight-color:inherit;text-align:inherit;text-decoration:inherit;text-shadow:inherit;text-transform:inherit;vertical-align:inherit;white-space:inherit;background:0 0;border:0;box-shadow:none;float:none;height:auto;margin:0;max-width:none;outline:0;padding:0;position:static;width:auto}.tox:not([dir=rtl]){direction:ltr;text-align:left}.tox[dir=rtl]{direction:rtl;text-align:right}.tox-tinymce{border:1px solid var(--surfaceAlt3Color);border-radius:var(--borderRadius);box-sizing:border-box;display:flex;flex-direction:column;overflow:hidden;visibility:inherit!important}.tox.tox-tinymce-inline{border:none;box-shadow:none;overflow:initial}.tox.tox-tinymce-inline .tox-editor-container{overflow:initial}.tox.tox-tinymce-inline .tox-editor-header{background-color:var(--surfaceColor);border:2px solid var(--surfaceAlt3Color);border-radius:var(--borderRadius);box-shadow:none;overflow:hidden}.tox .tox-button,.tox .tox-button[disabled]{border-color:var(--primaryColor);background-image:none}.tox-tinymce-aux{z-index:1300}.tox-tinymce :focus,.tox-tinymce-aux :focus{outline:0}button::-moz-focus-inner{border:0}.tox[dir=rtl] .tox-icon--flip svg{transform:rotateY(180deg)}.tox .accessibility-issue__header{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description{align-items:stretch;border-radius:var(--borderRadius);display:flex;justify-content:space-between}.tox .accessibility-issue__description>div{padding-bottom:4px}.tox .accessibility-issue__description>div>div{align-items:center;display:flex;margin-bottom:4px}.tox .accessibility-issue__description>div>div .tox-icon svg,.tox input.tox-checkbox__input:checked+.tox-checkbox__icons .tox-checkbox-icon__checked svg{display:block}.tox .accessibility-issue__repair{margin-top:16px}.tox .tox-dialog__body-content .accessibility-issue--info .accessibility-issue__description{background-color:rgba(0,101,216,.1);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--info .tox-icon svg{fill:var(--primaryColor)}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon{background-color:var(--primaryColor);color:var(--primaryTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:hover{background-color:#0060ce}.tox .tox-button,.tox .tox-button[disabled],.tox .tox-dialog__body-content .accessibility-issue--info a.tox-button--naked.tox-button--icon:active{background-color:var(--primaryColor)}.tox .tox-dialog__body-content .accessibility-issue--warn .accessibility-issue__description{background-color:rgba(255,165,0,.08);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-form__group h2{color:#8f5d00}.tox .tox-dialog__body-content .accessibility-issue--warn .tox-icon svg{fill:#8f5d00}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon{background-color:#ffe89d;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:focus,.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:hover{background-color:#f2d574;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--warn a.tox-button--naked.tox-button--icon:active{background-color:#e8c657;color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--error .accessibility-issue__description{background-color:rgba(204,0,0,.1);color:var(--surfaceTxtColor)}.tox .tox-dialog__body-content .accessibility-issue--error .tox-fLine truncated
+4
View File
@@ -20,6 +20,10 @@ export function docsAuthWithOTP(collection) {
},
// description
t.p(null, "Authenticate with an one-time/short-lived password (OTP)."),
t.p(
null,
"On successful authentication the user will be also marked as verified (if the OTP source is email and the user is not verified already).",
),
t.p(
null,
"Note that when requesting an OTP we return an ",
+8 -3
View File
@@ -19,7 +19,7 @@ export function docsEmailChange(collection) {
t.p(null, `Sends ${collection.name} email change request.`),
t.p(
null,
"On successful email change all previously issued auth tokens for the specific record will be automatically invalidated.",
"On successful email change all previously issued auth tokens for the specific record will be invalidated (and the user will be marked as verified if not already).",
),
app.components.codeBlockTabs({
className: "sdk-examples m-t-sm",
@@ -46,7 +46,8 @@ export function docsEmailChange(collection) {
// (optional) in your custom confirmation page:
// ---
// note: after this call all previously issued auth tokens are invalidated
// note: all previous user auth tokens will be invalidated
// (and the user will be marked as verified if not already)
await pb.collection('${collection.name}').confirmEmailChange(
'EMAIL_CHANGE_TOKEN',
'YOUR_PASSWORD',
@@ -83,7 +84,8 @@ export function docsEmailChange(collection) {
// (optional) in your custom confirmation page:
// ---
// note: after this call all previously issued auth tokens are invalidated
// note: all previous user auth tokens will be invalidated
// (and the user will be marked as verified if not already)
await pb.collection('${collection.name}').confirmEmailChange(
'EMAIL_CHANGE_TOKEN',
'YOUR_PASSWORD',
@@ -111,6 +113,9 @@ export function docsEmailChange(collection) {
'${baseURL}/api/collections/${collection.name}/request-email-change'
# Confirm email change
#
# note: all previous user auth tokens will be invalidated
# (and the user will be marked as verified if not already)
curl -X POST \\
-H 'Content-Type:application/json' \\
-d '{ "token":"...", "password":"" }' \\
+8 -3
View File
@@ -19,7 +19,7 @@ export function docsPasswordReset(collection) {
t.p(null, `Sends ${collection.name} password reset email request.`),
t.p(
null,
"On successful password reset all previously issued auth tokens for the specific record will be automatically invalidated.",
"On successful password reset all previously issued auth tokens for the specific record will be invalidated (and the user will be marked as verified if not already).",
),
app.components.codeBlockTabs({
className: "sdk-examples m-t-sm",
@@ -41,7 +41,8 @@ export function docsPasswordReset(collection) {
// (optional) in your custom confirmation page:
// ---
// note: after this call all previously issued auth tokens are invalidated
// note: all previous user auth tokens will be invalidated
// (and the user will be marked as verified if not already)
await pb.collection('${collection.name}').confirmPasswordReset(
'RESET_TOKEN',
'NEW_PASSWORD',
@@ -74,7 +75,8 @@ export function docsPasswordReset(collection) {
// (optional) in your custom confirmation page:
// ---
// note: after this call all previously issued auth tokens are invalidated
// note: all previous user auth tokens will be invalidated
// (and the user will be marked as verified if not already)
await pb.collection('${collection.name}').confirmPasswordReset(
'RESET_TOKEN',
'NEW_PASSWORD',
@@ -102,6 +104,9 @@ export function docsPasswordReset(collection) {
'${baseURL}/api/collections/${collection.name}/request-password-reset'
# Confirm password reset
#
# note: all previous user auth tokens will be invalidated
# (and the user will be marked as verified if not already)
curl -X POST \\
-H 'Content-Type:application/json' \\
-d '{ "token":"...", "password":"", "passwordConfirm":"" }' \\
+2
View File
@@ -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
View File
@@ -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;
Loaded 100 of 151 files, more files were not shown because too many files have changed in this diff. Show more