merge newui branch
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
## v0.37.0 (WIP)
|
||||
|
||||
- @todo
|
||||
|
||||
|
||||
## v0.36.9
|
||||
|
||||
- Updated the Discord `AuthUser.Name` field to use `global_name` ([#7603](https://github.com/pocketbase/pocketbase/pull/7603); thanks @HansHans135).
|
||||
|
||||
+15
-12
@@ -1,17 +1,20 @@
|
||||
# Contributing to PocketBase
|
||||
|
||||
Thanks for taking the time to improve PocketBase!
|
||||
> [!IMPORTANT]
|
||||
> Due to recent LLM spam, PRs are temporary disabled and only existing collaborators can open a PR.
|
||||
> If you stumble on an problem that you want to fix, please consider instead opening an issue or discussion with link to your fork _(if not obvious - LLM contributions are not welcome)_.
|
||||
> This status may change in the future in case GitHub finally decide to do something about the constant spam, or when I find time to move the project somewhere else.
|
||||
|
||||
This document describes how to prepare a PR for a change in the main repository.
|
||||
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Making changes in the Go code](#making-changes-in-the-go-code)
|
||||
- [Making changes in the Admin UI](#making-changes-in-the-admin-ui)
|
||||
- [Making changes in the Superuser UI](#making-changes-in-the-admin-ui)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Go 1.25+ (for making changes in the Go code)
|
||||
- Node 18+ (for making changes in the Admin UI)
|
||||
- Node 24+ (for making changes in the Superuser UI)
|
||||
|
||||
If you haven't already, you can fork the main repository and clone your fork so that you can work locally:
|
||||
|
||||
@@ -34,7 +37,7 @@ So, let's assume that you already done some changes in the PocketBase Go code an
|
||||
1. Navigate to `examples/base`
|
||||
2. Run `go run main.go serve`
|
||||
|
||||
This will start a web server on `http://localhost:8090` with the embedded prebuilt Admin UI from `ui/dist`. And that's it!
|
||||
This will start a web server on `http://localhost:8090` with the embedded prebuilt Superuser UI from `ui/dist`. And that's it!
|
||||
|
||||
**Before making a PR to the main repository, it is a good idea to:**
|
||||
|
||||
@@ -57,11 +60,11 @@ This will start a web server on `http://localhost:8090` with the embedded prebui
|
||||
make lint
|
||||
```
|
||||
|
||||
## Making changes in the Admin UI
|
||||
## Making changes in the Superuser UI
|
||||
|
||||
PocketBase Admin UI is a single-page application (SPA) built with Svelte and Vite.
|
||||
PocketBase Superuser UI is a single-page application (SPA) built with Svelte and Vite.
|
||||
|
||||
To start the Admin UI:
|
||||
To start the Superuser UI:
|
||||
|
||||
1. Navigate to the `ui` project directory
|
||||
2. Run `npm install` to install the node dependencies
|
||||
@@ -70,13 +73,13 @@ To start the Admin UI:
|
||||
npm run dev
|
||||
```
|
||||
|
||||
You could open the browser and access the running Admin UI at `http://localhost:3000`.
|
||||
You could open the browser and access the running Superuser UI at `http://localhost:5173`.
|
||||
|
||||
Since the Admin UI is just a client-side application, you need to have the PocketBase backend server also running in the background (either manually running the `examples/base/main.go` or download a prebuilt executable).
|
||||
Since the Superuser UI is just a client-side application, you need to have the PocketBase backend server also running in the background (either manually running the `examples/base/main.go` or download a prebuilt executable).
|
||||
|
||||
> [!NOTE]
|
||||
> By default, the Admin UI is expecting the backend server to be started at `http://localhost:8090`, but you could change that by creating a new `ui/.env.development.local` file with `PB_BACKEND_URL = YOUR_ADDRESS` variable inside it.
|
||||
> By default, the Superuser UI is expecting the backend server to be started at `http://localhost:8090`, but you could change that by creating a new `ui/.env.development.local` file with `PB_BACKEND_URL = YOUR_ADDRESS` variable inside it.
|
||||
|
||||
Every change you make in the Admin UI should be automatically reflected in the browser at `http://localhost:3000` without reloading the page.
|
||||
Every change you make in the Superuser UI should be automatically reflected in the browser at `http://localhost:5173` without reloading the page.
|
||||
|
||||
Once you are done with your changes, you have to build the Admin UI with `npm run build`, so that it can be embedded in the go package. And that's it - you can make your PR to the main PocketBase repository.
|
||||
Once you are done with your changes, you have to build the Superuser UI with `npm run build`, so that it can be embedded in the go package. And that's it - you can make your PR to the main PocketBase repository.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<p align="center">
|
||||
<a href="https://pocketbase.io" target="_blank" rel="noopener">
|
||||
<img src="https://i.imgur.com/5qimnm5.png" alt="PocketBase - open source backend in 1 file" />
|
||||
<img src="https://i.imgur.com/3FTfaXH.png" alt="PocketBase - open source backend in 1 file" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -146,10 +146,13 @@ You could help continuing its development by:
|
||||
- [Contribute to the source code](CONTRIBUTING.md)
|
||||
- [Suggest new features and report issues](https://github.com/pocketbase/pocketbase/issues)
|
||||
|
||||
PRs for new OAuth2 providers, bug fixes, code optimizations and documentation improvements are more than welcome.
|
||||
|
||||
But please refrain creating PRs for _new features_ without previously discussing the implementation details.
|
||||
Please refrain creating PRs for _new features_ without previously discussing the implementation details.
|
||||
PocketBase has a [roadmap](https://github.com/orgs/pocketbase/projects/2) and I try to work on issues in specific order and such PRs often come in out of nowhere and skew all initial planning with tedious back-and-forth communication.
|
||||
|
||||
Don't get upset if I close your PR, even if it is well executed and tested. This doesn't mean that it will never be merged.
|
||||
Later we can always refer to it and/or take pieces of your implementation when the time comes to work on the issue (don't worry you'll be credited in the release notes).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Due to recent LLM spam, PRs are temporary disabled and only existing collaborators can open a PR.
|
||||
> If you stumble on an problem that you want to fix, please consider instead opening an issue or discussion with link to your fork _(if not obvious - LLM contributions are not welcome)_.
|
||||
> This status may change in the future in case GitHub finally decide to do something about the constant spam, or when I find time to move the project somewhere else.
|
||||
|
||||
+5
-1
@@ -15,7 +15,7 @@ import (
|
||||
// StaticWildcardParam is the name of Static handler wildcard parameter.
|
||||
const StaticWildcardParam = "path"
|
||||
|
||||
// NewRouter returns a new router instance loaded with the default app middlewares and api routes.
|
||||
// NewRouter returns a new router instance loaded with the default app middlewares and routes.
|
||||
func NewRouter(app core.App) (*router.Router[*core.RequestEvent], error) {
|
||||
pbRouter := router.NewRouter(func(w http.ResponseWriter, r *http.Request) (*core.RequestEvent, router.EventCleanupFunc) {
|
||||
event := new(core.RequestEvent)
|
||||
@@ -34,6 +34,7 @@ func NewRouter(app core.App) (*router.Router[*core.RequestEvent], error) {
|
||||
pbRouter.Bind(securityHeaders())
|
||||
pbRouter.Bind(BodyLimit(DefaultMaxBodySize))
|
||||
|
||||
// API routes
|
||||
apiGroup := pbRouter.Group("/api")
|
||||
bindSettingsApi(app, apiGroup)
|
||||
bindCollectionApi(app, apiGroup)
|
||||
@@ -47,6 +48,9 @@ func NewRouter(app core.App) (*router.Router[*core.RequestEvent], error) {
|
||||
bindRealtimeApi(app, apiGroup)
|
||||
bindHealthApi(app, apiGroup)
|
||||
|
||||
// UI routes
|
||||
bindUIExtensions(app)
|
||||
|
||||
return pbRouter, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@ package apis
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
validation "github.com/go-ozzo/ozzo-validation/v4"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/auth"
|
||||
"github.com/pocketbase/pocketbase/tools/router"
|
||||
"github.com/pocketbase/pocketbase/tools/search"
|
||||
"github.com/pocketbase/pocketbase/tools/security"
|
||||
@@ -23,6 +25,10 @@ func bindCollectionApi(app core.App, rg *router.RouterGroup[*core.RequestEvent])
|
||||
subGroup.DELETE("/{collection}/truncate", collectionTruncate)
|
||||
subGroup.PUT("/import", collectionsImport)
|
||||
subGroup.GET("/meta/scaffolds", collectionScaffolds)
|
||||
|
||||
// @todo experimental
|
||||
subGroup.GET("/meta/oauth2-providers", collectionListOAuth2Providers)
|
||||
subGroup.POST("/meta/dry-run-view", collectionDryRunView)
|
||||
}
|
||||
|
||||
func collectionsList(e *core.RequestEvent) error {
|
||||
@@ -207,3 +213,84 @@ func collectionScaffolds(e *core.RequestEvent) error {
|
||||
|
||||
return e.JSON(http.StatusOK, collections)
|
||||
}
|
||||
|
||||
type providerListItem struct {
|
||||
order int
|
||||
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Logo string `json:"logo"`
|
||||
}
|
||||
|
||||
func collectionListOAuth2Providers(e *core.RequestEvent) error {
|
||||
providers := make([]*providerListItem, 0, len(auth.Providers))
|
||||
|
||||
for name, factory := range auth.Providers {
|
||||
p := factory()
|
||||
|
||||
providers = append(providers, &providerListItem{
|
||||
order: p.Order(),
|
||||
Name: name,
|
||||
DisplayName: p.DisplayName(),
|
||||
Logo: p.Logo(),
|
||||
})
|
||||
}
|
||||
|
||||
slices.SortStableFunc(providers, func(a, b *providerListItem) int {
|
||||
// sort by order
|
||||
if a.order < b.order {
|
||||
return -1
|
||||
}
|
||||
if a.order > b.order {
|
||||
return 1
|
||||
}
|
||||
|
||||
// fallback sort by name
|
||||
if a.Name < b.Name {
|
||||
return -1
|
||||
}
|
||||
if a.Name > b.Name {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
return e.JSON(http.StatusOK, providers)
|
||||
}
|
||||
|
||||
func collectionDryRunView(e *core.RequestEvent) error {
|
||||
// extra precaution in case reused in custom route group
|
||||
if !e.HasSuperuserAuth() {
|
||||
return e.ForbiddenError("", nil)
|
||||
}
|
||||
|
||||
form := dryRunViewForm{}
|
||||
|
||||
err := e.BindBody(&form)
|
||||
if err != nil {
|
||||
return firstApiError(err, e.BadRequestError("An error occurred while loading the submitted data.", err))
|
||||
}
|
||||
|
||||
err = form.validate()
|
||||
if err != nil {
|
||||
return firstApiError(err, e.BadRequestError("An error occurred while validating the submitted data.", err))
|
||||
}
|
||||
|
||||
result, err := e.App.DryRunView(form.Query, 10)
|
||||
if err != nil {
|
||||
return firstApiError(err, e.BadRequestError("Invalid view query. Raw error: \n"+err.Error(), nil))
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
type dryRunViewForm struct {
|
||||
Query string `form:"query" json:"query"`
|
||||
}
|
||||
|
||||
func (form *dryRunViewForm) validate() error {
|
||||
return validation.ValidateStruct(form,
|
||||
validation.Field(&form.Query, validation.Required, validation.Length(0, 5000)),
|
||||
)
|
||||
}
|
||||
|
||||
+190
-6
@@ -536,7 +536,7 @@ func TestCollectionCreate(t *testing.T) {
|
||||
`"type":"base"`,
|
||||
`"system":false`,
|
||||
// ensures that id field was prepended
|
||||
`"fields":[{"autogeneratePattern":"[a-z0-9]{15}","hidden":false,"id":"text3208210256","max":15,"min":15,"name":"id","pattern":"^[a-z0-9]+$","presentable":false,"primaryKey":true,"required":true,"system":true,"type":"text"},{"autogeneratePattern":"","hidden":false,"id":"12345789","max":0,"min":0,"name":"test","pattern":"","presentable":false,"primaryKey":false,"required":false,"system":false,"type":"text"}]`,
|
||||
`"fields":[{"autogeneratePattern":"[a-z0-9]{15}","help":"","hidden":false,"id":"text3208210256","max":15,"min":15,"name":"id","pattern":"^[a-z0-9]+$","presentable":false,"primaryKey":true,"required":true,"system":true,"type":"text"},{"autogeneratePattern":"","help":"","hidden":false,"id":"12345789","max":0,"min":0,"name":"test","pattern":"","presentable":false,"primaryKey":false,"required":false,"system":false,"type":"text"}]`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"*": 0,
|
||||
@@ -585,7 +585,7 @@ func TestCollectionCreate(t *testing.T) {
|
||||
`"name":"verified"`,
|
||||
`"duration":123`,
|
||||
// should overwrite the user required option but keep the min value
|
||||
`{"autogeneratePattern":"","hidden":true,"id":"text2504183744","max":0,"min":10,"name":"tokenKey","pattern":"","presentable":false,"primaryKey":false,"required":true,"system":true,"type":"text"}`,
|
||||
`{"autogeneratePattern":"","help":"","hidden":true,"id":"text2504183744","max":0,"min":10,"name":"tokenKey","pattern":"","presentable":false,"primaryKey":false,"required":true,"system":true,"type":"text"}`,
|
||||
},
|
||||
NotExpectedContent: []string{
|
||||
`"secret":"`,
|
||||
@@ -751,7 +751,7 @@ func TestCollectionCreate(t *testing.T) {
|
||||
"name":"new",
|
||||
"type":"view",
|
||||
"fields":[{"type":"text","id":"12345789","name":"ignored!@#$"}],
|
||||
"viewQuery":"invalid"
|
||||
"viewQuery":"select '123' as abc"
|
||||
}`),
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
@@ -780,7 +780,7 @@ func TestCollectionCreate(t *testing.T) {
|
||||
"name":"new",
|
||||
"type":"view",
|
||||
"fields":[{"type":"text","id":"12345789","name":"ignored!@#$"}],
|
||||
"viewQuery": "select 1 as id from ` + core.CollectionNameSuperusers + `"
|
||||
"viewQuery": "select 1 as id from ` + core.CollectionNameSuperusers + ` limit 1"
|
||||
}`),
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
@@ -789,7 +789,7 @@ func TestCollectionCreate(t *testing.T) {
|
||||
ExpectedContent: []string{
|
||||
`"name":"new"`,
|
||||
`"type":"view"`,
|
||||
`"fields":[{"autogeneratePattern":"","hidden":false,"id":"text3208210256","max":0,"min":0,"name":"id","pattern":"^[a-z0-9]+$","presentable":false,"primaryKey":true,"required":true,"system":true,"type":"text"}]`,
|
||||
`"fields":[{"autogeneratePattern":"","help":"","hidden":false,"id":"text3208210256","max":0,"min":0,"name":"id","pattern":"^[a-z0-9]+$","presentable":false,"primaryKey":true,"required":true,"system":true,"type":"text"}]`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"*": 0,
|
||||
@@ -1262,7 +1262,7 @@ func TestCollectionUpdate(t *testing.T) {
|
||||
Body: strings.NewReader(`{
|
||||
"name":"view2_update",
|
||||
"fields":[{"type":"text","id":"12345789","name":"ignored!@#$"}],
|
||||
"viewQuery": "select 2 as id, created, updated, email from ` + core.CollectionNameSuperusers + `"
|
||||
"viewQuery": "select 2 as id, created, updated, email from ` + core.CollectionNameSuperusers + ` limit 1"
|
||||
}`),
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
@@ -1584,3 +1584,187 @@ func TestCollectionTruncate(t *testing.T) {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionOAuth2Providers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scenarios := []tests.ApiScenario{
|
||||
{
|
||||
Name: "unauthorized",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/collections/meta/oauth2-providers",
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
{
|
||||
Name: "authorized as regular user",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/collections/meta/oauth2-providers",
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo",
|
||||
},
|
||||
ExpectedStatus: 403,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
{
|
||||
Name: "authorized as superuser",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/collections/meta/oauth2-providers",
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`{"name":"oidc3","displayName":"OIDC","logo":"\u003csvg`,
|
||||
},
|
||||
NotExpectedContent: []string{
|
||||
`"order":`,
|
||||
`"pkce":`,
|
||||
`"scopes":`,
|
||||
`"authURL":`,
|
||||
`"tokenURL":`,
|
||||
`"userInfoURL":`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionTestView(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scenarios := []tests.ApiScenario{
|
||||
{
|
||||
Name: "unauthorized",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/meta/dry-run-view",
|
||||
Body: strings.NewReader(`{"query":"select 1 as id"}`),
|
||||
ExpectedStatus: 401,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
{
|
||||
Name: "authorized as regular user",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/meta/dry-run-view",
|
||||
Body: strings.NewReader(`{"query":"select 1 as id"}`),
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjRxMXhsY2xtZmxva3UzMyIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoiX3BiX3VzZXJzX2F1dGhfIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.ZT3F0Z3iM-xbGgSG3LEKiEzHrPHr8t8IuHLZGGNuxLo",
|
||||
},
|
||||
ExpectedStatus: 403,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
{
|
||||
Name: "authorized as superuser",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/meta/dry-run-view",
|
||||
Body: strings.NewReader(`{"query":"select 1 as id"}`),
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"fields":[{`,
|
||||
`"name":"id"`,
|
||||
`"type":"text"`,
|
||||
`"sample":[{`,
|
||||
`"id":"1"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "empty query",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/meta/dry-run-view",
|
||||
Body: strings.NewReader(`{"query":""}`),
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{
|
||||
`"data":{"query":`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "query length beyond validator limit",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/meta/dry-run-view",
|
||||
Body: strings.NewReader(`{"query":"` + strings.Repeat("a", 5001) + `"}`),
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{
|
||||
`"data":{"query":`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "query with length equal to the validator limit",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/meta/dry-run-view",
|
||||
Body: strings.NewReader(`{"query":"select 1 as id` + strings.Repeat(" ", 4986) + `"}`),
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"fields":[{`,
|
||||
`"name":"id"`,
|
||||
`"type":"text"`,
|
||||
`"sample":[`,
|
||||
`"id":"1"`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "missing ids sample",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/meta/dry-run-view",
|
||||
Body: strings.NewReader(`{"query":"(select 1 as id union select '' as id)"}`),
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{
|
||||
`"data":{}`,
|
||||
`Raw error:`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "duplicated ids sample",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/meta/dry-run-view",
|
||||
Body: strings.NewReader(`{"query":"(select 1 as id union all select 1 as id)"}`),
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{
|
||||
`"data":{}`,
|
||||
`Raw error:`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "write query",
|
||||
Method: http.MethodPost,
|
||||
URL: "/api/collections/meta/dry-run-view",
|
||||
Body: strings.NewReader(`{"query":"CREATE TABLE t1(x INT)"}`),
|
||||
Headers: map[string]string{
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6InN5d2JoZWNuaDQ2cmhtMCIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoicGJjXzMxNDI2MzU4MjMiLCJleHAiOjI1MjQ2MDQ0NjEsInJlZnJlc2hhYmxlIjp0cnVlfQ.UXgO3j-0BumcugrFjbd7j0M4MQvbrLggLlcu_YNGjoY",
|
||||
},
|
||||
ExpectedStatus: 400,
|
||||
ExpectedContent: []string{
|
||||
`"data":{}`,
|
||||
`Raw error:`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package apis
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/hook"
|
||||
"github.com/pocketbase/pocketbase/ui"
|
||||
)
|
||||
|
||||
// bindUIExtensions binds the superuser UI extensions routes to the ServeEvent.Router.
|
||||
//
|
||||
// This method does nothing if the superuser UI is not bundled (aka. build with "no_ui" tag),
|
||||
func bindUIExtensions(app core.App) {
|
||||
if ui.DistDirFS == nil {
|
||||
return
|
||||
}
|
||||
|
||||
app.OnServe().Bind(&hook.Handler[*core.ServeEvent]{
|
||||
Priority: 9999, // execute as latest as possible
|
||||
Func: func(se *core.ServeEvent) error {
|
||||
uiGroup := se.Router.Group("/_").
|
||||
BindFunc(func(e *core.RequestEvent) error {
|
||||
if !e.App.IsDev() && e.Response.Header().Get("Cache-Control") == "" {
|
||||
e.Response.Header().Set("Cache-Control", "max-age=1209600, stale-while-revalidate=86400")
|
||||
}
|
||||
|
||||
if e.Response.Header().Get("Content-Security-Policy") == "" {
|
||||
e.Response.Header().Set("Content-Security-Policy", defaultCSP)
|
||||
}
|
||||
|
||||
return e.Next()
|
||||
}).
|
||||
Bind(Gzip())
|
||||
|
||||
// register static extension routes
|
||||
for _, ext := range se.UIExtensions {
|
||||
if ext.Name == "" || ext.FS == nil {
|
||||
se.App.Logger().Debug("Invalid UI extension configuration", slog.Any("extension", ext))
|
||||
continue
|
||||
}
|
||||
|
||||
uiGroup.GET("/extensions/"+ext.Name+"/{path...}", Static(ext.FS, false))
|
||||
}
|
||||
|
||||
// combine all extensions main.js in one file
|
||||
//
|
||||
// note: don't cache in memory to allow previewing changes without restart
|
||||
uiGroup.GET("/extensions.js", func(re *core.RequestEvent) error {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
for _, ext := range se.UIExtensions {
|
||||
err := copyExtensionMainjs(buf, ext)
|
||||
if err != nil {
|
||||
return re.InternalServerError("An error occurred while generating the main.js extension file", err)
|
||||
}
|
||||
}
|
||||
|
||||
return re.Stream(200, "text/javascript", buf)
|
||||
}).Bind(SkipSuccessActivityLog())
|
||||
|
||||
return se.Next()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func copyExtensionMainjs(buf *bytes.Buffer, ext core.UIExtension) error {
|
||||
f, err := ext.FS.Open("main.js")
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil // nothing to copy
|
||||
}
|
||||
|
||||
return fmt.Errorf("[UI extension %q] main.js open error: %w", ext.Name, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// wrap in a self-executing function to avoid scope and concatenation issues
|
||||
_, _ = buf.WriteString("(function(){")
|
||||
|
||||
_, err = io.Copy(buf, f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[UI extension %q] main.js copy error: %w", ext.Name, err)
|
||||
}
|
||||
|
||||
_, _ = buf.WriteString("})();")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package apis_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tests"
|
||||
"github.com/pocketbase/pocketbase/ui"
|
||||
)
|
||||
|
||||
// note: don't run in parallel to avoid conflicts with the ui.DistDirFS nil test
|
||||
func TestUIExtensions_Mainjs(t *testing.T) {
|
||||
successAfterTestFunc := func(t testing.TB, app *tests.TestApp, res *http.Response) {
|
||||
expected := "text/javascript"
|
||||
if ct := res.Header.Get("content-type"); ct != expected {
|
||||
t.Fatalf("Expected response Content-Type %q, got %q", expected, ct)
|
||||
}
|
||||
}
|
||||
|
||||
oldDistDirFS := ui.DistDirFS
|
||||
|
||||
scenarios := []tests.ApiScenario{
|
||||
{
|
||||
Name: "disabled UI",
|
||||
Method: http.MethodGet,
|
||||
URL: "/_/extensions.js",
|
||||
TestAppFactory: func(t testing.TB) *tests.TestApp {
|
||||
app, err := tests.NewTestApp()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// simulate no_ui tag (needs to be cleared before the router is initialized)
|
||||
ui.DistDirFS = nil
|
||||
|
||||
return app
|
||||
},
|
||||
AfterTestFunc: func(t testing.TB, app *tests.TestApp, res *http.Response) {
|
||||
ui.DistDirFS = oldDistDirFS
|
||||
},
|
||||
ExpectedStatus: 404,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
{
|
||||
Name: "no extensions",
|
||||
Method: http.MethodGet,
|
||||
URL: "/_/extensions.js",
|
||||
AfterTestFunc: successAfterTestFunc,
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{},
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
{
|
||||
Name: "with extensions",
|
||||
Method: http.MethodGet,
|
||||
URL: "/_/extensions.js",
|
||||
TestAppFactory: func(t testing.TB) *tests.TestApp {
|
||||
app, err := tests.NewTestApp()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
app.OnServe().BindFunc(func(e *core.ServeEvent) error {
|
||||
e.UIExtensions = createTestExtensions()
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
return app
|
||||
},
|
||||
AfterTestFunc: successAfterTestFunc,
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"(function(){ext1_main})();(function(){ext3_main})();"},
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
|
||||
// note: don't run in parallel to avoid conflicts with the ui.DistDirFS nil test
|
||||
func TestUIExtensions_Files(t *testing.T) {
|
||||
testAppFactory := func(t testing.TB) *tests.TestApp {
|
||||
app, err := tests.NewTestApp()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
app.OnServe().BindFunc(func(e *core.ServeEvent) error {
|
||||
e.UIExtensions = createTestExtensions()
|
||||
return e.Next()
|
||||
})
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
scenarios := []tests.ApiScenario{
|
||||
{
|
||||
Name: "no extensions",
|
||||
Method: http.MethodGet,
|
||||
URL: "/_/extensions/ext1/test.txt",
|
||||
ExpectedStatus: 404,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
{
|
||||
Name: "with missing extension file",
|
||||
Method: http.MethodGet,
|
||||
URL: "/_/extensions/ext1/missing",
|
||||
TestAppFactory: testAppFactory,
|
||||
ExpectedStatus: 404,
|
||||
ExpectedContent: []string{`"data":{}`},
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
{
|
||||
Name: "with existing extension file (ext1)",
|
||||
Method: http.MethodGet,
|
||||
URL: "/_/extensions/ext1/test.txt",
|
||||
TestAppFactory: testAppFactory,
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"ext1_txt"},
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
{
|
||||
Name: "with existing extension file (extension name escape)",
|
||||
Method: http.MethodGet,
|
||||
URL: "/_/extensions/ext3%20with%20spaces/test.txt",
|
||||
TestAppFactory: testAppFactory,
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{"ext3_txt"},
|
||||
ExpectedEvents: map[string]int{"*": 0},
|
||||
},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
scenario.Test(t)
|
||||
}
|
||||
}
|
||||
|
||||
func createTestExtensions() []core.UIExtension {
|
||||
return []core.UIExtension{
|
||||
{
|
||||
Name: "ext1",
|
||||
FS: fstest.MapFS{
|
||||
"main.js": &fstest.MapFile{
|
||||
Data: []byte("ext1_main"),
|
||||
},
|
||||
"test.txt": &fstest.MapFile{
|
||||
Data: []byte("ext1_txt"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ext2",
|
||||
FS: fstest.MapFS{
|
||||
"test.txt": &fstest.MapFile{
|
||||
Data: []byte("ext2_txt"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ext3 with spaces",
|
||||
FS: fstest.MapFS{
|
||||
"main.js": &fstest.MapFile{
|
||||
Data: []byte("ext3_main"),
|
||||
},
|
||||
"test.txt": &fstest.MapFile{
|
||||
Data: []byte("ext3_txt"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ func healthCheck(e *core.RequestEvent) error {
|
||||
Message: "API is healthy.",
|
||||
}
|
||||
|
||||
// @todo evaluate whether it is worth removing the extra info from the health endpoint
|
||||
if e.HasSuperuserAuth() {
|
||||
resp.Data = make(map[string]any, 3)
|
||||
resp.Data["canBackup"] = !e.App.Store().Has(core.StoreKeyActiveBackup)
|
||||
|
||||
+7
-1
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/osutils"
|
||||
"github.com/pocketbase/pocketbase/ui"
|
||||
)
|
||||
|
||||
// DefaultInstallerFunc is the default PocketBase installer function.
|
||||
@@ -22,13 +23,18 @@ import (
|
||||
//
|
||||
// See https://github.com/pocketbase/pocketbase/discussions/5814.
|
||||
func DefaultInstallerFunc(app core.App, systemSuperuser *core.Record, baseURL string) error {
|
||||
if ui.DistDirFS == nil {
|
||||
color.Magenta("You can create your first superuser by running: %s superuser upsert EMAIL PASS", executablePath())
|
||||
return nil
|
||||
}
|
||||
|
||||
token, err := systemSuperuser.NewStaticAuthToken(30 * time.Minute)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// launch url (ignore errors and always print a help text as fallback)
|
||||
url := fmt.Sprintf("%s/_/#/pbinstal/%s", strings.TrimRight(baseURL, "/"), token)
|
||||
url := fmt.Sprintf("%s/_/#/pbinstall/%s", strings.TrimRight(baseURL, "/"), token)
|
||||
_ = osutils.LaunchURL(url)
|
||||
color.Magenta("\n(!) Launch the URL below in the browser if it hasn't been open already to create your first superuser account:")
|
||||
color.New(color.Bold).Add(color.FgCyan).Println(url)
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
var ErrRequestEntityTooLarge = router.NewApiError(http.StatusRequestEntityTooLarge, "Request entity too large", nil)
|
||||
|
||||
const DefaultMaxBodySize int64 = 32 << 20
|
||||
const DefaultMaxBodySize int64 = 32 << 20 // @todo consider replacing with router.DefaultMaxMemory
|
||||
|
||||
const (
|
||||
DefaultBodyLimitMiddlewareId = "pbBodyLimit"
|
||||
|
||||
@@ -34,6 +34,7 @@ type oauth2Response struct {
|
||||
type providerInfo struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Logo string `json:"logo"`
|
||||
State string `json:"state"`
|
||||
AuthURL string `json:"authURL"`
|
||||
|
||||
@@ -68,7 +69,14 @@ func (amr *authMethodsResponse) fillLegacyFields() {
|
||||
amr.UsernamePassword = amr.Password.Enabled && slices.Contains(amr.Password.IdentityFields, "username")
|
||||
|
||||
if amr.OAuth2.Enabled {
|
||||
amr.AuthProviders = amr.OAuth2.Providers
|
||||
// clone without the logo
|
||||
legacyProviders := make([]providerInfo, len(amr.OAuth2.Providers))
|
||||
for i, p := range amr.OAuth2.Providers {
|
||||
legacyProviders[i] = p
|
||||
legacyProviders[i].Logo = ""
|
||||
}
|
||||
|
||||
amr.AuthProviders = legacyProviders
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +136,7 @@ func recordAuthMethods(e *core.RequestEvent) error {
|
||||
info := providerInfo{
|
||||
Name: config.Name,
|
||||
DisplayName: provider.DisplayName(),
|
||||
Logo: provider.Logo(),
|
||||
State: security.RandomString(30),
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,8 @@ func TestRecordAuthMethodsList(t *testing.T) {
|
||||
`"providers":[{`,
|
||||
`"name":"google"`,
|
||||
`"name":"gitlab"`,
|
||||
`"logo":"\u003csvg`,
|
||||
`"logo":""`, // for the legacy fields
|
||||
`"state":`,
|
||||
`"displayName":`,
|
||||
`"codeVerifier":`,
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/subscriptions"
|
||||
"github.com/pocketbase/pocketbase/ui"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -28,17 +29,12 @@ type oauth2RedirectData struct {
|
||||
}
|
||||
|
||||
func oauth2SubscriptionRedirect(e *core.RequestEvent) error {
|
||||
redirectStatusCode := http.StatusTemporaryRedirect
|
||||
if e.Request.Method != http.MethodGet {
|
||||
redirectStatusCode = http.StatusSeeOther
|
||||
}
|
||||
|
||||
data := oauth2RedirectData{}
|
||||
|
||||
if e.Request.Method == http.MethodPost {
|
||||
if err := e.BindBody(&data); err != nil {
|
||||
e.App.Logger().Debug("Failed to read OAuth2 redirect data", "error", err)
|
||||
return e.Redirect(redirectStatusCode, oauth2RedirectFailurePath)
|
||||
return failureRedirect(e)
|
||||
}
|
||||
} else {
|
||||
query := e.Request.URL.Query()
|
||||
@@ -49,13 +45,13 @@ func oauth2SubscriptionRedirect(e *core.RequestEvent) error {
|
||||
|
||||
if data.State == "" {
|
||||
e.App.Logger().Debug("Missing OAuth2 state parameter")
|
||||
return e.Redirect(redirectStatusCode, oauth2RedirectFailurePath)
|
||||
return failureRedirect(e)
|
||||
}
|
||||
|
||||
client, err := e.App.SubscriptionsBroker().ClientById(data.State)
|
||||
if err != nil || client.IsDiscarded() || !client.HasSubscription(oauth2SubscriptionTopic) {
|
||||
e.App.Logger().Debug("Missing or invalid OAuth2 subscription client", "error", err, "clientId", data.State)
|
||||
return e.Redirect(redirectStatusCode, oauth2RedirectFailurePath)
|
||||
return failureRedirect(e)
|
||||
}
|
||||
defer client.Unsubscribe(oauth2SubscriptionTopic)
|
||||
|
||||
@@ -76,7 +72,7 @@ func oauth2SubscriptionRedirect(e *core.RequestEvent) error {
|
||||
encodedData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
e.App.Logger().Debug("Failed to marshalize OAuth2 redirect data", "error", err)
|
||||
return e.Redirect(redirectStatusCode, oauth2RedirectFailurePath)
|
||||
return failureRedirect(e)
|
||||
}
|
||||
|
||||
msg := subscriptions.Message{
|
||||
@@ -88,10 +84,36 @@ func oauth2SubscriptionRedirect(e *core.RequestEvent) error {
|
||||
|
||||
if data.Error != "" || data.Code == "" {
|
||||
e.App.Logger().Debug("Failed OAuth2 redirect due to an error or missing code parameter", "error", data.Error, "clientId", data.State)
|
||||
return e.Redirect(redirectStatusCode, oauth2RedirectFailurePath)
|
||||
return failureRedirect(e)
|
||||
}
|
||||
|
||||
return e.Redirect(redirectStatusCode, oauth2RedirectSuccessPath)
|
||||
return successRedirect(e)
|
||||
}
|
||||
|
||||
func redirectStatusCode(e *core.RequestEvent) int {
|
||||
if e.Request.Method != http.MethodGet {
|
||||
return http.StatusSeeOther
|
||||
}
|
||||
|
||||
return http.StatusTemporaryRedirect
|
||||
}
|
||||
|
||||
func failureRedirect(e *core.RequestEvent) error {
|
||||
// fallback if UI is not bundled
|
||||
if ui.DistDirFS == nil {
|
||||
return e.String(http.StatusOK, "Failed to authenticate. You can close this window and go back to the app to try again.")
|
||||
}
|
||||
|
||||
return e.Redirect(redirectStatusCode(e), oauth2RedirectFailurePath)
|
||||
}
|
||||
|
||||
func successRedirect(e *core.RequestEvent) error {
|
||||
// fallback if UI is not bundled
|
||||
if ui.DistDirFS == nil {
|
||||
return e.HTML(http.StatusOK, "Auth completed. You can close this window and go back to the app.")
|
||||
}
|
||||
|
||||
return e.Redirect(redirectStatusCode(e), oauth2RedirectSuccessPath)
|
||||
}
|
||||
|
||||
// parseAndStoreAppleRedirectName extracts the first and last name
|
||||
|
||||
+25
-15
@@ -22,6 +22,8 @@ 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'"
|
||||
|
||||
// ServeConfig defines a configuration struct for apis.Serve().
|
||||
type ServeConfig struct {
|
||||
// ShowStartBanner indicates whether to show or hide the server start console message.
|
||||
@@ -77,21 +79,25 @@ func Serve(app core.App, config ServeConfig) error {
|
||||
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
|
||||
}))
|
||||
|
||||
pbRouter.GET("/_/{path...}", Static(ui.DistDirFS, false)).
|
||||
BindFunc(func(e *core.RequestEvent) error {
|
||||
// ignore root path
|
||||
if e.Request.PathValue(StaticWildcardParam) != "" {
|
||||
e.Response.Header().Set("Cache-Control", "max-age=1209600, stale-while-revalidate=86400")
|
||||
}
|
||||
// @todo consider moving in base
|
||||
if ui.DistDirFS != nil {
|
||||
pbRouter.GET("/_/{path...}", Static(ui.DistDirFS, false)).
|
||||
BindFunc(func(e *core.RequestEvent) error {
|
||||
if !e.App.IsDev() &&
|
||||
// exclude root path
|
||||
e.Request.PathValue(StaticWildcardParam) != "" &&
|
||||
e.Response.Header().Get("Cache-Control") == "" {
|
||||
e.Response.Header().Set("Cache-Control", "max-age=1209600, stale-while-revalidate=86400")
|
||||
}
|
||||
|
||||
// add a default CSP
|
||||
if e.Response.Header().Get("Content-Security-Policy") == "" {
|
||||
e.Response.Header().Set("Content-Security-Policy", "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' 'sha256-GRUzBA7PzKYug7pqxv5rJaec5bwDCw1Vo6/IXwvD3Tc='")
|
||||
}
|
||||
if e.Response.Header().Get("Content-Security-Policy") == "" {
|
||||
e.Response.Header().Set("Content-Security-Policy", defaultCSP)
|
||||
}
|
||||
|
||||
return e.Next()
|
||||
}).
|
||||
Bind(Gzip())
|
||||
return e.Next()
|
||||
}).
|
||||
Bind(Gzip())
|
||||
}
|
||||
|
||||
// start http server
|
||||
// ---
|
||||
@@ -279,8 +285,12 @@ func Serve(app core.App, config ServeConfig) error {
|
||||
)
|
||||
|
||||
regular := color.New()
|
||||
regular.Printf("├─ REST API: %s\n", color.CyanString("%s/api/", baseURL))
|
||||
regular.Printf("└─ Dashboard: %s\n", color.CyanString("%s/_/", baseURL))
|
||||
if ui.DistDirFS == nil {
|
||||
regular.Printf("└─ REST API: %s\n", color.CyanString("%s/api/", baseURL))
|
||||
} else {
|
||||
regular.Printf("├─ REST API: %s\n", color.CyanString("%s/api/", baseURL))
|
||||
regular.Printf("└─ Dashboard: %s\n", color.CyanString("%s/_/", baseURL))
|
||||
}
|
||||
}
|
||||
|
||||
var serveErr error
|
||||
|
||||
+7
-5
@@ -16,6 +16,8 @@ func bindSettingsApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) {
|
||||
subGroup.PATCH("", settingsSet)
|
||||
subGroup.POST("/test/s3", settingsTestS3)
|
||||
subGroup.POST("/test/email", settingsTestEmail)
|
||||
|
||||
// @todo move to collections
|
||||
subGroup.POST("/apple/generate-client-secret", settingsGenerateAppleClientSecret)
|
||||
}
|
||||
|
||||
@@ -62,12 +64,12 @@ func settingsSet(e *core.RequestEvent) error {
|
||||
return e.BadRequestError("An error occurred while saving the new settings.", err)
|
||||
}
|
||||
|
||||
appSettings, err := e.App.Settings().Clone()
|
||||
if err != nil {
|
||||
return e.InternalServerError("Failed to clone app settings.", err)
|
||||
}
|
||||
|
||||
return execAfterSuccessTx(true, e.App, func() error {
|
||||
appSettings, err := e.App.Settings().Clone()
|
||||
if err != nil {
|
||||
return e.InternalServerError("Failed to clone app settings.", err)
|
||||
}
|
||||
|
||||
return e.JSON(http.StatusOK, appSettings)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -267,6 +267,15 @@ type App interface {
|
||||
// "dangerousSelectQuery" argument must come only from trusted input!
|
||||
CreateViewFields(dangerousSelectQuery string) (FieldsList, error)
|
||||
|
||||
// DryRunView executes the provided query by creating a temporary view
|
||||
// collection and returning a sample of the resulting query records (if valid).
|
||||
//
|
||||
// The same caveats from CreateViewFields apply here too.
|
||||
//
|
||||
// NB! Be aware that this method is vulnerable to SQL injection and the
|
||||
// "dangerousSelectQuery" argument must come only from trusted input!
|
||||
DryRunView(dangerousSelectQuery string, sampleSize int) (*DryRunViewResult, error)
|
||||
|
||||
// FindRecordByViewFile returns the original Record of the provided view collection file.
|
||||
FindRecordByViewFile(viewCollectionModelOrIdentifier any, fileFieldName string, filename string) (*Record, error)
|
||||
|
||||
|
||||
@@ -817,19 +817,19 @@ func TestCollectionDBExport(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
"unknown",
|
||||
`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"hidden":false,"id":"f1_id","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"f2_id","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":"{}","system":true,"type":"unknown","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
|
||||
`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"help":"","hidden":false,"id":"f1_id","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"help":"","hidden":false,"id":"f2_id","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":"{}","system":true,"type":"unknown","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
|
||||
},
|
||||
{
|
||||
core.CollectionTypeBase,
|
||||
`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"hidden":false,"id":"f1_id","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"f2_id","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":"{}","system":true,"type":"base","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
|
||||
`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"help":"","hidden":false,"id":"f1_id","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"help":"","hidden":false,"id":"f2_id","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":"{}","system":true,"type":"base","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
|
||||
},
|
||||
{
|
||||
core.CollectionTypeView,
|
||||
`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"hidden":false,"id":"f1_id","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"f2_id","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":{"viewQuery":"select 1"},"system":true,"type":"view","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
|
||||
`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"help":"","hidden":false,"id":"f1_id","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"help":"","hidden":false,"id":"f2_id","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":{"viewQuery":"select 1"},"system":true,"type":"view","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
|
||||
},
|
||||
{
|
||||
core.CollectionTypeAuth,
|
||||
`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"hidden":false,"id":"f1_id","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"hidden":false,"id":"f2_id","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":{"authRule":null,"manageRule":"1=6","authAlert":{"enabled":false,"emailTemplate":{"subject":"","body":""}},"oauth2":{"providers":null,"mappedFields":{"id":"","name":"","username":"","avatarURL":""},"enabled":false},"passwordAuth":{"enabled":false,"identityFields":null},"mfa":{"enabled":false,"duration":0,"rule":""},"otp":{"enabled":false,"duration":0,"length":0,"emailTemplate":{"subject":"","body":""}},"authToken":{"duration":0},"passwordResetToken":{"duration":0},"emailChangeToken":{"duration":0},"verificationToken":{"duration":0},"fileToken":{"duration":0},"verificationTemplate":{"subject":"","body":""},"resetPasswordTemplate":{"subject":"","body":""},"confirmEmailChangeTemplate":{"subject":"","body":""}},"system":true,"type":"auth","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
|
||||
`{"createRule":"1=3","created":"2024-07-01 01:02:03.456Z","deleteRule":"1=5","fields":[{"help":"","hidden":false,"id":"f1_id","name":"f1","presentable":false,"required":false,"system":true,"type":"bool"},{"help":"","hidden":false,"id":"f2_id","name":"f2","presentable":false,"required":true,"system":false,"type":"bool"}],"id":"test_id","indexes":["CREATE INDEX idx1 on test_name(id)","CREATE INDEX idx2 on test_name(id)"],"listRule":"1=1","name":"test_name","options":{"authRule":null,"manageRule":"1=6","authAlert":{"enabled":false,"emailTemplate":{"subject":"","body":""}},"oauth2":{"providers":null,"mappedFields":{"id":"","name":"","username":"","avatarURL":""},"enabled":false},"passwordAuth":{"enabled":false,"identityFields":null},"mfa":{"enabled":false,"duration":0,"rule":""},"otp":{"enabled":false,"duration":0,"length":0,"emailTemplate":{"subject":"","body":""}},"authToken":{"duration":0},"passwordResetToken":{"duration":0},"emailChangeToken":{"duration":0},"verificationToken":{"duration":0},"fileToken":{"duration":0},"verificationTemplate":{"subject":"","body":""},"resetPasswordTemplate":{"subject":"","body":""},"confirmEmailChangeTemplate":{"subject":"","body":""}},"system":true,"type":"auth","updateRule":"1=4","updated":"2024-07-01 01:02:03.456Z","viewRule":"1=7"}`,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1576,60 +1576,62 @@ func TestCollectionSaveViewWrapping(t *testing.T) {
|
||||
|
||||
viewName := "test_wrapping"
|
||||
|
||||
// note: some of the queries use "limit 0" because the tested field value could be empty
|
||||
// which will trigger the extra sample records validation that are not important for this test
|
||||
scenarios := []struct {
|
||||
name string
|
||||
query string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"no wrapping - text field",
|
||||
"select text as id, bool from demo1",
|
||||
"CREATE VIEW `test_wrapping` AS SELECT * FROM (select text as id, bool from demo1)",
|
||||
"no wrapping - id field",
|
||||
"select id, bool from demo1",
|
||||
"CREATE VIEW `test_wrapping` AS SELECT * FROM (select id, bool from demo1)",
|
||||
},
|
||||
{
|
||||
"no wrapping - id field",
|
||||
"select text as id, bool from demo1",
|
||||
"CREATE VIEW `test_wrapping` AS SELECT * FROM (select text as id, bool from demo1)",
|
||||
"no wrapping - text field",
|
||||
"select text as id, bool from demo1 limit 0",
|
||||
"CREATE VIEW `test_wrapping` AS SELECT * FROM (select text as id, bool from demo1 limit 0)",
|
||||
},
|
||||
{
|
||||
"no wrapping - relation field",
|
||||
"select rel_one as id, bool from demo1",
|
||||
"CREATE VIEW `test_wrapping` AS SELECT * FROM (select rel_one as id, bool from demo1)",
|
||||
"select rel_one as id, bool from demo1 limit 0",
|
||||
"CREATE VIEW `test_wrapping` AS SELECT * FROM (select rel_one as id, bool from demo1 limit 0)",
|
||||
},
|
||||
{
|
||||
"no wrapping - select field",
|
||||
"select select_many as id, bool from demo1",
|
||||
"CREATE VIEW `test_wrapping` AS SELECT * FROM (select select_many as id, bool from demo1)",
|
||||
"select select_many as id, bool from demo1 limit 0",
|
||||
"CREATE VIEW `test_wrapping` AS SELECT * FROM (select select_many as id, bool from demo1 limit 0)",
|
||||
},
|
||||
{
|
||||
"no wrapping - email field",
|
||||
"select email as id, bool from demo1",
|
||||
"CREATE VIEW `test_wrapping` AS SELECT * FROM (select email as id, bool from demo1)",
|
||||
"select email as id, bool from demo1 limit 0",
|
||||
"CREATE VIEW `test_wrapping` AS SELECT * FROM (select email as id, bool from demo1 limit 0)",
|
||||
},
|
||||
{
|
||||
"no wrapping - datetime field",
|
||||
"select datetime as id, bool from demo1",
|
||||
"CREATE VIEW `test_wrapping` AS SELECT * FROM (select datetime as id, bool from demo1)",
|
||||
"select datetime as id, bool from demo1 limit 0",
|
||||
"CREATE VIEW `test_wrapping` AS SELECT * FROM (select datetime as id, bool from demo1 limit 0)",
|
||||
},
|
||||
{
|
||||
"no wrapping - url field",
|
||||
"select url as id, bool from demo1",
|
||||
"CREATE VIEW `test_wrapping` AS SELECT * FROM (select url as id, bool from demo1)",
|
||||
"select url as id, bool from demo1 limit 0",
|
||||
"CREATE VIEW `test_wrapping` AS SELECT * FROM (select url as id, bool from demo1 limit 0)",
|
||||
},
|
||||
{
|
||||
"wrapping - bool field",
|
||||
"select bool as id, text as txt, url from demo1",
|
||||
"CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT CAST(`id` as TEXT) `id`,`txt`,`url` FROM (select bool as id, text as txt, url from demo1))",
|
||||
"select bool as id, text as txt, url from demo1 limit 0",
|
||||
"CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT CAST(`id` as TEXT) `id`,`txt`,`url` FROM (select bool as id, text as txt, url from demo1 limit 0))",
|
||||
},
|
||||
{
|
||||
"wrapping - bool field (different order)",
|
||||
"select text as txt, url, bool as id from demo1",
|
||||
"CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT `txt`,`url`,CAST(`id` as TEXT) `id` FROM (select text as txt, url, bool as id from demo1))",
|
||||
"select text as txt, url, bool as id from demo1 limit 0",
|
||||
"CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT `txt`,`url`,CAST(`id` as TEXT) `id` FROM (select text as txt, url, bool as id from demo1 limit 0))",
|
||||
},
|
||||
{
|
||||
"wrapping - json field",
|
||||
"select json as id, text, url from demo1",
|
||||
"CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT CAST(`id` as TEXT) `id`,`text`,`url` FROM (select json as id, text, url from demo1))",
|
||||
"select json as id, text, url from demo1 limit 0",
|
||||
"CREATE VIEW `test_wrapping` AS SELECT * FROM (SELECT CAST(`id` as TEXT) `id`,`text`,`url` FROM (select json as id, text, url from demo1 limit 0))",
|
||||
},
|
||||
{
|
||||
"wrapping - numeric id",
|
||||
|
||||
@@ -41,6 +41,24 @@ func TestCollectionViewOptionsValidate(t *testing.T) {
|
||||
},
|
||||
expectedErrors: []string{"fields", "viewQuery"},
|
||||
},
|
||||
{
|
||||
name: "view with valid query but empty sample id",
|
||||
collection: func(app core.App) (*core.Collection, error) {
|
||||
c := core.NewViewCollection("new_auth")
|
||||
c.ViewQuery = "select '' as id"
|
||||
return c, nil
|
||||
},
|
||||
expectedErrors: []string{"viewQuery"},
|
||||
},
|
||||
{
|
||||
name: "view with valid query but duplicated sample id",
|
||||
collection: func(app core.App) (*core.Collection, error) {
|
||||
c := core.NewViewCollection("new_auth")
|
||||
c.ViewQuery = "(select 'a' as id union all select 'a' as id union all select 'c' as id)"
|
||||
return c, nil
|
||||
},
|
||||
expectedErrors: []string{"viewQuery"},
|
||||
},
|
||||
{
|
||||
name: "view with valid query",
|
||||
collection: func(app core.App) (*core.Collection, error) {
|
||||
|
||||
@@ -314,11 +314,15 @@ func (cv *collectionValidator) checkViewQuery(value any) error {
|
||||
return nil // nothing to check
|
||||
}
|
||||
|
||||
if _, err := cv.app.CreateViewFields(v); err != nil {
|
||||
return validation.NewError(
|
||||
"validation_invalid_view_query",
|
||||
fmt.Sprintf("Invalid query - %s", err.Error()),
|
||||
)
|
||||
_, err := cv.app.DryRunView(v, 10)
|
||||
if err != nil {
|
||||
rawErr := err.Error()
|
||||
if len(rawErr) > 500 {
|
||||
// restrict just as an extra precaution
|
||||
rawErr = rawErr[:500]
|
||||
}
|
||||
|
||||
return validation.NewError("validation_invalid_view_query", "Invalid query - "+rawErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -2,6 +2,7 @@ package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
@@ -57,6 +58,9 @@ type baseCollectionEventData struct {
|
||||
Collection *Collection
|
||||
}
|
||||
|
||||
// @todo consider storing the original collection name and use that as a tag
|
||||
// to avoid the ambiguity when the collection is being modified (#7613);
|
||||
// for new collection also maybe return empty tags?
|
||||
func (e *baseCollectionEventData) Tags() []string {
|
||||
if e.Collection == nil {
|
||||
return nil
|
||||
@@ -125,6 +129,21 @@ type ServeEvent struct {
|
||||
//
|
||||
// Set it to nil if you want to skip the installer.
|
||||
InstallerFunc func(app App, systemSuperuser *Record, baseURL string) error
|
||||
|
||||
// @todo experimental
|
||||
//
|
||||
// UIExtensions is a list with the superuser UI extensions.
|
||||
UIExtensions []UIExtension
|
||||
}
|
||||
|
||||
type UIExtension struct {
|
||||
// Name is the name of the extension.
|
||||
// It is also used as path segment for the registered public extension endpoint
|
||||
// (e.g. /_/extensions/{name}/*)
|
||||
Name string
|
||||
|
||||
// FS is the extension file system.
|
||||
FS fs.FS
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -184,6 +184,26 @@ type RecordInterceptor interface {
|
||||
) error
|
||||
}
|
||||
|
||||
// DefaultFieldHelpValidationRule performs base validation on a field's "help" value.
|
||||
func DefaultFieldHelpValidationRule(value any) error {
|
||||
v, ok := value.(string)
|
||||
if !ok {
|
||||
return validators.ErrUnsupportedValueType
|
||||
}
|
||||
|
||||
rules := []validation.Rule{
|
||||
validation.Length(1, 300),
|
||||
}
|
||||
|
||||
for _, r := range rules {
|
||||
if err := r.Validate(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultFieldIdValidationRule performs base validation on a field id value.
|
||||
func DefaultFieldIdValidationRule(value any) error {
|
||||
v, ok := value.(string)
|
||||
|
||||
@@ -46,12 +46,12 @@ type AutodateField struct {
|
||||
// Hidden hides the field from the API response.
|
||||
Hidden bool `form:"hidden" json:"hidden"`
|
||||
|
||||
// ---
|
||||
|
||||
// Presentable hints the Dashboard UI to use the underlying
|
||||
// field record value in the relation preview label.
|
||||
Presentable bool `form:"presentable" json:"presentable"`
|
||||
|
||||
// ---
|
||||
|
||||
// OnCreate auto sets the current datetime as field value on record create.
|
||||
OnCreate bool `form:"onCreate" json:"onCreate"`
|
||||
|
||||
|
||||
+6
-1
@@ -36,11 +36,15 @@ type BoolField struct {
|
||||
// Hidden hides the field from the API response.
|
||||
Hidden bool `form:"hidden" json:"hidden"`
|
||||
|
||||
// ---
|
||||
|
||||
// Presentable hints the Dashboard UI to use the underlying
|
||||
// field record value in the relation preview label.
|
||||
Presentable bool `form:"presentable" json:"presentable"`
|
||||
|
||||
// ---
|
||||
// Help is an extra text explaining what the field is about.
|
||||
// It is usually shown in Dashboard UI under the field input.
|
||||
Help string `form:"help" json:"help"`
|
||||
|
||||
// Required will require the field value to be always "true".
|
||||
Required bool `form:"required" json:"required"`
|
||||
@@ -120,5 +124,6 @@ func (f *BoolField) ValidateSettings(ctx context.Context, app App, collection *C
|
||||
return validation.ValidateStruct(f,
|
||||
validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)),
|
||||
validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)),
|
||||
validation.Field(&f.Help, validation.By(DefaultFieldHelpValidationRule)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -147,4 +147,5 @@ func TestBoolFieldValidateValue(t *testing.T) {
|
||||
func TestBoolFieldValidateSettings(t *testing.T) {
|
||||
testDefaultFieldIdValidation(t, core.FieldTypeBool)
|
||||
testDefaultFieldNameValidation(t, core.FieldTypeBool)
|
||||
testDefaultFieldHelpValidation[core.BoolField](t)
|
||||
}
|
||||
|
||||
+6
-1
@@ -36,11 +36,15 @@ type DateField struct {
|
||||
// Hidden hides the field from the API response.
|
||||
Hidden bool `form:"hidden" json:"hidden"`
|
||||
|
||||
// ---
|
||||
|
||||
// Presentable hints the Dashboard UI to use the underlying
|
||||
// field record value in the relation preview label.
|
||||
Presentable bool `form:"presentable" json:"presentable"`
|
||||
|
||||
// ---
|
||||
// Help is an extra text explaining what the field is about.
|
||||
// It is usually shown in Dashboard UI under the field input.
|
||||
Help string `form:"help" json:"help"`
|
||||
|
||||
// Min specifies the min allowed field value.
|
||||
//
|
||||
@@ -148,6 +152,7 @@ func (f *DateField) ValidateSettings(ctx context.Context, app App, collection *C
|
||||
return validation.ValidateStruct(f,
|
||||
validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)),
|
||||
validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)),
|
||||
validation.Field(&f.Help, validation.By(DefaultFieldHelpValidationRule)),
|
||||
validation.Field(&f.Max, validation.By(f.checkRange(f.Min, f.Max))),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -133,6 +133,7 @@ func TestDateFieldValidateValue(t *testing.T) {
|
||||
func TestDateFieldValidateSettings(t *testing.T) {
|
||||
testDefaultFieldIdValidation(t, core.FieldTypeDate)
|
||||
testDefaultFieldNameValidation(t, core.FieldTypeDate)
|
||||
testDefaultFieldHelpValidation[core.DateField](t)
|
||||
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
@@ -41,11 +41,15 @@ type EditorField struct {
|
||||
// Hidden hides the field from the API response.
|
||||
Hidden bool `form:"hidden" json:"hidden"`
|
||||
|
||||
// ---
|
||||
|
||||
// Presentable hints the Dashboard UI to use the underlying
|
||||
// field record value in the relation preview label.
|
||||
Presentable bool `form:"presentable" json:"presentable"`
|
||||
|
||||
// ---
|
||||
// Help is an extra text explaining what the field is about.
|
||||
// It is usually shown in Dashboard UI under the field input.
|
||||
Help string `form:"help" json:"help"`
|
||||
|
||||
// MaxSize specifies the maximum size of the allowed field value (in bytes and up to 2^53-1).
|
||||
//
|
||||
@@ -148,6 +152,7 @@ func (f *EditorField) ValidateSettings(ctx context.Context, app App, collection
|
||||
return validation.ValidateStruct(f,
|
||||
validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)),
|
||||
validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)),
|
||||
validation.Field(&f.Help, validation.By(DefaultFieldHelpValidationRule)),
|
||||
validation.Field(&f.MaxSize, validation.Min(0), validation.Max(maxSafeJSONInt)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -163,6 +163,7 @@ func TestEditorFieldValidateValue(t *testing.T) {
|
||||
func TestEditorFieldValidateSettings(t *testing.T) {
|
||||
testDefaultFieldIdValidation(t, core.FieldTypeEditor)
|
||||
testDefaultFieldNameValidation(t, core.FieldTypeEditor)
|
||||
testDefaultFieldHelpValidation[core.EditorField](t)
|
||||
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
+6
-1
@@ -39,11 +39,15 @@ type EmailField struct {
|
||||
// Hidden hides the field from the API response.
|
||||
Hidden bool `form:"hidden" json:"hidden"`
|
||||
|
||||
// ---
|
||||
|
||||
// Presentable hints the Dashboard UI to use the underlying
|
||||
// field record value in the relation preview label.
|
||||
Presentable bool `form:"presentable" json:"presentable"`
|
||||
|
||||
// ---
|
||||
// Help is an extra text explaining what the field is about.
|
||||
// It is usually shown in Dashboard UI under the field input.
|
||||
Help string `form:"help" json:"help"`
|
||||
|
||||
// ExceptDomains will require the email domain to NOT be included in the listed ones.
|
||||
//
|
||||
@@ -155,6 +159,7 @@ func (f *EmailField) ValidateSettings(ctx context.Context, app App, collection *
|
||||
return validation.ValidateStruct(f,
|
||||
validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)),
|
||||
validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)),
|
||||
validation.Field(&f.Help, validation.By(DefaultFieldHelpValidationRule)),
|
||||
validation.Field(
|
||||
&f.ExceptDomains,
|
||||
validation.When(len(f.OnlyDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)),
|
||||
|
||||
@@ -182,6 +182,7 @@ func TestEmailFieldValidateValue(t *testing.T) {
|
||||
func TestEmailFieldValidateSettings(t *testing.T) {
|
||||
testDefaultFieldIdValidation(t, core.FieldTypeEmail)
|
||||
testDefaultFieldNameValidation(t, core.FieldTypeEmail)
|
||||
testDefaultFieldHelpValidation[core.EmailField](t)
|
||||
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
+6
-1
@@ -88,11 +88,15 @@ type FileField struct {
|
||||
// Hidden hides the field from the API response.
|
||||
Hidden bool `form:"hidden" json:"hidden"`
|
||||
|
||||
// ---
|
||||
|
||||
// Presentable hints the Dashboard UI to use the underlying
|
||||
// field record value in the relation preview label.
|
||||
Presentable bool `form:"presentable" json:"presentable"`
|
||||
|
||||
// ---
|
||||
// Help is an extra text explaining what the field is about.
|
||||
// It is usually shown in Dashboard UI under the field input.
|
||||
Help string `form:"help" json:"help"`
|
||||
|
||||
// MaxSize specifies the maximum size of a single uploaded file (in bytes and up to 2^53-1).
|
||||
//
|
||||
@@ -223,6 +227,7 @@ func (f *FileField) ValidateSettings(ctx context.Context, app App, collection *C
|
||||
return validation.ValidateStruct(f,
|
||||
validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)),
|
||||
validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)),
|
||||
validation.Field(&f.Help, validation.By(DefaultFieldHelpValidationRule)),
|
||||
validation.Field(&f.MaxSelect, validation.Min(0), validation.Max(maxSafeJSONInt)),
|
||||
validation.Field(&f.MaxSize, validation.Min(0), validation.Max(maxSafeJSONInt)),
|
||||
validation.Field(&f.Thumbs, validation.Each(
|
||||
|
||||
@@ -443,6 +443,7 @@ func TestFileFieldValidateValue(t *testing.T) {
|
||||
func TestFileFieldValidateSettings(t *testing.T) {
|
||||
testDefaultFieldIdValidation(t, core.FieldTypeFile)
|
||||
testDefaultFieldNameValidation(t, core.FieldTypeFile)
|
||||
testDefaultFieldHelpValidation[core.FileField](t)
|
||||
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
@@ -46,11 +46,15 @@ type GeoPointField struct {
|
||||
// Hidden hides the field from the API response.
|
||||
Hidden bool `form:"hidden" json:"hidden"`
|
||||
|
||||
// ---
|
||||
|
||||
// Presentable hints the Dashboard UI to use the underlying
|
||||
// field record value in the relation preview label.
|
||||
Presentable bool `form:"presentable" json:"presentable"`
|
||||
|
||||
// ---
|
||||
// Help is an extra text explaining what the field is about.
|
||||
// It is usually shown in Dashboard UI under the field input.
|
||||
Help string `form:"help" json:"help"`
|
||||
|
||||
// Required will require the field coordinates to be non-zero (aka. not "Null Island").
|
||||
Required bool `form:"required" json:"required"`
|
||||
@@ -144,5 +148,6 @@ func (f *GeoPointField) ValidateSettings(ctx context.Context, app App, collectio
|
||||
return validation.ValidateStruct(f,
|
||||
validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)),
|
||||
validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)),
|
||||
validation.Field(&f.Help, validation.By(DefaultFieldHelpValidationRule)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -199,4 +199,5 @@ func TestGeoPointFieldValidateValue(t *testing.T) {
|
||||
func TestGeoPointFieldValidateSettings(t *testing.T) {
|
||||
testDefaultFieldIdValidation(t, core.FieldTypeGeoPoint)
|
||||
testDefaultFieldNameValidation(t, core.FieldTypeGeoPoint)
|
||||
testDefaultFieldHelpValidation[core.GeoPointField](t)
|
||||
}
|
||||
|
||||
+6
-1
@@ -45,11 +45,15 @@ type JSONField struct {
|
||||
// Hidden hides the field from the API response.
|
||||
Hidden bool `form:"hidden" json:"hidden"`
|
||||
|
||||
// ---
|
||||
|
||||
// Presentable hints the Dashboard UI to use the underlying
|
||||
// field record value in the relation preview label.
|
||||
Presentable bool `form:"presentable" json:"presentable"`
|
||||
|
||||
// ---
|
||||
// Help is an extra text explaining what the field is about.
|
||||
// It is usually shown in Dashboard UI under the field input.
|
||||
Help string `form:"help" json:"help"`
|
||||
|
||||
// MaxSize specifies the maximum size of the allowed field value (in bytes and up to 2^53-1).
|
||||
//
|
||||
@@ -181,6 +185,7 @@ func (f *JSONField) ValidateSettings(ctx context.Context, app App, collection *C
|
||||
return validation.ValidateStruct(f,
|
||||
validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)),
|
||||
validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)),
|
||||
validation.Field(&f.Help, validation.By(DefaultFieldHelpValidationRule)),
|
||||
validation.Field(&f.MaxSize, validation.Min(0), validation.Max(maxSafeJSONInt)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -188,6 +188,7 @@ func TestJSONFieldValidateValue(t *testing.T) {
|
||||
func TestJSONFieldValidateSettings(t *testing.T) {
|
||||
testDefaultFieldIdValidation(t, core.FieldTypeJSON)
|
||||
testDefaultFieldNameValidation(t, core.FieldTypeJSON)
|
||||
testDefaultFieldHelpValidation[core.JSONField](t)
|
||||
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
@@ -48,11 +48,15 @@ type NumberField struct {
|
||||
// Hidden hides the field from the API response.
|
||||
Hidden bool `form:"hidden" json:"hidden"`
|
||||
|
||||
// ---
|
||||
|
||||
// Presentable hints the Dashboard UI to use the underlying
|
||||
// field record value in the relation preview label.
|
||||
Presentable bool `form:"presentable" json:"presentable"`
|
||||
|
||||
// ---
|
||||
// Help is an extra text explaining what the field is about.
|
||||
// It is usually shown in Dashboard UI under the field input.
|
||||
Help string `form:"help" json:"help"`
|
||||
|
||||
// Min specifies the min allowed field value.
|
||||
//
|
||||
@@ -173,6 +177,7 @@ func (f *NumberField) ValidateSettings(ctx context.Context, app App, collection
|
||||
return validation.ValidateStruct(f,
|
||||
validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)),
|
||||
validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)),
|
||||
validation.Field(&f.Help, validation.By(DefaultFieldHelpValidationRule)),
|
||||
validation.Field(&f.Min, validation.By(f.checkOnlyInt)),
|
||||
validation.Field(&f.Max, maxRules...),
|
||||
)
|
||||
|
||||
@@ -214,6 +214,7 @@ func TestNumberFieldValidateValue(t *testing.T) {
|
||||
func TestNumberFieldValidateSettings(t *testing.T) {
|
||||
testDefaultFieldIdValidation(t, core.FieldTypeNumber)
|
||||
testDefaultFieldNameValidation(t, core.FieldTypeNumber)
|
||||
testDefaultFieldHelpValidation[core.NumberField](t)
|
||||
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
@@ -61,11 +61,17 @@ type PasswordField struct {
|
||||
// Hidden hides the field from the API response.
|
||||
Hidden bool `form:"hidden" json:"hidden"`
|
||||
|
||||
// ---
|
||||
|
||||
// @todo remove
|
||||
//
|
||||
// Presentable hints the Dashboard UI to use the underlying
|
||||
// field record value in the relation preview label.
|
||||
Presentable bool `form:"presentable" json:"presentable"`
|
||||
|
||||
// ---
|
||||
// Help is an extra text explaining what the field is about.
|
||||
// It is usually shown in Dashboard UI under the field input.
|
||||
Help string `form:"help" json:"help"`
|
||||
|
||||
// Pattern specifies an optional regex pattern to match against the field value.
|
||||
//
|
||||
@@ -209,6 +215,7 @@ func (f *PasswordField) ValidateSettings(ctx context.Context, app App, collectio
|
||||
return validation.ValidateStruct(f,
|
||||
validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)),
|
||||
validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)),
|
||||
validation.Field(&f.Help, validation.By(DefaultFieldHelpValidationRule)),
|
||||
validation.Field(&f.Min, validation.Min(1), validation.Max(71)),
|
||||
validation.Field(&f.Max, validation.Min(f.Min), validation.Max(71)),
|
||||
validation.Field(&f.Cost, validation.Min(bcrypt.MinCost), validation.Max(bcrypt.MaxCost)),
|
||||
|
||||
@@ -287,6 +287,7 @@ func TestPasswordFieldValidateValue(t *testing.T) {
|
||||
func TestPasswordFieldValidateSettings(t *testing.T) {
|
||||
testDefaultFieldIdValidation(t, core.FieldTypePassword)
|
||||
testDefaultFieldNameValidation(t, core.FieldTypePassword)
|
||||
testDefaultFieldHelpValidation[core.PasswordField](t)
|
||||
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
@@ -66,11 +66,15 @@ type RelationField struct {
|
||||
// Hidden hides the field from the API response.
|
||||
Hidden bool `form:"hidden" json:"hidden"`
|
||||
|
||||
// ---
|
||||
|
||||
// Presentable hints the Dashboard UI to use the underlying
|
||||
// field record value in the relation preview label.
|
||||
Presentable bool `form:"presentable" json:"presentable"`
|
||||
|
||||
// ---
|
||||
// Help is an extra text explaining what the field is about.
|
||||
// It is usually shown in Dashboard UI under the field input.
|
||||
Help string `form:"help" json:"help"`
|
||||
|
||||
// CollectionId is the id of the related collection.
|
||||
CollectionId string `form:"collectionId" json:"collectionId"`
|
||||
@@ -237,6 +241,7 @@ func (f *RelationField) ValidateSettings(ctx context.Context, app App, collectio
|
||||
return validation.ValidateStruct(f,
|
||||
validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)),
|
||||
validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)),
|
||||
validation.Field(&f.Help, validation.By(DefaultFieldHelpValidationRule)),
|
||||
validation.Field(&f.CollectionId, validation.Required, validation.By(f.checkCollectionId(app, collection))),
|
||||
validation.Field(&f.MinSelect, validation.Min(0)),
|
||||
validation.Field(&f.MaxSelect, validation.When(f.MinSelect > 0, validation.Required), validation.Min(f.MinSelect)),
|
||||
|
||||
@@ -348,6 +348,7 @@ func TestRelationFieldValidateValue(t *testing.T) {
|
||||
func TestRelationFieldValidateSettings(t *testing.T) {
|
||||
testDefaultFieldIdValidation(t, core.FieldTypeRelation)
|
||||
testDefaultFieldNameValidation(t, core.FieldTypeRelation)
|
||||
testDefaultFieldHelpValidation[core.RelationField](t)
|
||||
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
@@ -66,11 +66,15 @@ type SelectField struct {
|
||||
// Hidden hides the field from the API response.
|
||||
Hidden bool `form:"hidden" json:"hidden"`
|
||||
|
||||
// ---
|
||||
|
||||
// Presentable hints the Dashboard UI to use the underlying
|
||||
// field record value in the relation preview label.
|
||||
Presentable bool `form:"presentable" json:"presentable"`
|
||||
|
||||
// ---
|
||||
// Help is an extra text explaining what the field is about.
|
||||
// It is usually shown in Dashboard UI under the field input.
|
||||
Help string `form:"help" json:"help"`
|
||||
|
||||
// Values specifies the list of accepted values.
|
||||
Values []string `form:"values" json:"values"`
|
||||
@@ -216,6 +220,7 @@ func (f *SelectField) ValidateSettings(ctx context.Context, app App, collection
|
||||
return validation.ValidateStruct(f,
|
||||
validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)),
|
||||
validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)),
|
||||
validation.Field(&f.Help, validation.By(DefaultFieldHelpValidationRule)),
|
||||
validation.Field(&f.Values, validation.Required),
|
||||
validation.Field(&f.MaxSelect, validation.Min(0), validation.Max(max)),
|
||||
)
|
||||
|
||||
@@ -337,6 +337,7 @@ func TestSelectFieldValidateValue(t *testing.T) {
|
||||
func TestSelectFieldValidateSettings(t *testing.T) {
|
||||
testDefaultFieldIdValidation(t, core.FieldTypeSelect)
|
||||
testDefaultFieldNameValidation(t, core.FieldTypeSelect)
|
||||
testDefaultFieldHelpValidation[core.SelectField](t)
|
||||
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
+61
-2
@@ -2,6 +2,8 @@ package core_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -113,7 +115,7 @@ func testDefaultFieldIdValidation(t *testing.T, fieldType string) {
|
||||
|
||||
hasErr := errs["id"] != nil
|
||||
if hasErr != s.expectError {
|
||||
t.Fatalf("Expected hasErr %v, got %v", s.expectError, hasErr)
|
||||
t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, errs)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -254,7 +256,64 @@ func testDefaultFieldNameValidation(t *testing.T, fieldType string) {
|
||||
|
||||
hasErr := errs["name"] != nil
|
||||
if hasErr != s.expectError {
|
||||
t.Fatalf("Expected hasErr %v, got %v", s.expectError, hasErr)
|
||||
t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, errs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testDefaultFieldHelpValidation[T any](t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
collection := core.NewBaseCollection("test_collection")
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
json string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
"empty value",
|
||||
`{}`,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"< max limit",
|
||||
`{"help":"abc"}`,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"= max limit",
|
||||
`{"help":"` + strings.Repeat("a", 300) + `"}`,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"> max limit",
|
||||
`{"help":"` + strings.Repeat("a", 301) + `"}`,
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run("[help] "+s.name, func(t *testing.T) {
|
||||
var zeroField T
|
||||
|
||||
field, ok := reflect.New(reflect.TypeOf(zeroField)).Interface().(core.Field)
|
||||
if !ok {
|
||||
t.Fatalf("Expected core.Field instance, got %T", zeroField)
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(s.json), &field)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
errs, _ := field.ValidateSettings(context.Background(), app, collection).(validation.Errors)
|
||||
|
||||
hasErr := errs["help"] != nil
|
||||
if hasErr != s.expectError {
|
||||
t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, errs)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+6
-1
@@ -72,11 +72,15 @@ type TextField struct {
|
||||
// Hidden hides the field from the API response.
|
||||
Hidden bool `form:"hidden" json:"hidden"`
|
||||
|
||||
// ---
|
||||
|
||||
// Presentable hints the Dashboard UI to use the underlying
|
||||
// field record value in the relation preview label.
|
||||
Presentable bool `form:"presentable" json:"presentable"`
|
||||
|
||||
// ---
|
||||
// Help is an extra text explaining what the field is about.
|
||||
// It is usually shown in Dashboard UI under the field input.
|
||||
Help string `form:"help" json:"help"`
|
||||
|
||||
// Min specifies the minimum required string characters.
|
||||
//
|
||||
@@ -283,6 +287,7 @@ func (f *TextField) ValidateSettings(ctx context.Context, app App, collection *C
|
||||
validation.By(DefaultFieldNameValidationRule),
|
||||
validation.When(f.PrimaryKey, validation.In(idColumn).Error(`The primary key must be named "id".`)),
|
||||
),
|
||||
validation.Field(&f.Help, validation.By(DefaultFieldHelpValidationRule)),
|
||||
validation.Field(&f.PrimaryKey, validation.By(f.checkOtherFieldsForPK(collection))),
|
||||
validation.Field(&f.Min, validation.Min(0), validation.Max(maxSafeJSONInt)),
|
||||
validation.Field(&f.Max, validation.Min(f.Min), validation.Max(maxSafeJSONInt)),
|
||||
|
||||
@@ -381,6 +381,7 @@ func TestTextFieldValidateValue(t *testing.T) {
|
||||
func TestTextFieldValidateSettings(t *testing.T) {
|
||||
testDefaultFieldIdValidation(t, core.FieldTypeText)
|
||||
testDefaultFieldNameValidation(t, core.FieldTypeText)
|
||||
testDefaultFieldHelpValidation[core.TextField](t)
|
||||
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
+6
-1
@@ -39,11 +39,15 @@ type URLField struct {
|
||||
// Hidden hides the field from the API response.
|
||||
Hidden bool `form:"hidden" json:"hidden"`
|
||||
|
||||
// ---
|
||||
|
||||
// Presentable hints the Dashboard UI to use the underlying
|
||||
// field record value in the relation preview label.
|
||||
Presentable bool `form:"presentable" json:"presentable"`
|
||||
|
||||
// ---
|
||||
// Help is an extra text explaining what the field is about.
|
||||
// It is usually shown in Dashboard UI under the field input.
|
||||
Help string `form:"help" json:"help"`
|
||||
|
||||
// ExceptDomains will require the URL domain to NOT be included in the listed ones.
|
||||
//
|
||||
@@ -156,6 +160,7 @@ func (f *URLField) ValidateSettings(ctx context.Context, app App, collection *Co
|
||||
return validation.ValidateStruct(f,
|
||||
validation.Field(&f.Id, validation.By(DefaultFieldIdValidationRule)),
|
||||
validation.Field(&f.Name, validation.By(DefaultFieldNameValidationRule)),
|
||||
validation.Field(&f.Help, validation.By(DefaultFieldHelpValidationRule)),
|
||||
validation.Field(
|
||||
&f.ExceptDomains,
|
||||
validation.When(len(f.OnlyDomains) > 0, validation.Empty).Else(validation.Each(is.Domain)),
|
||||
|
||||
@@ -182,6 +182,7 @@ func TestURLFieldValidateValue(t *testing.T) {
|
||||
func TestURLFieldValidateSettings(t *testing.T) {
|
||||
testDefaultFieldIdValidation(t, core.FieldTypeURL)
|
||||
testDefaultFieldNameValidation(t, core.FieldTypeURL)
|
||||
testDefaultFieldHelpValidation[core.URLField](t)
|
||||
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
@@ -473,13 +473,13 @@ func TestFieldsListScan(t *testing.T) {
|
||||
"only the minimum field options",
|
||||
`[{"id":"123","name":"test1","type":"text","required":true},{"id":"456","name":"test2","type":"bool"}]`,
|
||||
false,
|
||||
`[{"autogeneratePattern":"","hidden":false,"id":"123","max":0,"min":0,"name":"test1","pattern":"","presentable":false,"primaryKey":false,"required":true,"system":false,"type":"text"},{"hidden":false,"id":"456","name":"test2","presentable":false,"required":false,"system":false,"type":"bool"}]`,
|
||||
`[{"autogeneratePattern":"","help":"","hidden":false,"id":"123","max":0,"min":0,"name":"test1","pattern":"","presentable":false,"primaryKey":false,"required":true,"system":false,"type":"text"},{"help":"","hidden":false,"id":"456","name":"test2","presentable":false,"required":false,"system":false,"type":"bool"}]`,
|
||||
},
|
||||
{
|
||||
"all field options",
|
||||
`[{"autogeneratePattern":"","hidden":true,"id":"123","max":12,"min":0,"name":"test1","pattern":"","presentable":true,"primaryKey":false,"required":true,"system":false,"type":"text"},{"hidden":false,"id":"456","name":"test2","presentable":false,"required":false,"system":true,"type":"bool"}]`,
|
||||
`[{"autogeneratePattern":"","help":"abc","hidden":true,"id":"123","max":12,"min":0,"name":"test1","pattern":"","presentable":true,"primaryKey":false,"required":true,"system":false,"type":"text"},{"help":"def","hidden":false,"id":"456","name":"test2","presentable":false,"required":false,"system":true,"type":"bool"}]`,
|
||||
false,
|
||||
`[{"autogeneratePattern":"","hidden":true,"id":"123","max":12,"min":0,"name":"test1","pattern":"","presentable":true,"primaryKey":false,"required":true,"system":false,"type":"text"},{"hidden":false,"id":"456","name":"test2","presentable":false,"required":false,"system":true,"type":"bool"}]`,
|
||||
`[{"autogeneratePattern":"","help":"abc","hidden":true,"id":"123","max":12,"min":0,"name":"test1","pattern":"","presentable":true,"primaryKey":false,"required":true,"system":false,"type":"text"},{"help":"def","hidden":false,"id":"456","name":"test2","presentable":false,"required":false,"system":true,"type":"bool"}]`,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -523,13 +523,13 @@ func TestFieldsListJSON(t *testing.T) {
|
||||
"only the minimum field options",
|
||||
`[{"id":"123","name":"test1","type":"text","required":true},{"id":"456","name":"test2","type":"bool"}]`,
|
||||
false,
|
||||
`[{"autogeneratePattern":"","hidden":false,"id":"123","max":0,"min":0,"name":"test1","pattern":"","presentable":false,"primaryKey":false,"required":true,"system":false,"type":"text"},{"hidden":false,"id":"456","name":"test2","presentable":false,"required":false,"system":false,"type":"bool"}]`,
|
||||
`[{"autogeneratePattern":"","help":"","hidden":false,"id":"123","max":0,"min":0,"name":"test1","pattern":"","presentable":false,"primaryKey":false,"required":true,"system":false,"type":"text"},{"help":"","hidden":false,"id":"456","name":"test2","presentable":false,"required":false,"system":false,"type":"bool"}]`,
|
||||
},
|
||||
{
|
||||
"all field options",
|
||||
`[{"autogeneratePattern":"","hidden":true,"id":"123","max":12,"min":0,"name":"test1","pattern":"","presentable":true,"primaryKey":false,"required":true,"system":false,"type":"text"},{"hidden":false,"id":"456","name":"test2","presentable":false,"required":false,"system":true,"type":"bool"}]`,
|
||||
`[{"autogeneratePattern":"","help":"abc","hidden":true,"id":"123","max":12,"min":0,"name":"test1","pattern":"","presentable":true,"primaryKey":false,"required":true,"system":false,"type":"text"},{"help":"def","hidden":false,"id":"456","name":"test2","presentable":false,"required":false,"system":true,"type":"bool"}]`,
|
||||
false,
|
||||
`[{"autogeneratePattern":"","hidden":true,"id":"123","max":12,"min":0,"name":"test1","pattern":"","presentable":true,"primaryKey":false,"required":true,"system":false,"type":"text"},{"hidden":false,"id":"456","name":"test2","presentable":false,"required":false,"system":true,"type":"bool"}]`,
|
||||
`[{"autogeneratePattern":"","help":"abc","hidden":true,"id":"123","max":12,"min":0,"name":"test1","pattern":"","presentable":true,"primaryKey":false,"required":true,"system":false,"type":"text"},{"help":"def","hidden":false,"id":"456","name":"test2","presentable":false,"required":false,"system":true,"type":"bool"}]`,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@ func newDefaultSettings() *Settings {
|
||||
isNew: true,
|
||||
settings: settings{
|
||||
Meta: MetaConfig{
|
||||
AccentColor: "#1055c9",
|
||||
AppName: "Acme",
|
||||
AppURL: "http://localhost:8090",
|
||||
HideControls: false,
|
||||
@@ -513,6 +514,11 @@ func checkCronExpression(value any) error {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type MetaConfig struct {
|
||||
// @todo experimental
|
||||
//
|
||||
// AccentColor specify the UI "accent" color (HEX).
|
||||
AccentColor string `form:"accentColor" json:"accentColor"`
|
||||
|
||||
AppName string `form:"appName" json:"appName"`
|
||||
AppURL string `form:"appURL" json:"appURL"`
|
||||
SenderName string `form:"senderName" json:"senderName"`
|
||||
@@ -523,6 +529,7 @@ type MetaConfig struct {
|
||||
// Validate makes MetaConfig validatable by implementing [validation.Validatable] interface.
|
||||
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)),
|
||||
validation.Field(&c.AppURL, validation.Required, is.URL),
|
||||
validation.Field(&c.SenderName, validation.Required, validation.Length(1, 255)),
|
||||
|
||||
@@ -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":{"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 := `{"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}}`
|
||||
if valueStr != expected {
|
||||
t.Fatalf("Expected exported settings\n%s\ngot\n%s", expected, valueStr)
|
||||
}
|
||||
@@ -93,6 +93,8 @@ func TestSettings_DBExport(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSettingsMerge(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s1 := &core.Settings{}
|
||||
s1.Meta.AppURL = "app_url" // should be unset
|
||||
|
||||
@@ -126,6 +128,8 @@ func TestSettingsMerge(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSettingsClone(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
s1 := &core.Settings{}
|
||||
s1.Meta.AppName = "test_name"
|
||||
|
||||
@@ -156,6 +160,8 @@ func TestSettingsClone(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSettingsMarshalJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
settings := &core.Settings{}
|
||||
|
||||
// control fields
|
||||
@@ -174,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":{"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 := `{"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}}`
|
||||
|
||||
if rawStr != expected {
|
||||
t.Fatalf("Expected\n%v\ngot\n%v", expected, rawStr)
|
||||
@@ -230,6 +236,8 @@ func TestSettingsValidate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMetaConfigValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
config core.MetaConfig
|
||||
@@ -248,12 +256,14 @@ func TestMetaConfigValidate(t *testing.T) {
|
||||
{
|
||||
"invalid data",
|
||||
core.MetaConfig{
|
||||
AccentColor: "#fff",
|
||||
AppName: strings.Repeat("a", 300),
|
||||
AppURL: "test",
|
||||
SenderName: strings.Repeat("a", 300),
|
||||
SenderAddress: "invalid_email",
|
||||
},
|
||||
[]string{
|
||||
"accentColor",
|
||||
"appName",
|
||||
"appURL",
|
||||
"senderName",
|
||||
@@ -263,6 +273,7 @@ func TestMetaConfigValidate(t *testing.T) {
|
||||
{
|
||||
"valid data",
|
||||
core.MetaConfig{
|
||||
AccentColor: "#ffffff",
|
||||
AppName: "test",
|
||||
AppURL: "https://example.com",
|
||||
SenderName: "test",
|
||||
@@ -282,6 +293,8 @@ func TestMetaConfigValidate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLogsConfigValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
config core.LogsConfig
|
||||
@@ -314,6 +327,8 @@ func TestLogsConfigValidate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSMTPConfigValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
config core.SMTPConfig
|
||||
@@ -373,6 +388,8 @@ func TestSMTPConfigValidate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestS3ConfigValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
config core.S3Config
|
||||
@@ -444,6 +461,8 @@ func TestS3ConfigValidate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBackupsConfigValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
config core.BackupsConfig
|
||||
@@ -499,6 +518,8 @@ func TestBackupsConfigValidate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBatchConfigValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
config core.BatchConfig
|
||||
@@ -554,6 +575,8 @@ func TestBatchConfigValidate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRateLimitsConfigValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
config core.RateLimitsConfig
|
||||
@@ -699,6 +722,8 @@ func TestRateLimitsConfigValidate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRateLimitsFindRateLimitRule(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
limits := core.RateLimitsConfig{
|
||||
Rules: []core.RateLimitRule{
|
||||
{Label: "abc"},
|
||||
@@ -753,6 +778,8 @@ func TestRateLimitsFindRateLimitRule(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRateLimitRuleValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
rule core.RateLimitRule
|
||||
@@ -860,6 +887,8 @@ func TestRateLimitRuleValidate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRateLimitRuleDurationTime(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scenarios := []struct {
|
||||
rule core.RateLimitRule
|
||||
expected time.Duration
|
||||
@@ -880,6 +909,8 @@ func TestRateLimitRuleDurationTime(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRateLimitRuleString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
rule core.RateLimitRule
|
||||
|
||||
+107
-16
@@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -36,17 +37,14 @@ func (app *BaseApp) DeleteView(dangerousViewName string) error {
|
||||
func (app *BaseApp) SaveView(dangerousViewName string, dangerousSelectQuery string) error {
|
||||
return app.RunInTransaction(func(txApp App) error {
|
||||
// delete old view (if exists)
|
||||
if err := txApp.DeleteView(dangerousViewName); err != nil {
|
||||
err := txApp.DeleteView(dangerousViewName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dangerousSelectQuery = strings.Trim(strings.TrimSpace(dangerousSelectQuery), ";")
|
||||
|
||||
// try to loosely detect multiple inline statements
|
||||
tk := tokenizer.NewFromString(dangerousSelectQuery)
|
||||
tk.Separators(';')
|
||||
if queryParts, _ := tk.ScanAll(); len(queryParts) > 1 {
|
||||
return errors.New("multiple statements are not supported")
|
||||
dangerousSelectQuery, err = normalizeViewSelectQuery(dangerousSelectQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// (re)create the view
|
||||
@@ -54,7 +52,8 @@ func (app *BaseApp) SaveView(dangerousViewName string, dangerousSelectQuery stri
|
||||
// note: the query is wrapped in a secondary SELECT as a rudimentary
|
||||
// measure to discourage multiple inline sql statements execution
|
||||
viewQuery := fmt.Sprintf("CREATE VIEW {{%s}} AS SELECT * FROM (%s)", dangerousViewName, dangerousSelectQuery)
|
||||
if _, err := txApp.DB().NewQuery(viewQuery).Execute(); err != nil {
|
||||
_, err = txApp.DB().NewQuery(viewQuery).Execute()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -124,6 +123,76 @@ func (app *BaseApp) CreateViewFields(dangerousSelectQuery string) (FieldsList, e
|
||||
return result, txErr
|
||||
}
|
||||
|
||||
type DryRunViewResult struct {
|
||||
Fields FieldsList `json:"fields"`
|
||||
Sample []*Record `json:"sample"`
|
||||
}
|
||||
|
||||
// DryRunView executes the provided query by creating a temporary view
|
||||
// collection and returning a sample of the resulting query records (if valid).
|
||||
//
|
||||
// The same caveats from CreateViewFields apply here too.
|
||||
//
|
||||
// NB! Be aware that this method is vulnerable to SQL injection and the
|
||||
// "dangerousSelectQuery" argument must come only from trusted input!
|
||||
func (app *BaseApp) DryRunView(dangerousSelectQuery string, sampleSize int) (*DryRunViewResult, error) {
|
||||
dangerousSelectQuery, err := normalizeViewSelectQuery(dangerousSelectQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fields, err := app.CreateViewFields(dangerousSelectQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tempName := "temp_view_" + security.RandomString(5)
|
||||
tempCollection := NewViewCollection(tempName)
|
||||
tempCollection.Fields = fields
|
||||
|
||||
// validate generated view fields
|
||||
ctx := context.Background()
|
||||
for i, f := range fields {
|
||||
err = f.ValidateSettings(ctx, app, tempCollection)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid field %q (%d): %w", f.GetName(), i, err)
|
||||
}
|
||||
}
|
||||
|
||||
records := []*Record{}
|
||||
|
||||
err = app.RecordQuery(tempCollection).
|
||||
// note: the query is wrapped in a secondary SELECT as a rudimentary
|
||||
// measure to discourage multiple inline sql statements execution
|
||||
From("(SELECT * FROM (" + dangerousSelectQuery + ")) as " + tempName).
|
||||
Limit(int64(sampleSize)).
|
||||
All(&records)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve query records: %w", err)
|
||||
}
|
||||
|
||||
// warn for possible empty or duplicated record ids found in the sample
|
||||
// (it is not intended for security and it is here to quickly provide a
|
||||
// helpful error message without doing multiple query executions)
|
||||
ids := make(map[string]struct{}, len(records))
|
||||
for _, r := range records {
|
||||
if r.Id == "" {
|
||||
return nil, errors.New("the query could return records with empty or invalid ids")
|
||||
}
|
||||
|
||||
if _, ok := ids[r.Id]; ok {
|
||||
return nil, errors.New("the query could return records with non-unique ids")
|
||||
}
|
||||
|
||||
ids[r.Id] = struct{}{}
|
||||
}
|
||||
|
||||
return &DryRunViewResult{
|
||||
Fields: fields,
|
||||
Sample: records,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FindRecordByViewFile returns the original Record of the provided view collection file.
|
||||
func (app *BaseApp) FindRecordByViewFile(viewCollectionModelOrIdentifier any, fileFieldName string, filename string) (*Record, error) {
|
||||
view, err := getCollectionByModelOrIdentifier(app, viewCollectionModelOrIdentifier)
|
||||
@@ -198,6 +267,20 @@ func (app *BaseApp) FindRecordByViewFile(viewCollectionModelOrIdentifier any, fi
|
||||
// Raw query to schema helpers
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// loosely normalizes the specified view query and warn against multiple inline statements
|
||||
// (the check is not perfect and it is NOT intended as a security measure; it is done primarily to provide a helpful error message)
|
||||
func normalizeViewSelectQuery(dangerousSelectQuery string) (string, error) {
|
||||
dangerousSelectQuery = strings.Trim(strings.TrimSpace(dangerousSelectQuery), ";")
|
||||
|
||||
tk := tokenizer.NewFromString(dangerousSelectQuery)
|
||||
tk.Separators(';')
|
||||
if queryParts, _ := tk.ScanAll(); len(queryParts) > 1 {
|
||||
return "", errors.New("multiple statements are not supported")
|
||||
}
|
||||
|
||||
return dangerousSelectQuery, nil
|
||||
}
|
||||
|
||||
type queryField struct {
|
||||
// field is the final resolved field.
|
||||
field Field
|
||||
@@ -212,12 +295,26 @@ type queryField struct {
|
||||
}
|
||||
|
||||
func defaultViewField(name string) Field {
|
||||
if name == FieldNameId {
|
||||
return defaultViewIdField()
|
||||
}
|
||||
|
||||
return &JSONField{
|
||||
Name: name,
|
||||
MaxSize: 1, // unused for views
|
||||
}
|
||||
}
|
||||
|
||||
func defaultViewIdField() Field {
|
||||
return &TextField{
|
||||
Name: FieldNameId,
|
||||
System: true,
|
||||
Required: true,
|
||||
PrimaryKey: true,
|
||||
Pattern: `^[a-z0-9]+$`,
|
||||
}
|
||||
}
|
||||
|
||||
var castRegex = regexp.MustCompile(`(?is)^cast\s*\(.*\s+as\s+(\w+)\s*\)$`)
|
||||
|
||||
func parseQueryToFields(app App, selectQuery string) (map[string]*queryField, error) {
|
||||
@@ -245,13 +342,7 @@ func parseQueryToFields(app App, selectQuery string) (map[string]*queryField, er
|
||||
// pk (always assume text field for now)
|
||||
if col.alias == FieldNameId {
|
||||
result[col.alias] = &queryField{
|
||||
field: &TextField{
|
||||
Name: col.alias,
|
||||
System: true,
|
||||
Required: true,
|
||||
PrimaryKey: true,
|
||||
Pattern: `^[a-z0-9]+$`,
|
||||
},
|
||||
field: defaultViewIdField(),
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -732,3 +732,129 @@ func TestFindRecordByViewFile(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDryRunView(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
query string
|
||||
sampleSize int
|
||||
expectError bool
|
||||
expectFields map[string]string // name-type pairs
|
||||
expectSampleIds []string // record ids of the resulting sample
|
||||
}{
|
||||
{
|
||||
"empty query",
|
||||
"",
|
||||
10,
|
||||
true,
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"non-select query",
|
||||
"CREATE TABLE t1(x INT)",
|
||||
10,
|
||||
true,
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"multiple inline select statements",
|
||||
"select 'a' as id; select 'b' as id",
|
||||
10,
|
||||
true,
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"select with invalid formatted field name",
|
||||
"select 'a' as id, count(*)", // missing field alias
|
||||
10,
|
||||
true,
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"select resolving to records with missing id",
|
||||
"(select 'a' as id UNION ALL select null as id UNION ALL select 'c' as id)",
|
||||
10,
|
||||
true,
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"select resolving to records with duplicated ids",
|
||||
"(select 'a' as id UNION ALL select 'a' as id UNION ALL select 'c' as id)",
|
||||
10,
|
||||
true,
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"no sample size and valid select query but with invalid records result",
|
||||
"(select 'a' as id UNION ALL select 'a' as id UNION ALL select 'c' as id)",
|
||||
0,
|
||||
false, // still "valid" because there is no sample to check
|
||||
map[string]string{"id": "text"},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"sample size < total select records",
|
||||
"(select 'a' as id UNION ALL select 'b' as id UNION ALL select 'c' as id UNION ALL select 'd' as id)",
|
||||
3,
|
||||
false,
|
||||
map[string]string{"id": "text"},
|
||||
[]string{"a", "b", "c"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result, err := app.DryRunView(s.query, s.sampleSize)
|
||||
|
||||
hasErr := err != nil
|
||||
if hasErr != s.expectError {
|
||||
t.Fatalf("Expected hasErr %v, got %v (%v)", s.expectError, hasErr, err)
|
||||
}
|
||||
|
||||
if hasErr {
|
||||
return
|
||||
}
|
||||
|
||||
// check fields
|
||||
// ---
|
||||
if len(s.expectFields) != len(result.Fields) {
|
||||
serialized, _ := json.Marshal(result.Fields)
|
||||
t.Fatalf("Expected %d fields, got %d: \n%s", len(s.expectFields), len(result.Fields), serialized)
|
||||
}
|
||||
for name, typ := range s.expectFields {
|
||||
field := result.Fields.GetByName(name)
|
||||
if field == nil {
|
||||
t.Fatalf("Expected to find field %s, got nil", name)
|
||||
}
|
||||
|
||||
if field.Type() != typ {
|
||||
t.Fatalf("Expected field %s to be %q, got %q", name, typ, field.Type())
|
||||
}
|
||||
}
|
||||
|
||||
// check sample ids
|
||||
// ---
|
||||
if len(s.expectSampleIds) != len(result.Sample) {
|
||||
t.Fatalf("Expected %d sample records, got %d", len(s.expectSampleIds), len(result.Sample))
|
||||
}
|
||||
for i, r := range result.Sample {
|
||||
if s.expectSampleIds[i] != r.Id {
|
||||
t.Fatalf("Expected sample record id %q, got %q at %d", s.expectSampleIds[i], r.Id, i)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ensureNoTempViews(app, t)
|
||||
}
|
||||
|
||||
+45
-10
@@ -289,9 +289,13 @@ func wrapMiddlewares(executors *vmsPool, rawMiddlewares ...goja.Value) ([]*hook.
|
||||
return wrappedMiddlewares, nil
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
var cachedArrayOfTypes = store.New[reflect.Type, reflect.Type](nil)
|
||||
|
||||
func baseBinds(vm *goja.Runtime) {
|
||||
// BindCore registers common core objects and functions such as sleep,
|
||||
// toString, DynamicModel, etc. into the provided runtime.
|
||||
func BindCore(vm *goja.Runtime) {
|
||||
vm.SetFieldNameMapper(FieldMapper{})
|
||||
|
||||
// deprecated: use toString
|
||||
@@ -659,7 +663,10 @@ func baseBinds(vm *goja.Runtime) {
|
||||
})
|
||||
}
|
||||
|
||||
func dbxBinds(vm *goja.Runtime) {
|
||||
// BindDbx registers $dbx.* namespaced object with dbx database builder related methods.
|
||||
//
|
||||
// See https://pocketbase.io/jsvm/modules/_dbx.html.
|
||||
func BindDbx(vm *goja.Runtime) {
|
||||
obj := vm.NewObject()
|
||||
vm.Set("$dbx", obj)
|
||||
|
||||
@@ -682,7 +689,10 @@ func dbxBinds(vm *goja.Runtime) {
|
||||
obj.Set("notBetween", dbx.NotBetween)
|
||||
}
|
||||
|
||||
func mailsBinds(vm *goja.Runtime) {
|
||||
// BindMails registers $mail.* namespaced object with common mail related helpers.
|
||||
//
|
||||
// See https://pocketbase.io/jsvm/modules/_mails.html.
|
||||
func BindMails(vm *goja.Runtime) {
|
||||
obj := vm.NewObject()
|
||||
vm.Set("$mails", obj)
|
||||
|
||||
@@ -693,7 +703,10 @@ func mailsBinds(vm *goja.Runtime) {
|
||||
obj.Set("sendRecordAuthAlert", mails.SendRecordAuthAlert)
|
||||
}
|
||||
|
||||
func securityBinds(vm *goja.Runtime) {
|
||||
// BindSecurity registers $security.* namespaced object with common security related helpers.
|
||||
//
|
||||
// See https://pocketbase.io/jsvm/modules/_security.html.
|
||||
func BindSecurity(vm *goja.Runtime) {
|
||||
obj := vm.NewObject()
|
||||
vm.Set("$security", obj)
|
||||
|
||||
@@ -736,7 +749,11 @@ func securityBinds(vm *goja.Runtime) {
|
||||
})
|
||||
}
|
||||
|
||||
func filesystemBinds(vm *goja.Runtime) {
|
||||
// BindFilesystem registers $filesystem.* namespaced object with
|
||||
// common filesystem package related helpers.
|
||||
//
|
||||
// See https://pocketbase.io/jsvm/modules/_filesystem.html.
|
||||
func BindFilesystem(vm *goja.Runtime) {
|
||||
obj := vm.NewObject()
|
||||
vm.Set("$filesystem", obj)
|
||||
|
||||
@@ -757,7 +774,11 @@ func filesystemBinds(vm *goja.Runtime) {
|
||||
})
|
||||
}
|
||||
|
||||
func filepathBinds(vm *goja.Runtime) {
|
||||
// BindFilesystem registers $filepath.* namespaced object with
|
||||
// common std Go filepath package related exports.
|
||||
//
|
||||
// See https://pocketbase.io/jsvm/modules/_filepath.html.
|
||||
func BindFilepath(vm *goja.Runtime) {
|
||||
obj := vm.NewObject()
|
||||
vm.Set("$filepath", obj)
|
||||
|
||||
@@ -778,7 +799,11 @@ func filepathBinds(vm *goja.Runtime) {
|
||||
obj.Set("walkDir", filepath.WalkDir)
|
||||
}
|
||||
|
||||
func osBinds(vm *goja.Runtime) {
|
||||
// BindFilesystem registers $os.* namespaced object with
|
||||
// common std Go os package related exports.
|
||||
//
|
||||
// See https://pocketbase.io/jsvm/modules/_os.html.
|
||||
func BindOs(vm *goja.Runtime) {
|
||||
obj := vm.NewObject()
|
||||
vm.Set("$os", obj)
|
||||
|
||||
@@ -804,14 +829,20 @@ func osBinds(vm *goja.Runtime) {
|
||||
obj.Set("openInRoot", os.OpenInRoot)
|
||||
}
|
||||
|
||||
func formsBinds(vm *goja.Runtime) {
|
||||
// BindForms registers various application form constructors.
|
||||
// These bindings are mostly used internally and/or preserved for backward compatibility with earlier versions.
|
||||
func BindForms(vm *goja.Runtime) {
|
||||
registerFactoryAsConstructor(vm, "AppleClientSecretCreateForm", forms.NewAppleClientSecretCreate)
|
||||
registerFactoryAsConstructor(vm, "RecordUpsertForm", forms.NewRecordUpsert)
|
||||
registerFactoryAsConstructor(vm, "TestEmailSendForm", forms.NewTestEmailSend)
|
||||
registerFactoryAsConstructor(vm, "TestS3FilesystemForm", forms.NewTestS3Filesystem)
|
||||
}
|
||||
|
||||
func apisBinds(vm *goja.Runtime) {
|
||||
// BindApis registers $apis.* namespaced object with reusable Web API
|
||||
// handlers, middlewares and other related helpers.
|
||||
//
|
||||
// See https://pocketbase.io/jsvm/modules/_apis.html.
|
||||
func BindApis(vm *goja.Runtime) {
|
||||
obj := vm.NewObject()
|
||||
vm.Set("$apis", obj)
|
||||
|
||||
@@ -850,7 +881,11 @@ func apisBinds(vm *goja.Runtime) {
|
||||
registerFactoryAsConstructor(vm, "InternalServerError", router.NewInternalServerError)
|
||||
}
|
||||
|
||||
func httpClientBinds(vm *goja.Runtime) {
|
||||
// BindHttpClient registers $http.* namespaced object with common utils
|
||||
// for sending HTTP requests.
|
||||
//
|
||||
// See https://pocketbase.io/jsvm/modules/_http.html.
|
||||
func BindHttpClient(vm *goja.Runtime) {
|
||||
obj := vm.NewObject()
|
||||
vm.Set("$http", obj)
|
||||
|
||||
|
||||
+88
-88
@@ -43,16 +43,16 @@ func testBindsCount(vm *goja.Runtime, namespace string, count int, t *testing.T)
|
||||
|
||||
// note: this test is useful as a reminder to update the tests in case
|
||||
// a new base binding is added.
|
||||
func TestBaseBindsCount(t *testing.T) {
|
||||
func TestBindCoreCount(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
|
||||
testBindsCount(vm, "this", 41, t)
|
||||
}
|
||||
|
||||
func TestBaseBindsSleep(t *testing.T) {
|
||||
func TestBindCoreSleep(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
vm.Set("reader", strings.NewReader("test"))
|
||||
|
||||
start := time.Now()
|
||||
@@ -69,9 +69,9 @@ func TestBaseBindsSleep(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseBindsReaderToString(t *testing.T) {
|
||||
func TestBindCoreReaderToString(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
vm.Set("reader", strings.NewReader("test"))
|
||||
|
||||
_, err := vm.RunString(`
|
||||
@@ -86,9 +86,9 @@ func TestBaseBindsReaderToString(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseBindsToString(t *testing.T) {
|
||||
func TestBindCoreToString(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
vm.Set("scenarios", []struct {
|
||||
Name string
|
||||
Value any
|
||||
@@ -120,9 +120,9 @@ func TestBaseBindsToString(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseBindsToBytes(t *testing.T) {
|
||||
func TestBindCoreToBytes(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
vm.Set("bytesEqual", bytes.Equal)
|
||||
vm.Set("scenarios", []struct {
|
||||
Name string
|
||||
@@ -160,9 +160,9 @@ func TestBaseBindsToBytes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseBindsUnmarshal(t *testing.T) {
|
||||
func TestBindCoreUnmarshal(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
vm.Set("data", &map[string]any{"a": 123})
|
||||
|
||||
_, err := vm.RunString(`
|
||||
@@ -181,9 +181,9 @@ func TestBaseBindsUnmarshal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseBindsContext(t *testing.T) {
|
||||
func TestBindCoreContext(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
|
||||
_, err := vm.RunString(`
|
||||
const base = new Context(null, "a", 123);
|
||||
@@ -205,9 +205,9 @@ func TestBaseBindsContext(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseBindsCookie(t *testing.T) {
|
||||
func TestBindCoreCookie(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
|
||||
_, err := vm.RunString(`
|
||||
const cookie = new Cookie({
|
||||
@@ -234,9 +234,9 @@ func TestBaseBindsCookie(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseBindsSubscriptionMessage(t *testing.T) {
|
||||
func TestBindCoreSubscriptionMessage(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
vm.Set("bytesToString", func(b []byte) string {
|
||||
return string(b)
|
||||
})
|
||||
@@ -262,7 +262,7 @@ func TestBaseBindsSubscriptionMessage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseBindsRecord(t *testing.T) {
|
||||
func TestBindCoreRecord(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
@@ -272,7 +272,7 @@ func TestBaseBindsRecord(t *testing.T) {
|
||||
}
|
||||
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
vm.Set("collection", collection)
|
||||
|
||||
// without record data
|
||||
@@ -308,9 +308,9 @@ func TestBaseBindsRecord(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseBindsCollection(t *testing.T) {
|
||||
func TestBindCoreCollection(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
|
||||
v, err := vm.RunString(`new Collection({ name: "test", createRule: "@request.auth.id != ''", fields: [{name: "title", "type": "text"}] })`)
|
||||
if err != nil {
|
||||
@@ -336,9 +336,9 @@ func TestBaseBindsCollection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseBindsFieldsList(t *testing.T) {
|
||||
func TestBindCoreFieldsList(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
|
||||
v, err := vm.RunString(`new FieldsList([{name: "title", "type": "text"}])`)
|
||||
if err != nil {
|
||||
@@ -355,9 +355,9 @@ func TestBaseBindsFieldsList(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseBindsField(t *testing.T) {
|
||||
func TestBindCoreField(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
|
||||
v, err := vm.RunString(`new Field({name: "test", "type": "bool"})`)
|
||||
if err != nil {
|
||||
@@ -379,11 +379,11 @@ func isType[T any](v any) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
func TestBaseBindsNamedFields(t *testing.T) {
|
||||
func TestBindCoreNamedFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
|
||||
scenarios := []struct {
|
||||
js string
|
||||
@@ -470,9 +470,9 @@ func TestBaseBindsNamedFields(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseBindsMailerMessage(t *testing.T) {
|
||||
func TestBindCoreMailerMessage(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
|
||||
v, err := vm.RunString(`new MailerMessage({
|
||||
from: {name: "test_from", address: "test_from@example.com"},
|
||||
@@ -517,9 +517,9 @@ func TestBaseBindsMailerMessage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseBindsCommand(t *testing.T) {
|
||||
func TestBindCoreCommand(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
|
||||
_, err := vm.RunString(`
|
||||
let runCalls = 0;
|
||||
@@ -546,9 +546,9 @@ func TestBaseBindsCommand(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseBindsRequestInfo(t *testing.T) {
|
||||
func TestBindCoreRequestInfo(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
|
||||
_, err := vm.RunString(`
|
||||
const info = new RequestInfo({
|
||||
@@ -564,9 +564,9 @@ func TestBaseBindsRequestInfo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseBindsMiddleware(t *testing.T) {
|
||||
func TestBindCoreMiddleware(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
|
||||
_, err := vm.RunString(`
|
||||
const m = new Middleware(
|
||||
@@ -584,9 +584,9 @@ func TestBaseBindsMiddleware(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseBindsTimezone(t *testing.T) {
|
||||
func TestBindCoreTimezone(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
|
||||
_, err := vm.RunString(`
|
||||
const v0 = (new Timezone()).string();
|
||||
@@ -609,9 +609,9 @@ func TestBaseBindsTimezone(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseBindsDateTime(t *testing.T) {
|
||||
func TestBindCoreDateTime(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
|
||||
_, err := vm.RunString(`
|
||||
const now = new DateTime();
|
||||
@@ -650,9 +650,9 @@ func TestBaseBindsDateTime(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseBindsValidationError(t *testing.T) {
|
||||
func TestBindCoreValidationError(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
|
||||
scenarios := []struct {
|
||||
js string
|
||||
@@ -697,14 +697,14 @@ func TestBaseBindsValidationError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDbxBinds(t *testing.T) {
|
||||
func TestBindDbx(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
vm := goja.New()
|
||||
vm.Set("db", app.DB())
|
||||
baseBinds(vm)
|
||||
dbxBinds(vm)
|
||||
BindCore(vm)
|
||||
BindDbx(vm)
|
||||
|
||||
testBindsCount(vm, "$dbx", 15, t)
|
||||
|
||||
@@ -792,14 +792,14 @@ func TestDbxBinds(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMailsBindsCount(t *testing.T) {
|
||||
func TestBindMailsCount(t *testing.T) {
|
||||
vm := goja.New()
|
||||
mailsBinds(vm)
|
||||
BindMails(vm)
|
||||
|
||||
testBindsCount(vm, "$mails", 5, t)
|
||||
}
|
||||
|
||||
func TestMailsBinds(t *testing.T) {
|
||||
func TestBindMails(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
@@ -809,8 +809,8 @@ func TestMailsBinds(t *testing.T) {
|
||||
}
|
||||
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
mailsBinds(vm)
|
||||
BindCore(vm)
|
||||
BindMails(vm)
|
||||
vm.Set("$app", app)
|
||||
vm.Set("record", record)
|
||||
|
||||
@@ -845,17 +845,17 @@ func TestMailsBinds(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecurityBindsCount(t *testing.T) {
|
||||
func TestBindSecurityCount(t *testing.T) {
|
||||
vm := goja.New()
|
||||
securityBinds(vm)
|
||||
BindSecurity(vm)
|
||||
|
||||
testBindsCount(vm, "$security", 16, t)
|
||||
}
|
||||
|
||||
func TestSecurityCryptoBinds(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
securityBinds(vm)
|
||||
BindCore(vm)
|
||||
BindSecurity(vm)
|
||||
|
||||
sceneraios := []struct {
|
||||
js string
|
||||
@@ -888,8 +888,8 @@ func TestSecurityCryptoBinds(t *testing.T) {
|
||||
|
||||
func TestSecurityRandomStringBinds(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
securityBinds(vm)
|
||||
BindCore(vm)
|
||||
BindSecurity(vm)
|
||||
|
||||
sceneraios := []struct {
|
||||
js string
|
||||
@@ -964,8 +964,8 @@ func TestSecurityJWTBinds(t *testing.T) {
|
||||
for _, s := range sceneraios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
securityBinds(vm)
|
||||
BindCore(vm)
|
||||
BindSecurity(vm)
|
||||
|
||||
_, err := vm.RunString(s.js)
|
||||
if err != nil {
|
||||
@@ -977,8 +977,8 @@ func TestSecurityJWTBinds(t *testing.T) {
|
||||
|
||||
func TestSecurityEncryptAndDecryptBinds(t *testing.T) {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
securityBinds(vm)
|
||||
BindCore(vm)
|
||||
BindSecurity(vm)
|
||||
|
||||
_, err := vm.RunString(`
|
||||
const key = "abcdabcdabcdabcdabcdabcdabcdabcd"
|
||||
@@ -996,7 +996,7 @@ func TestSecurityEncryptAndDecryptBinds(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilesystemBinds(t *testing.T) {
|
||||
func TestBindFilesystem(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
@@ -1020,8 +1020,8 @@ func TestFilesystemBinds(t *testing.T) {
|
||||
vm.Set("tmpDir", tmpDir)
|
||||
vm.Set("testFile", filepath.Join(app.DataDir(), "data.db"))
|
||||
vm.Set("baseURL", srv.URL)
|
||||
baseBinds(vm)
|
||||
filesystemBinds(vm)
|
||||
BindCore(vm)
|
||||
BindFilesystem(vm)
|
||||
|
||||
testBindsCount(vm, "$filesystem", 6, t)
|
||||
|
||||
@@ -1116,24 +1116,24 @@ func TestFilesystemBinds(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormsBinds(t *testing.T) {
|
||||
func TestBindForms(t *testing.T) {
|
||||
vm := goja.New()
|
||||
formsBinds(vm)
|
||||
BindForms(vm)
|
||||
|
||||
testBindsCount(vm, "this", 4, t)
|
||||
}
|
||||
|
||||
func TestApisBindsCount(t *testing.T) {
|
||||
func TestBindApisCount(t *testing.T) {
|
||||
vm := goja.New()
|
||||
apisBinds(vm)
|
||||
BindApis(vm)
|
||||
|
||||
testBindsCount(vm, "this", 8, t)
|
||||
testBindsCount(vm, "$apis", 11, t)
|
||||
}
|
||||
|
||||
func TestApisBindsApiError(t *testing.T) {
|
||||
func TestBindApisErrors(t *testing.T) {
|
||||
vm := goja.New()
|
||||
apisBinds(vm)
|
||||
BindApis(vm)
|
||||
|
||||
scenarios := []struct {
|
||||
js string
|
||||
@@ -1190,8 +1190,8 @@ func TestLoadingDynamicModel(t *testing.T) {
|
||||
defer app.Cleanup()
|
||||
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
dbxBinds(vm)
|
||||
BindCore(vm)
|
||||
BindDbx(vm)
|
||||
vm.Set("$app", app)
|
||||
|
||||
_, err := vm.RunString(`
|
||||
@@ -1291,8 +1291,8 @@ func TestDynamicModelMapFieldCaching(t *testing.T) {
|
||||
defer app.Cleanup()
|
||||
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
dbxBinds(vm)
|
||||
BindCore(vm)
|
||||
BindDbx(vm)
|
||||
vm.Set("$app", app)
|
||||
|
||||
_, err := vm.RunString(`
|
||||
@@ -1350,8 +1350,8 @@ func TestLoadingArrayOf(t *testing.T) {
|
||||
defer app.Cleanup()
|
||||
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
dbxBinds(vm)
|
||||
BindCore(vm)
|
||||
BindDbx(vm)
|
||||
vm.Set("$app", app)
|
||||
|
||||
_, err := vm.RunString(`
|
||||
@@ -1391,18 +1391,18 @@ func TestLoadingArrayOf(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHttpClientBindsCount(t *testing.T) {
|
||||
func TestBindHttpClientCount(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
vm := goja.New()
|
||||
httpClientBinds(vm)
|
||||
BindHttpClient(vm)
|
||||
|
||||
testBindsCount(vm, "this", 2, t) // + FormData
|
||||
testBindsCount(vm, "$http", 1, t)
|
||||
}
|
||||
|
||||
func TestHttpClientBindsSend(t *testing.T) {
|
||||
func TestBindHttpClientSend(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// start a test server
|
||||
@@ -1447,8 +1447,8 @@ func TestHttpClientBindsSend(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
httpClientBinds(vm)
|
||||
BindCore(vm)
|
||||
BindHttpClient(vm)
|
||||
vm.Set("testURL", server.URL)
|
||||
|
||||
_, err := vm.RunString(`
|
||||
@@ -1626,7 +1626,7 @@ func TestHooksBinds(t *testing.T) {
|
||||
|
||||
vmFactory := func() *goja.Runtime {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
vm.Set("$app", app)
|
||||
vm.Set("result", result)
|
||||
return vm
|
||||
@@ -1712,7 +1712,7 @@ func TestHooksExceptionUnwrapping(t *testing.T) {
|
||||
|
||||
vmFactory := func() *goja.Runtime {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
BindCore(vm)
|
||||
vm.Set("$app", app)
|
||||
vm.Set("goErr", goErr)
|
||||
return vm
|
||||
@@ -1766,8 +1766,8 @@ func TestRouterBinds(t *testing.T) {
|
||||
|
||||
vmFactory := func() *goja.Runtime {
|
||||
vm := goja.New()
|
||||
baseBinds(vm)
|
||||
apisBinds(vm)
|
||||
BindCore(vm)
|
||||
BindApis(vm)
|
||||
vm.Set("$app", app)
|
||||
vm.Set("result", result)
|
||||
return vm
|
||||
@@ -1855,16 +1855,16 @@ func TestRouterBinds(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilepathBindsCount(t *testing.T) {
|
||||
func TestBindFilepathCount(t *testing.T) {
|
||||
vm := goja.New()
|
||||
filepathBinds(vm)
|
||||
BindFilepath(vm)
|
||||
|
||||
testBindsCount(vm, "$filepath", 15, t)
|
||||
}
|
||||
|
||||
func TestOsBindsCount(t *testing.T) {
|
||||
func TestBindOsCount(t *testing.T) {
|
||||
vm := goja.New()
|
||||
osBinds(vm)
|
||||
BindOs(vm)
|
||||
|
||||
testBindsCount(vm, "$os", 20, t)
|
||||
}
|
||||
|
||||
+5386
-5230
File diff suppressed because it is too large
Load Diff
+22
-19
@@ -1,6 +1,9 @@
|
||||
// Package jsvm implements pluggable utilities for binding a JS goja runtime
|
||||
// to the PocketBase instance (loading migrations, attaching to app hooks, etc.).
|
||||
//
|
||||
// The package also exports several reusable bindings so that users
|
||||
// can utilize them as part of their own custom goja runtime setup.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// jsvm.MustRegister(app, jsvm.Config{
|
||||
@@ -200,15 +203,15 @@ func (p *plugin) registerMigrations() error {
|
||||
process.Enable(vm)
|
||||
buffer.Enable(vm)
|
||||
|
||||
baseBinds(vm)
|
||||
dbxBinds(vm)
|
||||
securityBinds(vm)
|
||||
osBinds(vm)
|
||||
filepathBinds(vm)
|
||||
httpClientBinds(vm)
|
||||
filesystemBinds(vm)
|
||||
formsBinds(vm)
|
||||
mailsBinds(vm)
|
||||
BindCore(vm)
|
||||
BindDbx(vm)
|
||||
BindSecurity(vm)
|
||||
BindOs(vm)
|
||||
BindFilepath(vm)
|
||||
BindHttpClient(vm)
|
||||
BindFilesystem(vm)
|
||||
BindForms(vm)
|
||||
BindMails(vm)
|
||||
|
||||
vm.Set("$template", templateRegistry)
|
||||
vm.Set("__hooks", absHooksDir)
|
||||
@@ -288,16 +291,16 @@ func (p *plugin) registerHooks() error {
|
||||
process.Enable(vm)
|
||||
buffer.Enable(vm)
|
||||
|
||||
baseBinds(vm)
|
||||
dbxBinds(vm)
|
||||
filesystemBinds(vm)
|
||||
securityBinds(vm)
|
||||
osBinds(vm)
|
||||
filepathBinds(vm)
|
||||
httpClientBinds(vm)
|
||||
formsBinds(vm)
|
||||
apisBinds(vm)
|
||||
mailsBinds(vm)
|
||||
BindCore(vm)
|
||||
BindDbx(vm)
|
||||
BindSecurity(vm)
|
||||
BindOs(vm)
|
||||
BindFilepath(vm)
|
||||
BindHttpClient(vm)
|
||||
BindFilesystem(vm)
|
||||
BindForms(vm)
|
||||
BindMails(vm)
|
||||
BindApis(vm)
|
||||
|
||||
vm.Set("$app", p.app)
|
||||
vm.Set("$template", templateRegistry)
|
||||
|
||||
@@ -50,6 +50,7 @@ migrate((app) => {
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "[a-z0-9]{15}",
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "text@TEST_RANDOM",
|
||||
"max": 15,
|
||||
@@ -64,6 +65,7 @@ migrate((app) => {
|
||||
},
|
||||
{
|
||||
"cost": 0,
|
||||
"help": "",
|
||||
"hidden": true,
|
||||
"id": "password@TEST_RANDOM",
|
||||
"max": 0,
|
||||
@@ -77,6 +79,7 @@ migrate((app) => {
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "[a-zA-Z0-9]{50}",
|
||||
"help": "",
|
||||
"hidden": true,
|
||||
"id": "text@TEST_RANDOM",
|
||||
"max": 60,
|
||||
@@ -91,6 +94,7 @@ migrate((app) => {
|
||||
},
|
||||
{
|
||||
"exceptDomains": null,
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "email@TEST_RANDOM",
|
||||
"name": "email",
|
||||
@@ -101,6 +105,7 @@ migrate((app) => {
|
||||
"type": "email"
|
||||
},
|
||||
{
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "bool@TEST_RANDOM",
|
||||
"name": "emailVisibility",
|
||||
@@ -110,6 +115,7 @@ migrate((app) => {
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "bool@TEST_RANDOM",
|
||||
"name": "verified",
|
||||
@@ -226,6 +232,7 @@ func init() {
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "[a-z0-9]{15}",
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "text@TEST_RANDOM",
|
||||
"max": 15,
|
||||
@@ -240,6 +247,7 @@ func init() {
|
||||
},
|
||||
{
|
||||
"cost": 0,
|
||||
"help": "",
|
||||
"hidden": true,
|
||||
"id": "password@TEST_RANDOM",
|
||||
"max": 0,
|
||||
@@ -253,6 +261,7 @@ func init() {
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "[a-zA-Z0-9]{50}",
|
||||
"help": "",
|
||||
"hidden": true,
|
||||
"id": "text@TEST_RANDOM",
|
||||
"max": 60,
|
||||
@@ -267,6 +276,7 @@ func init() {
|
||||
},
|
||||
{
|
||||
"exceptDomains": null,
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "email@TEST_RANDOM",
|
||||
"name": "email",
|
||||
@@ -277,6 +287,7 @@ func init() {
|
||||
"type": "email"
|
||||
},
|
||||
{
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "bool@TEST_RANDOM",
|
||||
"name": "emailVisibility",
|
||||
@@ -286,6 +297,7 @@ func init() {
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "bool@TEST_RANDOM",
|
||||
"name": "verified",
|
||||
@@ -491,6 +503,7 @@ migrate((app) => {
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "[a-z0-9]{15}",
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "text@TEST_RANDOM",
|
||||
"max": 15,
|
||||
@@ -505,6 +518,7 @@ migrate((app) => {
|
||||
},
|
||||
{
|
||||
"cost": 0,
|
||||
"help": "",
|
||||
"hidden": true,
|
||||
"id": "password@TEST_RANDOM",
|
||||
"max": 0,
|
||||
@@ -518,6 +532,7 @@ migrate((app) => {
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "[a-zA-Z0-9]{50}",
|
||||
"help": "",
|
||||
"hidden": true,
|
||||
"id": "text@TEST_RANDOM",
|
||||
"max": 60,
|
||||
@@ -532,6 +547,7 @@ migrate((app) => {
|
||||
},
|
||||
{
|
||||
"exceptDomains": null,
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "email3885137012",
|
||||
"name": "email",
|
||||
@@ -542,6 +558,7 @@ migrate((app) => {
|
||||
"type": "email"
|
||||
},
|
||||
{
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "bool@TEST_RANDOM",
|
||||
"name": "emailVisibility",
|
||||
@@ -551,6 +568,7 @@ migrate((app) => {
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "bool256245529",
|
||||
"name": "verified",
|
||||
@@ -670,6 +688,7 @@ func init() {
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "[a-z0-9]{15}",
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "text@TEST_RANDOM",
|
||||
"max": 15,
|
||||
@@ -684,6 +703,7 @@ func init() {
|
||||
},
|
||||
{
|
||||
"cost": 0,
|
||||
"help": "",
|
||||
"hidden": true,
|
||||
"id": "password@TEST_RANDOM",
|
||||
"max": 0,
|
||||
@@ -697,6 +717,7 @@ func init() {
|
||||
},
|
||||
{
|
||||
"autogeneratePattern": "[a-zA-Z0-9]{50}",
|
||||
"help": "",
|
||||
"hidden": true,
|
||||
"id": "text@TEST_RANDOM",
|
||||
"max": 60,
|
||||
@@ -711,6 +732,7 @@ func init() {
|
||||
},
|
||||
{
|
||||
"exceptDomains": null,
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "email3885137012",
|
||||
"name": "email",
|
||||
@@ -721,6 +743,7 @@ func init() {
|
||||
"type": "email"
|
||||
},
|
||||
{
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "bool@TEST_RANDOM",
|
||||
"name": "emailVisibility",
|
||||
@@ -730,6 +753,7 @@ func init() {
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "bool256245529",
|
||||
"name": "verified",
|
||||
@@ -923,6 +947,7 @@ migrate((app) => {
|
||||
// add field
|
||||
collection.fields.addAt(8, new Field({
|
||||
"autogeneratePattern": "",
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "f4_id",
|
||||
"max": 0,
|
||||
@@ -938,6 +963,7 @@ migrate((app) => {
|
||||
|
||||
// update field
|
||||
collection.fields.addAt(7, new Field({
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "f2_id",
|
||||
"max": null,
|
||||
@@ -976,6 +1002,7 @@ migrate((app) => {
|
||||
|
||||
// add field
|
||||
collection.fields.addAt(8, new Field({
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "f3_id",
|
||||
"name": "f3_name",
|
||||
@@ -990,6 +1017,7 @@ migrate((app) => {
|
||||
|
||||
// update field
|
||||
collection.fields.addAt(7, new Field({
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "f2_id",
|
||||
"max": null,
|
||||
@@ -1054,6 +1082,7 @@ func init() {
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(8, []byte(` + "`" + `{
|
||||
"autogeneratePattern": "",
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "f4_id",
|
||||
"max": 0,
|
||||
@@ -1071,6 +1100,7 @@ func init() {
|
||||
|
||||
// update field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(7, []byte(` + "`" + `{
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "f2_id",
|
||||
"max": null,
|
||||
@@ -1116,6 +1146,7 @@ func init() {
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(8, []byte(` + "`" + `{
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "f3_id",
|
||||
"name": "f3_name",
|
||||
@@ -1132,6 +1163,7 @@ func init() {
|
||||
|
||||
// update field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(7, []byte(` + "`" + `{
|
||||
"help": "",
|
||||
"hidden": false,
|
||||
"id": "f2_id",
|
||||
"max": null,
|
||||
|
||||
@@ -36,6 +36,8 @@ func NewAppleProvider() *Apple {
|
||||
return &Apple{
|
||||
BaseProvider: BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 1,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="256" height="315" preserveAspectRatio="xMidYMid"><path d="M213.8 167c.4 47.6 41.7 63.4 42.2 63.6-.3 1.2-6.6 22.6-21.8 44.8-13 19.1-26.7 38.2-48 38.6-21.1.4-28-12.5-52-12.5s-31.6 12.1-51.5 12.9c-20.7.8-36.4-20.7-49.6-39.8-27-39-47.7-110.3-20-158.4a77 77 0 0 1 65.1-39.4c20.3-.4 39.5 13.6 51.9 13.6s35.7-16.9 60.2-14.4c10.2.4 39 4.2 57.5 31.2-1.5 1-34.4 20-34 59.8M174.2 50.2A69 69 0 0 0 190.6 0c-15.8.6-35 10.5-46.3 23.8-10.2 11.8-19.1 30.6-16.7 48.7 17.6 1.3 35.7-9 46.6-22.3"/></svg>`,
|
||||
displayName: "Apple",
|
||||
pkce: true,
|
||||
scopes: []string{"name", "email"},
|
||||
|
||||
+20
-1
@@ -28,8 +28,20 @@ func NewProviderByName(name string) (Provider, error) {
|
||||
return factory(), nil
|
||||
}
|
||||
|
||||
// @todo refactor and consider replace with a plain struct
|
||||
//
|
||||
// Provider defines a common interface for an OAuth2 client.
|
||||
type Provider interface {
|
||||
// @todo temp backport
|
||||
//
|
||||
// Order returns the sorting order of the provider usually used in the auth methods list response.
|
||||
Logo() string
|
||||
|
||||
// @todo temp backport
|
||||
//
|
||||
// Order returns the sorting order of the provider usually used in the auth methods list response.
|
||||
Order() int
|
||||
|
||||
// Context returns the context associated with the provider (if any).
|
||||
Context() context.Context
|
||||
|
||||
@@ -133,11 +145,18 @@ type AuthUser struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
AvatarURL string `json:"avatarURL"`
|
||||
AccessToken string `json:"accessToken"`
|
||||
RefreshToken string `json:"refreshToken"`
|
||||
|
||||
// @todo consider assigning the non-verified email and combining
|
||||
// with an extra Verified bool flag.
|
||||
|
||||
// The VERIFIED OAuth2 account email.
|
||||
//
|
||||
// It must be empty if the provider is not able to verify the email ownership.
|
||||
Email string `json:"email"`
|
||||
|
||||
// @todo
|
||||
// deprecated: use AvatarURL instead
|
||||
// AvatarUrl will be removed after dropping v0.22 support
|
||||
|
||||
@@ -13,16 +13,28 @@ import (
|
||||
// BaseProvider defines common fields and methods used by OAuth2 client providers.
|
||||
type BaseProvider struct {
|
||||
ctx context.Context
|
||||
extra map[string]any
|
||||
redirectURL string
|
||||
clientId string
|
||||
clientSecret string
|
||||
displayName string
|
||||
redirectURL string
|
||||
logo string
|
||||
authURL string
|
||||
tokenURL string
|
||||
userInfoURL string
|
||||
scopes []string
|
||||
order int
|
||||
pkce bool
|
||||
extra map[string]any
|
||||
}
|
||||
|
||||
// Order implements Provider.Order() interface method.
|
||||
func (p *BaseProvider) Order() int {
|
||||
return p.order
|
||||
}
|
||||
|
||||
// Logo implements Provider.Logo() interface method.
|
||||
func (p *BaseProvider) Logo() string {
|
||||
return p.logo
|
||||
}
|
||||
|
||||
// Context implements Provider.Context() interface method.
|
||||
|
||||
@@ -30,14 +30,46 @@ func TestDisplayName(t *testing.T) {
|
||||
|
||||
before := b.DisplayName()
|
||||
if before != "" {
|
||||
t.Fatalf("Expected displayName to be empty, got %v", before)
|
||||
t.Fatalf("Expected displayName to be empty, got %q", before)
|
||||
}
|
||||
|
||||
b.SetDisplayName("test")
|
||||
|
||||
after := b.DisplayName()
|
||||
if after != "test" {
|
||||
t.Fatalf("Expected displayName to be 'test', got %v", after)
|
||||
t.Fatalf("Expected displayName to be %q, got %q", "test", after)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrder(t *testing.T) {
|
||||
b := BaseProvider{}
|
||||
|
||||
before := b.Order()
|
||||
if before != 0 {
|
||||
t.Fatalf("Expected order to be empty, got %d", before)
|
||||
}
|
||||
|
||||
b.order = 123
|
||||
|
||||
after := b.Order()
|
||||
if after != 123 {
|
||||
t.Fatalf("Expected order to be %d, got %d", 123, after)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogo(t *testing.T) {
|
||||
b := BaseProvider{}
|
||||
|
||||
before := b.Logo()
|
||||
if before != "" {
|
||||
t.Fatalf("Expected logo to be empty, got %q", before)
|
||||
}
|
||||
|
||||
b.logo = "test"
|
||||
|
||||
after := b.Logo()
|
||||
if after != "test" {
|
||||
t.Fatalf("Expected logo to be %q, got %q", "test", after)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,14 +110,14 @@ func TestClientId(t *testing.T) {
|
||||
|
||||
before := b.ClientId()
|
||||
if before != "" {
|
||||
t.Fatalf("Expected clientId to be empty, got %v", before)
|
||||
t.Fatalf("Expected clientId to be empty, got %q", before)
|
||||
}
|
||||
|
||||
b.SetClientId("test")
|
||||
|
||||
after := b.ClientId()
|
||||
if after != "test" {
|
||||
t.Fatalf("Expected clientId to be 'test', got %v", after)
|
||||
t.Fatalf("Expected clientId to be %q, got %q", "test", after)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,14 +126,14 @@ func TestClientSecret(t *testing.T) {
|
||||
|
||||
before := b.ClientSecret()
|
||||
if before != "" {
|
||||
t.Fatalf("Expected clientSecret to be empty, got %v", before)
|
||||
t.Fatalf("Expected clientSecret to be empty, got %q", before)
|
||||
}
|
||||
|
||||
b.SetClientSecret("test")
|
||||
|
||||
after := b.ClientSecret()
|
||||
if after != "test" {
|
||||
t.Fatalf("Expected clientSecret to be 'test', got %v", after)
|
||||
t.Fatalf("Expected clientSecret to be %q, got %q", "test", after)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,14 +142,14 @@ func TestRedirectURL(t *testing.T) {
|
||||
|
||||
before := b.RedirectURL()
|
||||
if before != "" {
|
||||
t.Fatalf("Expected RedirectURL to be empty, got %v", before)
|
||||
t.Fatalf("Expected RedirectURL to be empty, got %q", before)
|
||||
}
|
||||
|
||||
b.SetRedirectURL("test")
|
||||
|
||||
after := b.RedirectURL()
|
||||
if after != "test" {
|
||||
t.Fatalf("Expected RedirectURL to be 'test', got %v", after)
|
||||
t.Fatalf("Expected RedirectURL to be %q, got %q", "test", after)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,14 +158,14 @@ func TestAuthURL(t *testing.T) {
|
||||
|
||||
before := b.AuthURL()
|
||||
if before != "" {
|
||||
t.Fatalf("Expected authURL to be empty, got %v", before)
|
||||
t.Fatalf("Expected authURL to be empty, got %q", before)
|
||||
}
|
||||
|
||||
b.SetAuthURL("test")
|
||||
|
||||
after := b.AuthURL()
|
||||
if after != "test" {
|
||||
t.Fatalf("Expected authURL to be 'test', got %v", after)
|
||||
t.Fatalf("Expected authURL to be %q, got %q", "test", after)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,14 +174,14 @@ func TestTokenURL(t *testing.T) {
|
||||
|
||||
before := b.TokenURL()
|
||||
if before != "" {
|
||||
t.Fatalf("Expected tokenURL to be empty, got %v", before)
|
||||
t.Fatalf("Expected tokenURL to be empty, got %q", before)
|
||||
}
|
||||
|
||||
b.SetTokenURL("test")
|
||||
|
||||
after := b.TokenURL()
|
||||
if after != "test" {
|
||||
t.Fatalf("Expected tokenURL to be 'test', got %v", after)
|
||||
t.Fatalf("Expected tokenURL to be %q, got %q", "test", after)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,14 +190,14 @@ func TestUserInfoURL(t *testing.T) {
|
||||
|
||||
before := b.UserInfoURL()
|
||||
if before != "" {
|
||||
t.Fatalf("Expected userInfoURL to be empty, got %v", before)
|
||||
t.Fatalf("Expected userInfoURL to be empty, got %q", before)
|
||||
}
|
||||
|
||||
b.SetUserInfoURL("test")
|
||||
|
||||
after := b.UserInfoURL()
|
||||
if after != "test" {
|
||||
t.Fatalf("Expected userInfoURL to be 'test', got %v", after)
|
||||
t.Fatalf("Expected userInfoURL to be %q, got %q", "test", after)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ type Bitbucket struct {
|
||||
func NewBitbucketProvider() *Bitbucket {
|
||||
return &Bitbucket{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 9,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="2500" height="2256" preserveAspectRatio="xMidYMid" viewBox="-1 -0.6 257.9 230.8"><linearGradient id="a" x1="108.6%" x2="46.9%" y1="13.8%" y2="78.8%"><stop offset=".2" stop-color="#0052cc"/><stop offset="1" stop-color="#2684ff"/></linearGradient><g fill="none"><path d="M101 153h54l13-76H87z"/><path fill="#2684ff" d="M8 0a8 8 0 0 0-8 10l35 211a11 11 0 0 0 11 9h167a8 8 0 0 0 8-7l35-213a8 8 0 0 0-8-10zm147 153h-53L87 77h81z"/><path fill="url(#a)" d="M245 77h-77l-13 76h-53l-63 74 7 3h167a8 8 0 0 0 8-7z"/></g></svg>`,
|
||||
displayName: "Bitbucket",
|
||||
pkce: false,
|
||||
scopes: []string{"account"},
|
||||
|
||||
@@ -27,6 +27,8 @@ type Box struct {
|
||||
func NewBoxProvider() *Box {
|
||||
return &Box{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 20,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 21.6"><path fill="#0061d5" d="M39.7 19.2q.7 1.2-.2 2.1-1.2.7-2.2-.2l-3.5-4.5-3.4 4.4c-.5.7-1.5.7-2.2.2q-1-.9-.3-2.1l4-5.2-4-5.2c-.5-.7-.3-1.7.3-2.2s1.7-.3 2.2.3l3.4 4.5L37.3 7q.9-1 2.2-.3 1 1 .2 2.2L35.8 14zm-18.2-.6c-2.6 0-4.7-2-4.7-4.6s2.1-4.6 4.7-4.6 4.7 2.1 4.7 4.6a4.7 4.7 0 0 1-4.7 4.6m-13.8 0c-2.6 0-4.7-2-4.7-4.6s2.1-4.6 4.7-4.6 4.7 2.1 4.7 4.6c0 2.6-2.1 4.6-4.7 4.6M21.5 6.4a8 8 0 0 0-6.8 4 8 8 0 0 0-6.9-4q-2.7 0-4.7 1.5V1.5Q3 .2 1.6 0 .1.1 0 1.5v12.6a7.7 7.7 0 0 0 7.7 7.5c3 0 5.6-1.7 6.9-4.1a8 8 0 0 0 6.8 4.1c4.3 0 7.8-3.4 7.8-7.7a7.5 7.5 0 0 0-7.7-7.5"/></svg>`,
|
||||
displayName: "Box",
|
||||
pkce: true,
|
||||
scopes: []string{"root_readonly"},
|
||||
|
||||
@@ -29,6 +29,8 @@ func NewDiscordProvider() *Discord {
|
||||
// https://discord.com/developers/docs/resources/user#get-current-user
|
||||
return &Discord{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 12,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="256" height="199" preserveAspectRatio="xMidYMid"><path fill="#5865f2" d="M216.9 16.6A209 209 0 0 0 164 0c-2.2 4.1-4.9 9.6-6.7 14a194 194 0 0 0-58.6 0C97 9.6 94.2 4.1 92 0a208 208 0 0 0-53 16.6A222 222 0 0 0 1 165a211 211 0 0 0 65 33 161 161 0 0 0 13.8-22.8q-11.5-4.4-21.8-10.6l5.3-4.3a149 149 0 0 0 129.6 0q2.6 2.3 5.3 4.3a136 136 0 0 1-21.9 10.6q6 12 13.9 22.9a211 211 0 0 0 64.8-33.2c5.3-56.3-9-105.1-38-148.4M85.5 135.1c-12.7 0-23-11.8-23-26.2s10.1-26.2 23-26.2 23.2 11.8 23 26.2c0 14.4-10.2 26.2-23 26.2m85 0c-12.6 0-23-11.8-23-26.2s10.2-26.2 23-26.2 23.3 11.8 23 26.2c0 14.4-10.1 26.2-23 26.2"/></svg>`,
|
||||
displayName: "Discord",
|
||||
pkce: true,
|
||||
scopes: []string{"identify", "email"},
|
||||
|
||||
@@ -27,6 +27,8 @@ type Facebook struct {
|
||||
func NewFacebookProvider() *Facebook {
|
||||
return &Facebook{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 5,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" preserveAspectRatio="xMidYMid"><path fill="#1877f2" d="M256 128a128 128 0 1 0-148 126.4V165H75.5v-37H108V99.8c0-32 19.1-49.8 48.3-49.8 14 0 28.7 2.5 28.7 2.5V84h-16.1c-16 0-20.9 9.9-20.9 20v24h35.5l-5.7 37H148v89.4A128 128 0 0 0 256 128"/><path fill="#fff" d="m177.8 165 5.7-37H148v-24c0-10.1 5-20 20.9-20H185V52.5S170.4 50 156.3 50C127.1 50 108 67.7 108 99.8V128H75.5v37H108v89.4a129 129 0 0 0 40 0V165z"/></svg>`,
|
||||
displayName: "Facebook",
|
||||
pkce: true,
|
||||
scopes: []string{"email"},
|
||||
|
||||
@@ -27,6 +27,8 @@ type Gitea struct {
|
||||
func NewGiteaProvider() *Gitea {
|
||||
return &Gitea{BaseProvider{
|
||||
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",
|
||||
pkce: true,
|
||||
scopes: []string{"read:user", "user:email"},
|
||||
|
||||
@@ -29,6 +29,8 @@ type Gitee struct {
|
||||
func NewGiteeProvider() *Gitee {
|
||||
return &Gitee{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 10,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="120 13 72 72"><g fill="none" fill-rule="evenodd"><path d="M0 0h312v100H0z"/><path fill="#c71d23" d="M156 85a36 36 0 1 1 0-72 36 36 0 0 1 0 72m18.2-40h-20.4q-1.7.1-1.8 1.8v4.4q.2 1.6 1.8 1.8h12.4q1.7.1 1.8 1.8v.9c0 3-2.4 5.3-5.3 5.3h-17q-1.6-.1-1.7-1.8V42.3c0-3 2.4-5.3 5.3-5.3h25q1.5-.1 1.7-1.8v-4.4a2 2 0 0 0-1.8-1.8h-24.9C142 29 136 35 136 42.3v25q.2 1.5 1.8 1.7H164a12 12 0 0 0 12-12V46.8q-.2-1.6-1.8-1.8"/></g></svg>`,
|
||||
displayName: "Gitee",
|
||||
pkce: true,
|
||||
scopes: []string{"user_info", "emails"},
|
||||
|
||||
@@ -29,6 +29,8 @@ type Github struct {
|
||||
func NewGithubProvider() *Github {
|
||||
return &Github{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 7,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="256" height="250" preserveAspectRatio="xMidYMid"><path fill="#161614" d="M128 0a128 128 0 0 0-40.5 249.5c6.4 1.1 8.8-2.8 8.8-6.2l-.2-23.8C60.5 227.2 53 204.4 53 204.4c-5.8-14.8-14.2-18.8-14.2-18.8-11.6-7.9.8-7.7.8-7.7 12.9.9 19.7 13.1 19.7 13.1 11.4 19.6 30 14 37.2 10.7 1.2-8.3 4.5-14 8.1-17.1-28.4-3.3-58.3-14.2-58.3-63.3 0-14 5-25.4 13.2-34.3a46 46 0 0 1 1.3-34S71.5 49.7 96 66.3a123 123 0 0 1 64 0c24.5-16.6 35.2-13.1 35.2-13.1a46 46 0 0 1 1.3 33.9c8.2 9 13.2 20.3 13.2 34.3 0 49.2-30 60-58.5 63.2 4.6 4 8.7 11.7 8.7 23.7l-.2 35.1c0 3.4 2.4 7.4 8.8 6.1A128 128 0 0 0 128 0M48 182.3q-.6 1.1-2.3.4c-.9-.4-1.4-1.3-1.1-1.9q.6-1 2.2-.4 1.6.8 1.1 2m6.2 5.7c-.6.5-1.8.3-2.6-.6q-1.2-1.7-.4-2.7 1.2-.8 2.7.6 1.3 1.5.3 2.7m4.4 7.1c-.8.6-2.1 0-2.9-1-.8-1.2-.8-2.6 0-3.1q1.4-.7 2.9 1c.8 1.2.8 2.6 0 3.1m7.3 8.4c-.7.7-2.2.5-3.3-.5s-1.5-2.5-.8-3.3c.8-.8 2.3-.5 3.4.5 1 1 1.4 2.5.7 3.3m9.4 2.8c-.3 1-1.7 1.4-3.2 1-1.4-.4-2.4-1.6-2.1-2.6s1.7-1.5 3.2-1 2.4 1.6 2.1 2.6m10.7 1.2q-.1 1.7-2.7 2-2.5-.2-2.8-2c0-1 1.2-1.9 2.8-1.9s2.7.8 2.7 1.9m10.6-.4c.2 1-.9 2-2.4 2.3s-2.8-.3-3-1.3c-.2-1.1.9-2.2 2.3-2.4 1.6-.3 3 .3 3.1 1.4"/></svg>`,
|
||||
displayName: "GitHub",
|
||||
pkce: true, // technically is not supported yet but it is safe as the PKCE params are just ignored
|
||||
scopes: []string{"read:user", "user:email"},
|
||||
|
||||
@@ -27,6 +27,8 @@ type Gitlab struct {
|
||||
func NewGitlabProvider() *Gitlab {
|
||||
return &Gitlab{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 8,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="256" height="247" fill="none"><path fill="#e24329" d="m251.7 97.7-.3-.9-34.7-90.6a9 9 0 0 0-9-5.7q-3 .2-5.2 2a9 9 0 0 0-3.1 4.7L176 78.9H81L57.7 7.2a9.1 9.1 0 0 0-17.2-1L5.6 96.8l-.4.9a64.4 64.4 0 0 0 21.4 74.5h.1l.3.3L80 212l26 19.8 16 12a11 11 0 0 0 13 0l15.9-12 26.2-19.8 53.1-39.9h.2a64.5 64.5 0 0 0 21.3-74.5"/><path fill="#fc6d26" d="m251.7 97.7-.3-.9c-17 3.5-32.9 10.6-46.7 21l-76.2 57.6 48.5 36.7 53.2-39.8.2-.1a64.5 64.5 0 0 0 21.3-74.5"/><path fill="#fca326" d="m80 212.1 26 19.8 16 12a11 11 0 0 0 13 0l15.9-12 26.2-19.8s-22.7-17-48.6-36.7z"/><path fill="#fc6d26" d="M52.2 117.8a117 117 0 0 0-46.6-21l-.4.9a64.4 64.4 0 0 0 21.4 74.5h.1l.3.3L80 212l48.5-36.7z"/></svg>`,
|
||||
displayName: "GitLab",
|
||||
pkce: true,
|
||||
scopes: []string{"read_user"},
|
||||
|
||||
@@ -26,6 +26,8 @@ type Google struct {
|
||||
func NewGoogleProvider() *Google {
|
||||
return &Google{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 2,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="256" height="262" preserveAspectRatio="xMidYMid"><path fill="#4285f4" d="M255.9 133.5c0-10.8-.9-18.6-2.8-26.7H130.6v48.4h71.9a64 64 0 0 1-26.7 42.4l-.2 1.6 38.7 30 2.7.3c24.7-22.8 38.9-56.3 38.9-96"/><path fill="#34a853" d="M130.6 261.1c35.2 0 64.8-11.6 86.4-31.6l-41.2-32a76 76 0 0 1-45.2 13.1 79 79 0 0 1-74.3-54.2l-1.5.1-40.3 31.2-.6 1.5A131 131 0 0 0 130.6 261"/><path fill="#fbbc05" d="M56.3 156.4a80 80 0 0 1-.2-51.7V103L15.3 71.3l-1.4.6a131 131 0 0 0 0 117.3z"/><path fill="#eb4335" d="M130.6 50.5c24.5 0 41 10.6 50.4 19.4L218 34c-22.8-21-52.2-34-87.4-34C79.5 0 35.4 29.3 13.9 72l42.2 32.7a79 79 0 0 1 74.5-54.2"/></svg>`,
|
||||
displayName: "Google",
|
||||
pkce: true,
|
||||
scopes: []string{
|
||||
|
||||
@@ -26,6 +26,8 @@ type Instagram struct {
|
||||
func NewInstagramProvider() *Instagram {
|
||||
return &Instagram{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 6,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 132 132"><defs><radialGradient xlink:href="#a" id="c" cx="158.4" cy="578.1" r="65" fx="158.4" fy="578.1" gradientTransform="matrix(0 -1.98198 1.8439 0 -1031.4 454)" gradientUnits="userSpaceOnUse"/><radialGradient xlink:href="#b" id="d" cx="147.7" cy="473.5" r="65" fx="147.7" fy="473.5" gradientTransform="rotate(78.7 1103.9 776.3)scale(.88596 3.6529)" gradientUnits="userSpaceOnUse"/><linearGradient id="b"><stop offset="0" stop-color="#3771c8"/><stop offset=".1" stop-color="#3771c8"/><stop offset="1" stop-color="#60f" stop-opacity="0"/></linearGradient><linearGradient id="a"><stop offset="0" stop-color="#fd5"/><stop offset=".1" stop-color="#fd5"/><stop offset=".5" stop-color="#ff543e"/><stop offset="1" stop-color="#c837ab"/></linearGradient></defs><path fill="url(#c)" d="M65 0C38 0 30 0 28.4.2c-5.6.4-9 1.3-12.8 3.2a28 28 0 0 0-15 21.3c-.4 3-.6 3.6-.6 19.1V65c0 27 0 35 .2 36.6.4 5.4 1.3 8.8 3 12.5 3.5 7.2 10 12.5 17.8 14.5q4 1 9.5 1.3a2913 2913 0 0 0 68.8 0c4.4-.2 7-.6 9.8-1.3 7.8-2 14.2-7.3 17.7-14.5 1.8-3.7 2.7-7.2 3.1-12.3a2759 2759 0 0 0 0-73.6c-.4-5.2-1.3-8.8-3.1-12.5q-2.1-4.3-5.6-7.6A28 28 0 0 0 105.4.6c-3-.4-3.7-.6-19.2-.6z" transform="translate(1 1)"/><path fill="url(#d)" d="M65 0C38 0 30 0 28.4.2c-5.6.4-9 1.3-12.8 3.2a28 28 0 0 0-15 21.3c-.4 3-.6 3.6-.6 19.1V65c0 27 0 35 .2 36.6.4 5.4 1.3 8.8 3 12.5 3.5 7.2 10 12.5 17.8 14.5q4 1 9.5 1.3a2913 2913 0 0 0 68.8 0c4.4-.2 7-.6 9.8-1.3 7.8-2 14.2-7.3 17.7-14.5 1.8-3.7 2.7-7.2 3.1-12.3a2759 2759 0 0 0 0-73.6c-.4-5.2-1.3-8.8-3.1-12.5q-2.1-4.3-5.6-7.6A28 28 0 0 0 105.4.6c-3-.4-3.7-.6-19.2-.6z" transform="translate(1 1)"/><path fill="#fff" d="M66 18c-13 0-14.7 0-19.8.3s-8.6 1-11.6 2.2a24 24 0 0 0-8.5 5.6 24 24 0 0 0-5.6 8.5 35 35 0 0 0-2.2 11.6C18 51.3 18 53 18 66s0 14.7.3 19.8 1 8.6 2.2 11.6q1.7 4.7 5.6 8.5 3.8 4 8.5 5.6c3 1.2 6.5 2 11.6 2.2s6.8.3 19.8.3 14.7 0 19.8-.3 8.6-1 11.6-2.2q4.7-1.7 8.5-5.6t5.6-8.5c1.2-3 2-6.5 2.2-11.6s.3-6.8.3-19.8 0-14.7-.3-19.8-1-8.6-2.2-11.6a24 24 0 0 0-5.6-8.5 24 24 0 0 0-8.5-5.6c-3-1.2-6.5-2-11.6-2.2S79 18 66 18m-4.3 8.7H66c12.8 0 14.3 0 19.4.2 4.7.2 7.2 1 9 1.7 2.2.8 3.7 1.9 5.4 3.6q2.4 2.2 3.6 5.5c.7 1.7 1.5 4.2 1.7 8.9.2 5 .3 6.6.3 19.4s-.1 14.3-.3 19.4c-.2 4.7-1 7.2-1.7 8.9q-1.1 3.1-3.6 5.5a15 15 0 0 1-5.5 3.6c-1.7.7-4.2 1.4-8.9 1.6-5 .3-6.6.3-19.4.3a334 334 0 0 1-19.4-.3 28 28 0 0 1-8.9-1.6q-3.1-1.3-5.5-3.6a15 15 0 0 1-3.6-5.5 25 25 0 0 1-1.7-9c-.2-5-.2-6.5-.2-19.3s0-14.4.2-19.4a26 26 0 0 1 1.7-9q1.2-3.1 3.6-5.4 2.4-2.5 5.5-3.6a25 25 0 0 1 9-1.7c4.3-.2 6-.3 15-.3zm30 8a5.8 5.8 0 1 0 0 11.4 5.8 5.8 0 0 0 0-11.5M66 41.2a24.7 24.7 0 1 0 0 49.4 24.7 24.7 0 0 0 0-49.3m0 8.7a16 16 0 1 1 0 32 16 16 0 0 1 0-32"/></svg>`,
|
||||
displayName: "Instagram",
|
||||
pkce: true,
|
||||
scopes: []string{"instagram_business_basic"},
|
||||
|
||||
@@ -28,6 +28,8 @@ type Kakao struct {
|
||||
func NewKakaoProvider() *Kakao {
|
||||
return &Kakao{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 14,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="2500" height="2500" viewBox="0 0 256 256"><path fill="#ffe812" d="M256 236q-2 18-20 20H20q-18-2-20-20V20Q2 2 20 0h216q18 2 20 20z"/><path d="M128 36C71 36 24 73 24 118c0 29 19 55 49 69l-11 38 1 3h3l44-29 18 1c57 0 104-37 104-82s-47-82-104-82"/><path fill="#ffe812" d="M71 147q-6-1-6-6v-36H55a6 6 0 0 1 0-11h31a6 6 0 0 1 0 11h-9v36q-1 5-6 6m52 0q-3 0-5-3l-3-8H97l-3 8q-2 3-5 3l-4-1q-3-1-1-9l14-38q2-4 8-5 6 1 8 5l14 38q2 8-1 9zm-11-22-6-17-6 17zm26 21q-5-1-6-6v-40q1-6 6-6 7 0 7 6v35h12q5 0 6 5 0 7-6 6zm33 1q-5-1-6-6v-41a6 6 0 0 1 12 0v12l17-16 3-2 5 2 1 4-1 4-14 13 15 20 1 4-2 4-4 1-5-2-14-19-2 2v14a6 6 0 0 1-6 6"/></svg>`,
|
||||
displayName: "Kakao",
|
||||
pkce: true,
|
||||
scopes: []string{"account_email", "profile_nickname", "profile_image"},
|
||||
|
||||
@@ -26,6 +26,8 @@ type Lark struct {
|
||||
func NewLarkProvider() *Lark {
|
||||
return &Lark{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 19,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" fill="none"><path fill="#00d6b9" d="M137 126v-1l2-1v-1l3-2 3-3 3-3 2-3 3-2 2-3 4-3 2-2 4-3 9-7 5-3 9-3 2-1a135 135 0 0 0-26-51 12 12 0 0 0-9-4H56l-2 1 1 2a288 288 0 0 1 82 93"/><path fill="#3370ff" d="M98 212c51 0 95-28 118-69l3-5-6 9-4 4-6 5-2 2-4 3-11 4-6 2h-15l-4-1h-3l-2-1-5-1-3-1-4-1-3-1-3-1-1-1-3-1h-2l-3-2h-2l-2-1-3-1-2-1-2-1-2-1h-2l-1-1-2-1h-1l-1-1-2-1-2-1-2-1-2-1a285 285 0 0 1-81-60l-3 1v94a12 12 0 0 0 5 11 135 135 0 0 0 76 22"/><path fill="#133c9a" d="M248 90a78 78 0 0 0-58-5l-2 1-14 6-6 4-7 6-2 2-4 3-2 3-3 2-2 3-3 3-3 3-3 2v1l-2 1-1 1-1 1a137 137 0 0 1-28 20l2 1 1 1h2l1 1 1 1h2l2 1 2 1 2 1 3 1 2 1h2l3 2h2l3 1 2 1 2 1 3 1 4 1 8 2 2 1 7 1h15l6-2 7-2 6-4 2-1 2-2 3-1 7-8 6-9 1-2 12-24a77 77 0 0 1 16-22"/></svg>`,
|
||||
displayName: "Lark",
|
||||
pkce: true,
|
||||
// Lark has two domains with the same API: feishu.cn and larksuite.com.
|
||||
|
||||
@@ -31,6 +31,8 @@ type Linear struct {
|
||||
func NewLinearProvider() *Linear {
|
||||
return &Linear{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 16,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" fill="#222326" viewBox="0 0 100 100"><path d="M1.2 61.5c-.2-1 1-1.5 1.6-.8l36.5 36.5c.7.7.1 1.8-.8 1.6A50 50 0 0 1 1.2 61.5M0 47q0 .4.3.7l52 52.1q.4.3.8.3a50 50 0 0 0 7-1 1 1 0 0 0 .5-1.6l-58-58a1 1 0 0 0-1.7.5 50 50 0 0 0-.9 7m4.2-17.2q-.2.6.2 1.1l64.8 64.8q.5.5 1 .2l5.3-2.7q.8-.6.2-1.5L8.4 24.3a1 1 0 0 0-1.5.2Q5.4 27 4.2 29.7m8.5-11.6a1 1 0 0 1 0-1.4A50 50 0 0 1 100 50a50 50 0 0 1-16.7 37.4 1 1 0 0 1-1.4 0z"/></svg>`,
|
||||
displayName: "Linear",
|
||||
pkce: false, // Linear doesn't support PKCE at the moment and returns an error if enabled
|
||||
scopes: []string{"read"},
|
||||
|
||||
@@ -26,6 +26,8 @@ type Livechat struct {
|
||||
func NewLivechatProvider() *Livechat {
|
||||
return &Livechat{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 27,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 80 80"><path d="M79.5 49.7a19 19 0 0 1-19 17.3H50L30 80V67l20-13h10.5a6 6 0 0 0 6.1-5.3q1-15-.2-30a5.6 5.6 0 0 0-5.2-5.1Q50.9 13.1 40 13c-10.9-.1-14.4.2-21.2.7-2.8.2-5 2.3-5.2 5.1q-1 15-.2 30c.3 3.1 3 5.3 6.1 5.2H30v13H19.5c-9.9.1-18.2-7.4-19-17.3q-1-16 .2-32C1.5 8.5 8.8 1.3 18 .7a313 313 0 0 1 44.1.1c9.2.6 16.5 7.8 17.3 17q1.1 16 .1 31.9" style="fill:#ff5100"/></svg>`,
|
||||
displayName: "LiveChat",
|
||||
pkce: true,
|
||||
scopes: []string{}, // default scopes are specified from the provider dashboard
|
||||
|
||||
@@ -28,6 +28,8 @@ type Mailcow struct {
|
||||
func NewMailcowProvider() *Mailcow {
|
||||
return &Mailcow{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 28,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="394.8" height="376.9"><path d="M56 213.3c0-20.3-.8-17.3-3.7-39.3 2.2-22.5-7.6-38.9-7.7-58.4 0-4.7-5.8-6.7-4.7-11A99 99 0 0 0 56 214.7z" style="fill:#3d5263" transform="translate(10 10)"/><path d="m254.8 180.4-.6.5a121 121 0 0 1-80 84l11 9.2 53.2 44.8 31.6 26.5q1.3-3 1.3-6.4V167.1zM23 185.5l-6.5-5.2-16.5-13v171.6q0 3.5 1.2 6.5l58.4-48.7 27-22.4L99 263.9a121 121 0 0 1-76-78.4" style="fill:#f9e82d" transform="translate(10 10)"/><path d="M238.4 318.9 185.1 274l-11-9.2a120 120 0 0 1-75.1-1l-12.4 10.4-27 22.5-58.4 48.6A18 18 0 0 0 18 357h235.4a18 18 0 0 0 16.7-11.5z" style="fill:#edd514;fill-opacity:.89499996" transform="translate(10 10)"/><path d="M238.4 318.9c-41.4 4-115 5.5-178.8-22.1-21.5-9.4-42-22.1-59.6-39.2V339q0 3.5 1.2 6.5A18 18 0 0 0 18 357h235.4a18 18 0 0 0 18-18v-24.5s-12.6 2.5-32.9 4.5" style="opacity:.1;fill:#3d5263" transform="translate(10 10)"/><path d="M86.6 274.3a120 120 0 0 0 98.5-.2 120 120 0 0 0 69.7-93.7l-.6.5a120.5 120.5 0 0 1-155.2 83 121 121 0 0 1-76-78.4l-6.5-5.2c5.5 42 32.7 77.3 70 94" style="opacity:.1;fill:#3d5263" transform="translate(10 10)"/><path d="M54.3 63.9q-2.7 2.6-5.2 5.4l-.2.1-.7.8.2-.2a120.2 120.2 0 0 1 174.8 165 120 120 0 0 0 35-84.8A120 120 0 0 0 187 40.4a119 119 0 0 0-100.7 1.2 120 120 0 0 0-32 22.3" style="fill:#fff" transform="translate(10 10)"/><path d="M95.8 118.5a4.6 4.6 0 1 0 0-9.2 4.6 4.6 0 0 0 0 9.2m91.1 0a4.6 4.6 0 1 0 0-9.2 4.6 4.6 0 0 0 0 9.2" style="fill:#fff" transform="translate(10 10)"/><path d="M223.7 234.4A120.2 120.2 0 0 0 48.4 70 121 121 0 0 0 23 185.5 120.5 120.5 0 0 0 174.2 265a120 120 0 0 0 47.7-28.6zm-5.8-58.4q-3.2 11-8.7 20.5A94.8 94.8 0 0 1 56 214.8a99 99 0 0 1-16-110.3 96 96 0 0 1 97.8-54 97.3 97.3 0 0 1 84.3 97.2q0 14.9-4 28.3M49 69.3" style="fill:#f1f2f2" transform="translate(10 10)"/><path d="M257.6 161.9a120 120 0 0 1-3.4 19l.6-.5 16.5-13.2zM0 167.2l16.5 13.1 6.5 5.2q-3.5-11.6-4.7-23.9l-2.9.9zm87.5 25.1a11.3 11.3 0 1 0 0 22.5 11.3 11.3 0 0 0 0-22.5m93.8 0a11.3 11.3 0 1 0 0 22.5 11.3 11.3 0 0 0 0-22.5m1.7-90c-7 0-15.4 7.7-15.4 14.7s8 17.2 15 17.2 15.8-9.5 15.8-16.5-8.4-15.4-15.4-15.4m3.9 16.2a4.6 4.6 0 1 1 0-9.2 4.6 4.6 0 0 1 0 9.2m-97.2-15.9c-7 0-14.4 8.1-14.4 15s8.9 16.2 15.8 16.2 15.8-9.1 15.8-16.1-10.2-15.1-17.2-15.1m6.1 16a4.6 4.6 0 1 1 0-9.3 4.6 4.6 0 0 1 0 9.2" style="fill:#5a3620" transform="translate(10 10)"/><path d="M336.3 256.4c3.6-9.1 7.7-11 9.3-11.3-40.7 3.7-36.6 27.7-34 36l1 2.7s2 4.8 7.6 8.7c4.1 3 10.3 5.5 19 5.2 27 .4 35.6-50.2 35.6-50.2-6.6 11.7-26.4 9.9-38.5 9M49 69.4l5.3-5.4c-9-7-24.4-15.9-41.8-11.7A54 54 0 0 0 36 86.5q5.5-8.8 12.4-16.5l-.2.2zm209.8-17.2a49 49 0 0 0-39.2 9.8q10.8 9.9 19 22.3a54 54 0 0 0 20.2-32M134.3 160.2c-43.3 0-78.4 23-78.4 51.3v1.7l.2 1.6a94.8 94.8 0 0 0 153.1-18.3c-9.8-21-39.6-36.3-75-36.3M87.5 215a11.3 11.3 0 1 1 0-22.6 11.3 11.3 0 0 1 0 22.5m93.8 0a11.3 11.3 0 1 1 0-22.6 11.3 11.3 0 0 1 0 22.5M86.3 0c-18.2 16.4-.2 41.4 0 41.6Q102.6 34 121 31.1C97.6 27.7 86.3 0 86.3 0m99.9 0S175.3 26.5 153 30.9q18 2.3 34 9.5c3.4-5.3 15-26.1-.8-40.4" style="fill:#fef3df" transform="translate(10 10)"/><path d="M218 176a163 163 0 0 0 6.5-35.7c0-50-39.3-83.9-86.8-89.8-2.1 28 3.7 87 80.2 125.5m-47.6-58.3a12.6 12.6 0 1 1 25.2 0 12.6 12.6 0 0 1-25.2 0" style="fill:#87654a" transform="translate(10 10)"/><path d="m312.7 283.8-1-2.8a54 54 0 0 1-27.1 12.5q-6 1-13.3.5v28a206 206 0 0 0 26.2-12.5h.1c8.2-4.7 16.4-10.5 22.6-17-5.5-4-7.5-8.8-7.5-8.8M12.5 52.2C30 48 45.4 57 54.3 64q8.4-8.2 18.3-14.6C48.3 18.5 2.2 37.2 2.2 37.2A55 55 0 0 0 31.7 94q2-3.7 4.3-7.5C15.4 72.7 12.5 52.2 12.5 52.2m187.9-4.8q10.3 6.3 19.2 14.6a49 49 0 0 1 39.2-9.8s-2.5 18.4-20.3 32q2.5 3.8 4.6 7.7a54 54 0 0 0 26-54.7s-44-18-68.7 10.2m-61.5 3.1a96 96 0 0 0-99 54q-1.5 6.6-1.6 13.6v4.2q.3 6 1 11.1c4.2 25 7.9 42.5 13.4 66.8l.2 1.2q1 6 3 11.8v-1.7c0-28.3 35-51.3 78.4-51.3 35.3 0 65.1 15.3 75 36.3a99 99 0 0 0 8.6-20.5c-38-23.5-53.9-41.1-64.6-64.2-10.8-23-15.5-47.3-14.4-61.3M91 130.3a12.6 12.6 0 1 1 0-25.2 12.6 12.6 0 0 1 0 25.2" style="fill:#b58765" transform="translate(10 10)"/></svg>`,
|
||||
displayName: "mailcow",
|
||||
pkce: true,
|
||||
scopes: []string{"profile"},
|
||||
|
||||
@@ -28,6 +28,8 @@ func NewMicrosoftProvider() *Microsoft {
|
||||
endpoints := microsoft.AzureADEndpoint("")
|
||||
return &Microsoft{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 3,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" preserveAspectRatio="xMidYMid"><path fill="#f1511b" d="M122 122H0V0h122z"/><path fill="#80cc28" d="M256 122H134V0h122z"/><path fill="#00adef" d="M122 256H0V134h122z"/><path fill="#fbbc09" d="M256 256H134V134h122z"/></svg>`,
|
||||
displayName: "Microsoft",
|
||||
pkce: true,
|
||||
scopes: []string{"User.Read"},
|
||||
|
||||
@@ -29,6 +29,8 @@ type Monday struct {
|
||||
func NewMondayProvider() *Monday {
|
||||
return &Monday{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 18,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 127 127"><path fill="#fb275d" d="M24.8 87.2c-3.7 0-7.1-1.9-8.9-5.1a9 9 0 0 1 .3-9.9L34.5 44c1.9-3.1 5.4-4.9 9.1-4.8s7.1 2.1 8.8 5.3 1.5 7-.5 9.9L33.4 82.6a10 10 0 0 1-8.6 4.6"/><path fill="#fc0" d="M56.1 87.2c-3.7 0-7.1-1.9-8.9-5a9 9 0 0 1 .3-9.9l18.4-28.1c1.9-3.1 5.3-5 9.1-4.9s7.1 2.1 8.8 5.3 1.5 7-.7 10l-18.4 28a11 11 0 0 1-8.6 4.6"/><path fill="#00ca72" d="M86.7 87.2c5.6 0 10.2-4.6 10.2-10.2s-4.6-10.2-10.2-10.2S76.5 71.4 76.5 77c0 5.7 4.5 10.2 10.2 10.2"/></svg>`,
|
||||
displayName: "monday.com",
|
||||
pkce: true,
|
||||
scopes: []string{"me:read"},
|
||||
|
||||
@@ -27,6 +27,8 @@ type Notion struct {
|
||||
func NewNotionProvider() *Notion {
|
||||
return &Notion{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 17,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="none"><path fill="#000" d="M61 0 6 4q-6 2-6 7v61l3 8 13 17q2 4 8 3l65-4q7-1 7-7V21q0-3-4-5L74 3q-4-4-13-3M26 20q-7 1-9-2l-8-6q-1-2 1-2l54-4q5 0 8 2l9 7 1 1zm-6 68V30q0-3 3-4l63-3q3 0 3 3v58q1 5-4 5l-60 3q-5 1-5-4m59-55q1 4-1 4l-3 1v43l-7 2q-4 0-6-4L43 49v29l6 1s0 4-5 4H30l2-3 3-1V41h-5q0-4 4-5l14-1 20 31V39l-5-1q0-3 3-4z"/></svg>`,
|
||||
displayName: "Notion",
|
||||
pkce: true,
|
||||
authURL: "https://api.notion.com/v1/oauth/authorize",
|
||||
|
||||
@@ -57,6 +57,8 @@ type OIDC struct {
|
||||
func NewOIDCProvider() *OIDC {
|
||||
return &OIDC{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 99,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="93" height="84" fill="none"><path fill="#ccc" d="M83.4 32.9c-8.7-5.5-21-8.8-34.3-8.8C22 24 .4 37.5.4 54 .4 69.3 18.5 81.8 42 84v-8.7c-15.9-2-27.8-10.7-27.8-21 0-11.9 15.5-21.6 34.8-21.6 9.5 0 18.2 2.4 24.5 6.3l-9 5.6h27.9V27.3z"/><path fill="#ff6200" d="M42 9.2V84l14-8.7V.2z"/></svg>`,
|
||||
displayName: "OIDC",
|
||||
pkce: true,
|
||||
scopes: []string{
|
||||
|
||||
@@ -27,6 +27,8 @@ type Patreon struct {
|
||||
func NewPatreonProvider() *Patreon {
|
||||
return &Patreon{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 24,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 1080 1080"><path d="M1033 324c0-137-108-250-234-291a746 746 0 0 0-512 27C106 145 49 333 47 519c-2 154 14 558 242 561 169 2 194-216 273-321 56-75 127-96 216-118a320 320 0 0 0 255-317"/></svg>`,
|
||||
displayName: "Patreon",
|
||||
pkce: true,
|
||||
scopes: []string{"identity", "identity[email]"},
|
||||
|
||||
@@ -27,6 +27,8 @@ type Planningcenter struct {
|
||||
func NewPlanningcenterProvider() *Planningcenter {
|
||||
return &Planningcenter{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 29,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0.1 26.2 26.4"><path fill="#2565f4" d="M23.2 2.7c1.8.5 3 2.2 3 4v13c0 1.8-1.3 3.5-3 4l-8 2.5a8 8 0 0 1-4.3 0L3 23.8c-1.7-.6-3-2.3-3-4.1v-13c0-1.8 1.3-3.4 3-4l8-2.3a8 8 0 0 1 4.2 0z"/><path fill="#fff" d="m18 7.6-4.7 1.3h-.5L8 7.6q-1.7-.3-1.8 1.3v10.5q0 .3.3.4l1.7.4q.5.1.5-.3v-2.5H9l3.5 1h1.4l5.2-1.6q.9-.2.9-1V9c0-1-1-1.7-2-1.4m-.6 7-.2.1-3.7 1.2h-.6l-3.7-1a.2.2 0 0 1-.2-.3v-3.8q0-.2.3-.2l3 .8a3 3 0 0 0 1.8 0l3-.8q.2 0 .3.2z"/></svg>`,
|
||||
displayName: "Planning Center",
|
||||
pkce: true,
|
||||
scopes: []string{"people"},
|
||||
|
||||
@@ -27,6 +27,8 @@ type Spotify struct {
|
||||
func NewSpotifyProvider() *Spotify {
|
||||
return &Spotify{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 21,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" preserveAspectRatio="xMidYMid"><path fill="#1ed760" d="M128 0C57.3 0 0 57.3 0 128s57.3 128 128 128 128-57.3 128-128C256 57.31 198.7 0 128 0m58.7 184.61a7.97 7.97 0 0 1-10.98 2.65c-30.05-18.36-67.88-22.52-112.44-12.34a7.98 7.98 0 0 1-3.55-15.56c48.76-11.14 90.58-6.34 124.32 14.28a8 8 0 0 1 2.65 10.97m15.67-34.85a10 10 0 0 1-13.73 3.29c-34.4-21.15-86.85-27.27-127.55-14.92a10 10 0 0 1-12.45-6.65 10 10 0 0 1 6.65-12.45c46.49-14.1 104.28-7.27 143.79 17.01a10 10 0 0 1 3.29 13.72m1.34-36.3c-41.25-24.5-109.32-26.75-148.7-14.8a11.97 11.97 0 1 1-6.95-22.9c45.21-13.73 120.37-11.08 167.87 17.12a11.96 11.96 0 1 1-12.21 20.6z"/></svg>`,
|
||||
displayName: "Spotify",
|
||||
pkce: true,
|
||||
scopes: []string{
|
||||
|
||||
@@ -27,6 +27,8 @@ type Strava struct {
|
||||
func NewStravaProvider() *Strava {
|
||||
return &Strava{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 25,
|
||||
logo: `<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" width="2500" height="2500"><path d="M0 0h16v16H0z" fill="#fc4c02"/><g fill="#fff" fill-rule="evenodd"><path d="M6.9 8.8l2.5 4.5 2.4-4.5h-1.5l-.9 1.7-1-1.7z" opacity=".6"/><path d="M7.2 2.5l3.1 6.3H4zm0 3.8l1.2 2.5H5.9z"/></g></svg>`,
|
||||
displayName: "Strava",
|
||||
pkce: true,
|
||||
scopes: []string{
|
||||
|
||||
@@ -27,6 +27,8 @@ type Trakt struct {
|
||||
func NewTraktProvider() *Trakt {
|
||||
return &Trakt{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 22,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><defs><radialGradient id="a" cx="48.5" cy="-.9" r="64.8" fx="48.5" fy="-.9" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#9f42c6"/><stop offset=".3" stop-color="#a041c3"/><stop offset=".4" stop-color="#a43ebb"/><stop offset=".5" stop-color="#aa39ad"/><stop offset=".6" stop-color="#b4339a"/><stop offset=".7" stop-color="#c02b81"/><stop offset=".8" stop-color="#cf2061"/><stop offset=".9" stop-color="#e1143c"/><stop offset="1" stop-color="#f50613"/><stop offset="1" stop-color="red"/></radialGradient></defs><path d="M48 11.3v25.4C48 43 43 48 36.7 48H11.3C5 48 0 43 0 36.7V11.3C0 5 5 0 11.3 0h25.4A11 11 0 0 1 48 11.3" style="fill:url(#a)"/><path d="m13.6 18 8 7.9 1.4-1.5-8-7.9zM28 32.4l1.5-1.5-2.2-2.2L47.6 8.4q-.2-1-.8-2.1L24.5 28.7zM13 18.7 11.4 20l14.4 14.4 1.4-1.4-4.3-4.3L46.4 5.4 45 3.7 21.5 27.3zm34.9-9.1L28.7 28.8l1.5 1.4L48 12.4v-1.1zM25.2 22.3l-8-8-1.4 1.5 7.9 8zM41.3 35c0 3.4-2.8 6.2-6.2 6.2H13A6 6 0 0 1 6.8 35V13c0-3.4 2.8-6.2 6.2-6.2h20.8V4.6H12.9a8.3 8.3 0 0 0-8.3 8.3V35c0 4.6 3.7 8.3 8.3 8.3H35c4.6 0 8.3-3.7 8.3-8.3v-3.5h-2z" style="fill:#fff"/></svg>`,
|
||||
displayName: "Trakt",
|
||||
pkce: true,
|
||||
authURL: "https://trakt.tv/oauth/authorize",
|
||||
|
||||
@@ -29,6 +29,8 @@ type Twitch struct {
|
||||
func NewTwitchProvider() *Twitch {
|
||||
return &Twitch{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 23,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="256" height="268" preserveAspectRatio="xMidYMid"><path fill="#5a3e85" d="M17 0 0 47v186h64v35h35l35-35h52l70-70V0zm24 23h192v128l-41 41h-64l-35 35v-35H41zm64 117h23V70h-23zm64 0h23V70h-23z"/></svg>`,
|
||||
displayName: "Twitch",
|
||||
pkce: true,
|
||||
scopes: []string{"user:read:email"},
|
||||
|
||||
@@ -26,6 +26,8 @@ type Twitter struct {
|
||||
func NewTwitterProvider() *Twitter {
|
||||
return &Twitter{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 13,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="300" height="300.3"><path d="M179 127 290 0h-26l-97 110L89 0H0l117 167L0 300h26l103-116 82 116h89M36 20h41l187 262h-41"/></svg>`,
|
||||
displayName: "X/Twitter",
|
||||
pkce: true,
|
||||
scopes: []string{
|
||||
|
||||
@@ -22,6 +22,8 @@ var _ Provider = (*VK)(nil)
|
||||
// NameVK is the unique name of the VK provider.
|
||||
const NameVK string = "vk"
|
||||
|
||||
// @todo mark as deprecated
|
||||
//
|
||||
// VK allows authentication via VK OAuth2.
|
||||
type VK struct {
|
||||
BaseProvider
|
||||
@@ -33,6 +35,8 @@ type VK struct {
|
||||
func NewVKProvider() *VK {
|
||||
return &VK{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 15,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="none"><path fill="#07f" d="M0 23C0 12.2 0 6.7 3.4 3.4 6.7 0 12.2 0 23 0h2c10.8 0 16.3 0 19.6 3.4C48 6.7 48 12.2 48 23v2c0 10.8 0 16.3-3.4 19.6C41.3 48 35.8 48 25 48h-2c-10.8 0-16.3 0-19.6-3.4C0 41.3 0 35.8 0 25z"/><path fill="#fff" d="M25.5 34.6c-10.9 0-17.1-7.5-17.4-20h5.5c.2 9.2 4.2 13 7.4 13.8V14.6h5.2v7.9c3.1-.3 6.4-4 7.6-7.9h5.1a15 15 0 0 1-7 10c2.6 1.2 6.7 4.3 8.2 10h-5.7a10 10 0 0 0-8.2-7.2v7.2z"/></svg>`,
|
||||
displayName: "ВКонтакте",
|
||||
pkce: false, // VK currently doesn't support PKCE and throws an error if PKCE params are send
|
||||
scopes: []string{"email"},
|
||||
|
||||
@@ -26,6 +26,8 @@ type Wakatime struct {
|
||||
func NewWakatimeProvider() *Wakatime {
|
||||
return &Wakatime{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 26,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="340" height="340" fill="none"><path stroke="#000" stroke-width="40" d="M170 20a150 150 0 1 0 0 300 150 150 0 0 0 0-300Z" clip-rule="evenodd"/><path fill="#000" stroke="#000" stroke-width="10" d="M190.2 213.5a8 8 0 0 1-7.6 3l-2-.8a9 9 0 0 1-2.3-2l-.9-1.3-8.8-14.2-8.8 14.2a8 8 0 0 1-6.8 4.3 8 8 0 0 1-6.8-4.4l-38.6-56.2a9 9 0 0 1-2-5.8c0-4.8 3.4-8.6 7.7-8.6 2.8 0 5.3 1.6 6.6 4l32.7 48.2 9.1-15a8 8 0 0 1 6.9-4.4q4.2.2 6.4 3.8l9.5 15.5 51.2-73.2q2.3-3.8 6.5-4c4.3 0 7.8 4 7.8 8.7q0 3.2-1.8 5.4z"/></svg>`,
|
||||
displayName: "WakaTime",
|
||||
pkce: true,
|
||||
scopes: []string{"email"},
|
||||
|
||||
@@ -29,6 +29,8 @@ type Yandex struct {
|
||||
func NewYandexProvider() *Yandex {
|
||||
return &Yandex{BaseProvider{
|
||||
ctx: context.Background(),
|
||||
order: 4,
|
||||
logo: `<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" fill="none"><rect width="44" height="44" fill="#fc3f1d" rx="22"/><path fill="#fff" d="M25.2 12.3H23c-3.8 0-5.7 2-5.7 4.8 0 3.2 1.3 4.8 4.1 6.7l2.3 1.6-6.4 9.8h-5.1l6-8.9c-3.5-2.5-5.4-4.8-5.4-8.9 0-5 3.5-8.6 10.2-8.6h6.7v26.4h-4.5z"/></svg>`,
|
||||
displayName: "Yandex",
|
||||
pkce: true,
|
||||
scopes: []string{"login:email", "login:avatar", "login:info"},
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# all environments should start with 'PB_' prefix
|
||||
PB_BACKEND_URL = "../"
|
||||
PB_MFA_DOCS = "https://pocketbase.io/docs/authentication#multi-factor-authentication"
|
||||
PB_OAUTH2_EXAMPLE = "https://pocketbase.io/docs/authentication#authenticate-with-oauth2"
|
||||
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.36.9"
|
||||
PB_VERSION = "v0.37.0"
|
||||
|
||||
+21
-6
@@ -1,7 +1,22 @@
|
||||
/node_modules/
|
||||
/.vscode/
|
||||
.DS_Store
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# exclude local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
node_modules
|
||||
*.local
|
||||
*.todo
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
+8
-5
@@ -1,20 +1,23 @@
|
||||
PocketBase Superuser dashboard UI
|
||||
======================================================================
|
||||
|
||||
This is the PocketBase Superuser dashboard UI (built with Svelte and Vite).
|
||||
This is the PocketBase Superuser dashboard UI (built with Shablon and Vite).
|
||||
|
||||
Although it could be used independently, it is mainly intended to be embedded
|
||||
as part of a PocketBase app executable (hence the `embed.go` file).
|
||||
Although it could be used independently, it is intended to be embedded and extended
|
||||
as part of the PocketBase app executable (hence the `dist` directory and `embed.go` file).
|
||||
|
||||
> [!WARNING]
|
||||
> The UI kit and extension APIs remains deliberately undocumented for the time being until a stable PocketBase release is published ([#7612](https://github.com/pocketbase/pocketbase/discussions/7612)).
|
||||
|
||||
## Development
|
||||
|
||||
Download the repo and run the appropriate console commands:
|
||||
Download the repository and run the appropriate console commands:
|
||||
|
||||
```sh
|
||||
# install dependencies
|
||||
npm install
|
||||
|
||||
# start a dev server with hot reload at localhost:3000
|
||||
# start a dev server with hot reload at localhost:5173
|
||||
npm run dev
|
||||
|
||||
# or generate production ready bundle in dist/ directory
|
||||
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
import{S as Ce,i as Be,s as Te,V as Le,X as J,h as u,d as ae,t as Q,a as G,I as N,Z as we,_ as Se,C as De,$ as Re,D as Ue,l as d,n as a,m as ne,u as c,A as y,v as k,c as ie,w as h,p as oe,J as je,k as O,o as qe,W as Ee}from"./index-DMlPjiFP.js";import{F as Fe}from"./FieldsQueryParam-B36GF025.js";function ye(n,s,l){const o=n.slice();return o[8]=s[l],o}function Me(n,s,l){const o=n.slice();return o[8]=s[l],o}function Ae(n,s){let l,o=s[8].code+"",p,b,i,f;function m(){return s[6](s[8])}return{key:n,first:null,c(){l=c("button"),p=y(o),b=k(),h(l,"class","tab-item"),O(l,"active",s[1]===s[8].code),this.first=l},m(v,$){d(v,l,$),a(l,p),a(l,b),i||(f=qe(l,"click",m),i=!0)},p(v,$){s=v,$&4&&o!==(o=s[8].code+"")&&N(p,o),$&6&&O(l,"active",s[1]===s[8].code)},d(v){v&&u(l),i=!1,f()}}}function Pe(n,s){let l,o,p,b;return o=new Ee({props:{content:s[8].body}}),{key:n,first:null,c(){l=c("div"),ie(o.$$.fragment),p=k(),h(l,"class","tab-item"),O(l,"active",s[1]===s[8].code),this.first=l},m(i,f){d(i,l,f),ne(o,l,null),a(l,p),b=!0},p(i,f){s=i;const m={};f&4&&(m.content=s[8].body),o.$set(m),(!b||f&6)&&O(l,"active",s[1]===s[8].code)},i(i){b||(G(o.$$.fragment,i),b=!0)},o(i){Q(o.$$.fragment,i),b=!1},d(i){i&&u(l),ae(o)}}}function He(n){var ke,ge;let s,l,o=n[0].name+"",p,b,i,f,m,v,$,g=n[0].name+"",V,ce,W,M,X,L,Z,A,E,re,F,S,ue,z,H=n[0].name+"",K,de,Y,D,x,P,ee,fe,te,T,le,R,se,C,U,w=[],me=new Map,pe,j,_=[],be=new Map,B;M=new Le({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${n[3]}');
|
||||
|
||||
...
|
||||
|
||||
const result = await pb.collection('${(ke=n[0])==null?void 0:ke.name}').listAuthMethods();
|
||||
`,dart:`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${n[3]}');
|
||||
|
||||
...
|
||||
|
||||
final result = await pb.collection('${(ge=n[0])==null?void 0:ge.name}').listAuthMethods();
|
||||
`}}),T=new Fe({});let I=J(n[2]);const he=e=>e[8].code;for(let e=0;e<I.length;e+=1){let t=Me(n,I,e),r=he(t);me.set(r,w[e]=Ae(r,t))}let q=J(n[2]);const _e=e=>e[8].code;for(let e=0;e<q.length;e+=1){let t=ye(n,q,e),r=_e(t);be.set(r,_[e]=Pe(r,t))}return{c(){s=c("h3"),l=y("List auth methods ("),p=y(o),b=y(")"),i=k(),f=c("div"),m=c("p"),v=y("Returns a public list with all allowed "),$=c("strong"),V=y(g),ce=y(" authentication methods."),W=k(),ie(M.$$.fragment),X=k(),L=c("h6"),L.textContent="API details",Z=k(),A=c("div"),E=c("strong"),E.textContent="GET",re=k(),F=c("div"),S=c("p"),ue=y("/api/collections/"),z=c("strong"),K=y(H),de=y("/auth-methods"),Y=k(),D=c("div"),D.textContent="Query parameters",x=k(),P=c("table"),ee=c("thead"),ee.innerHTML='<tr><th>Param</th> <th>Type</th> <th width="50%">Description</th></tr>',fe=k(),te=c("tbody"),ie(T.$$.fragment),le=k(),R=c("div"),R.textContent="Responses",se=k(),C=c("div"),U=c("div");for(let e=0;e<w.length;e+=1)w[e].c();pe=k(),j=c("div");for(let e=0;e<_.length;e+=1)_[e].c();h(s,"class","m-b-sm"),h(f,"class","content txt-lg m-b-sm"),h(L,"class","m-b-xs"),h(E,"class","label label-primary"),h(F,"class","content"),h(A,"class","alert alert-info"),h(D,"class","section-title"),h(P,"class","table-compact table-border m-b-base"),h(R,"class","section-title"),h(U,"class","tabs-header compact combined left"),h(j,"class","tabs-content"),h(C,"class","tabs")},m(e,t){d(e,s,t),a(s,l),a(s,p),a(s,b),d(e,i,t),d(e,f,t),a(f,m),a(m,v),a(m,$),a($,V),a(m,ce),d(e,W,t),ne(M,e,t),d(e,X,t),d(e,L,t),d(e,Z,t),d(e,A,t),a(A,E),a(A,re),a(A,F),a(F,S),a(S,ue),a(S,z),a(z,K),a(S,de),d(e,Y,t),d(e,D,t),d(e,x,t),d(e,P,t),a(P,ee),a(P,fe),a(P,te),ne(T,te,null),d(e,le,t),d(e,R,t),d(e,se,t),d(e,C,t),a(C,U);for(let r=0;r<w.length;r+=1)w[r]&&w[r].m(U,null);a(C,pe),a(C,j);for(let r=0;r<_.length;r+=1)_[r]&&_[r].m(j,null);B=!0},p(e,[t]){var ve,$e;(!B||t&1)&&o!==(o=e[0].name+"")&&N(p,o),(!B||t&1)&&g!==(g=e[0].name+"")&&N(V,g);const r={};t&9&&(r.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${e[3]}');
|
||||
|
||||
...
|
||||
|
||||
const result = await pb.collection('${(ve=e[0])==null?void 0:ve.name}').listAuthMethods();
|
||||
`),t&9&&(r.dart=`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${e[3]}');
|
||||
|
||||
...
|
||||
|
||||
final result = await pb.collection('${($e=e[0])==null?void 0:$e.name}').listAuthMethods();
|
||||
`),M.$set(r),(!B||t&1)&&H!==(H=e[0].name+"")&&N(K,H),t&6&&(I=J(e[2]),w=we(w,t,he,1,e,I,me,U,Se,Ae,null,Me)),t&6&&(q=J(e[2]),De(),_=we(_,t,_e,1,e,q,be,j,Re,Pe,null,ye),Ue())},i(e){if(!B){G(M.$$.fragment,e),G(T.$$.fragment,e);for(let t=0;t<q.length;t+=1)G(_[t]);B=!0}},o(e){Q(M.$$.fragment,e),Q(T.$$.fragment,e);for(let t=0;t<_.length;t+=1)Q(_[t]);B=!1},d(e){e&&(u(s),u(i),u(f),u(W),u(X),u(L),u(Z),u(A),u(Y),u(D),u(x),u(P),u(le),u(R),u(se),u(C)),ae(M,e),ae(T);for(let t=0;t<w.length;t+=1)w[t].d();for(let t=0;t<_.length;t+=1)_[t].d()}}}function Ie(n,s,l){let o,{collection:p}=s,b=200,i=[],f={},m=!1;v();async function v(){l(5,m=!0);try{l(4,f=await oe.collection(p.name).listAuthMethods())}catch(g){oe.error(g)}l(5,m=!1)}const $=g=>l(1,b=g.code);return n.$$set=g=>{"collection"in g&&l(0,p=g.collection)},n.$$.update=()=>{n.$$.dirty&48&&l(2,i=[{code:200,body:m?"...":JSON.stringify(f,null,2)},{code:404,body:`
|
||||
{
|
||||
"status": 404,
|
||||
"message": "Missing collection context.",
|
||||
"data": {}
|
||||
}
|
||||
`}])},l(3,o=je.getApiExampleUrl(oe.baseURL)),[p,b,i,o,f,m,$]}class Ge extends Ce{constructor(s){super(),Be(this,s,Ie,He,Te,{collection:0})}}export{Ge as default};
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
import{S as je,i as xe,s as Ie,V as Ke,W as Ue,X as I,h as d,d as K,t as E,a as z,I as de,Z as Oe,_ as Qe,C as We,$ as Xe,D as Ze,l as u,n as o,m as Q,u as s,A as k,v as p,c as W,w as b,J as Ve,p as Ge,k as X,o as Ye}from"./index-DMlPjiFP.js";import{F as et}from"./FieldsQueryParam-B36GF025.js";function Ee(r,a,l){const n=r.slice();return n[5]=a[l],n}function ze(r,a,l){const n=r.slice();return n[5]=a[l],n}function Je(r,a){let l,n=a[5].code+"",m,_,i,h;function g(){return a[4](a[5])}return{key:r,first:null,c(){l=s("button"),m=k(n),_=p(),b(l,"class","tab-item"),X(l,"active",a[1]===a[5].code),this.first=l},m(v,w){u(v,l,w),o(l,m),o(l,_),i||(h=Ye(l,"click",g),i=!0)},p(v,w){a=v,w&4&&n!==(n=a[5].code+"")&&de(m,n),w&6&&X(l,"active",a[1]===a[5].code)},d(v){v&&d(l),i=!1,h()}}}function Ne(r,a){let l,n,m,_;return n=new Ue({props:{content:a[5].body}}),{key:r,first:null,c(){l=s("div"),W(n.$$.fragment),m=p(),b(l,"class","tab-item"),X(l,"active",a[1]===a[5].code),this.first=l},m(i,h){u(i,l,h),Q(n,l,null),o(l,m),_=!0},p(i,h){a=i;const g={};h&4&&(g.content=a[5].body),n.$set(g),(!_||h&6)&&X(l,"active",a[1]===a[5].code)},i(i){_||(z(n.$$.fragment,i),_=!0)},o(i){E(n.$$.fragment,i),_=!1},d(i){i&&d(l),K(n)}}}function tt(r){var qe,Fe;let a,l,n=r[0].name+"",m,_,i,h,g,v,w,D,Z,S,J,ue,N,M,pe,G,U=r[0].name+"",Y,he,fe,j,ee,q,te,T,oe,be,F,C,ae,me,le,_e,f,ke,P,ge,ve,$e,se,ye,ne,Se,we,Te,re,Ce,Re,A,ie,H,ce,R,L,y=[],Pe=new Map,Ae,O,$=[],Be=new Map,B;v=new Ke({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${r[3]}');
|
||||
|
||||
...
|
||||
|
||||
const authData = await pb.collection('${(qe=r[0])==null?void 0:qe.name}').authRefresh();
|
||||
|
||||
// after the above you can also access the refreshed auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.record.id);
|
||||
`,dart:`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${r[3]}');
|
||||
|
||||
...
|
||||
|
||||
final authData = await pb.collection('${(Fe=r[0])==null?void 0:Fe.name}').authRefresh();
|
||||
|
||||
// after the above you can also access the refreshed auth data from the authStore
|
||||
print(pb.authStore.isValid);
|
||||
print(pb.authStore.token);
|
||||
print(pb.authStore.record.id);
|
||||
`}}),P=new Ue({props:{content:"?expand=relField1,relField2.subRelField"}}),A=new et({props:{prefix:"record."}});let x=I(r[2]);const De=e=>e[5].code;for(let e=0;e<x.length;e+=1){let t=ze(r,x,e),c=De(t);Pe.set(c,y[e]=Je(c,t))}let V=I(r[2]);const Me=e=>e[5].code;for(let e=0;e<V.length;e+=1){let t=Ee(r,V,e),c=Me(t);Be.set(c,$[e]=Ne(c,t))}return{c(){a=s("h3"),l=k("Auth refresh ("),m=k(n),_=k(")"),i=p(),h=s("div"),h.innerHTML=`<p>Returns a new auth response (token and record data) for an
|
||||
<strong>already authenticated record</strong>.</p> <p>This method is usually called by users on page/screen reload to ensure that the previously stored data
|
||||
in <code>pb.authStore</code> is still valid and up-to-date.</p>`,g=p(),W(v.$$.fragment),w=p(),D=s("h6"),D.textContent="API details",Z=p(),S=s("div"),J=s("strong"),J.textContent="POST",ue=p(),N=s("div"),M=s("p"),pe=k("/api/collections/"),G=s("strong"),Y=k(U),he=k("/auth-refresh"),fe=p(),j=s("p"),j.innerHTML="Requires <code>Authorization:TOKEN</code> header",ee=p(),q=s("div"),q.textContent="Query parameters",te=p(),T=s("table"),oe=s("thead"),oe.innerHTML='<tr><th>Param</th> <th>Type</th> <th width="60%">Description</th></tr>',be=p(),F=s("tbody"),C=s("tr"),ae=s("td"),ae.textContent="expand",me=p(),le=s("td"),le.innerHTML='<span class="label">String</span>',_e=p(),f=s("td"),ke=k(`Auto expand record relations. Ex.:
|
||||
`),W(P.$$.fragment),ge=k(`
|
||||
Supports up to 6-levels depth nested relations expansion. `),ve=s("br"),$e=k(`
|
||||
The expanded relations will be appended to the record under the
|
||||
`),se=s("code"),se.textContent="expand",ye=k(" property (eg. "),ne=s("code"),ne.textContent='"expand": {"relField1": {...}, ...}',Se=k(`).
|
||||
`),we=s("br"),Te=k(`
|
||||
Only the relations to which the request user has permissions to `),re=s("strong"),re.textContent="view",Ce=k(" will be expanded."),Re=p(),W(A.$$.fragment),ie=p(),H=s("div"),H.textContent="Responses",ce=p(),R=s("div"),L=s("div");for(let e=0;e<y.length;e+=1)y[e].c();Ae=p(),O=s("div");for(let e=0;e<$.length;e+=1)$[e].c();b(a,"class","m-b-sm"),b(h,"class","content txt-lg m-b-sm"),b(D,"class","m-b-xs"),b(J,"class","label label-primary"),b(N,"class","content"),b(j,"class","txt-hint txt-sm txt-right"),b(S,"class","alert alert-success"),b(q,"class","section-title"),b(T,"class","table-compact table-border m-b-base"),b(H,"class","section-title"),b(L,"class","tabs-header compact combined left"),b(O,"class","tabs-content"),b(R,"class","tabs")},m(e,t){u(e,a,t),o(a,l),o(a,m),o(a,_),u(e,i,t),u(e,h,t),u(e,g,t),Q(v,e,t),u(e,w,t),u(e,D,t),u(e,Z,t),u(e,S,t),o(S,J),o(S,ue),o(S,N),o(N,M),o(M,pe),o(M,G),o(G,Y),o(M,he),o(S,fe),o(S,j),u(e,ee,t),u(e,q,t),u(e,te,t),u(e,T,t),o(T,oe),o(T,be),o(T,F),o(F,C),o(C,ae),o(C,me),o(C,le),o(C,_e),o(C,f),o(f,ke),Q(P,f,null),o(f,ge),o(f,ve),o(f,$e),o(f,se),o(f,ye),o(f,ne),o(f,Se),o(f,we),o(f,Te),o(f,re),o(f,Ce),o(F,Re),Q(A,F,null),u(e,ie,t),u(e,H,t),u(e,ce,t),u(e,R,t),o(R,L);for(let c=0;c<y.length;c+=1)y[c]&&y[c].m(L,null);o(R,Ae),o(R,O);for(let c=0;c<$.length;c+=1)$[c]&&$[c].m(O,null);B=!0},p(e,[t]){var He,Le;(!B||t&1)&&n!==(n=e[0].name+"")&&de(m,n);const c={};t&9&&(c.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${e[3]}');
|
||||
|
||||
...
|
||||
|
||||
const authData = await pb.collection('${(He=e[0])==null?void 0:He.name}').authRefresh();
|
||||
|
||||
// after the above you can also access the refreshed auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.record.id);
|
||||
`),t&9&&(c.dart=`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${e[3]}');
|
||||
|
||||
...
|
||||
|
||||
final authData = await pb.collection('${(Le=e[0])==null?void 0:Le.name}').authRefresh();
|
||||
|
||||
// after the above you can also access the refreshed auth data from the authStore
|
||||
print(pb.authStore.isValid);
|
||||
print(pb.authStore.token);
|
||||
print(pb.authStore.record.id);
|
||||
`),v.$set(c),(!B||t&1)&&U!==(U=e[0].name+"")&&de(Y,U),t&6&&(x=I(e[2]),y=Oe(y,t,De,1,e,x,Pe,L,Qe,Je,null,ze)),t&6&&(V=I(e[2]),We(),$=Oe($,t,Me,1,e,V,Be,O,Xe,Ne,null,Ee),Ze())},i(e){if(!B){z(v.$$.fragment,e),z(P.$$.fragment,e),z(A.$$.fragment,e);for(let t=0;t<V.length;t+=1)z($[t]);B=!0}},o(e){E(v.$$.fragment,e),E(P.$$.fragment,e),E(A.$$.fragment,e);for(let t=0;t<$.length;t+=1)E($[t]);B=!1},d(e){e&&(d(a),d(i),d(h),d(g),d(w),d(D),d(Z),d(S),d(ee),d(q),d(te),d(T),d(ie),d(H),d(ce),d(R)),K(v,e),K(P),K(A);for(let t=0;t<y.length;t+=1)y[t].d();for(let t=0;t<$.length;t+=1)$[t].d()}}}function ot(r,a,l){let n,{collection:m}=a,_=200,i=[];const h=g=>l(1,_=g.code);return r.$$set=g=>{"collection"in g&&l(0,m=g.collection)},r.$$.update=()=>{r.$$.dirty&1&&l(2,i=[{code:200,body:JSON.stringify({token:"JWT_TOKEN",record:Ve.dummyCollectionRecord(m)},null,2)},{code:401,body:`
|
||||
{
|
||||
"status": 401,
|
||||
"message": "The request requires valid record authorization token to be set.",
|
||||
"data": {}
|
||||
}
|
||||
`},{code:403,body:`
|
||||
{
|
||||
"status": 403,
|
||||
"message": "The authorized record model is not allowed to perform this action.",
|
||||
"data": {}
|
||||
}
|
||||
`},{code:404,body:`
|
||||
{
|
||||
"status": 404,
|
||||
"message": "Missing auth record context.",
|
||||
"data": {}
|
||||
}
|
||||
`}])},l(3,n=Ve.getApiExampleUrl(Ge.baseURL)),[m,_,i,n,h]}class st extends je{constructor(a){super(),xe(this,a,ot,tt,Ie,{collection:0})}}export{st as default};
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
import{S as Je,i as xe,s as Ee,V as Ne,W as je,X as Q,h as r,d as Z,t as j,a as J,I as pe,Z as Ue,_ as Ie,C as Qe,$ as Ze,D as ze,l as c,n as a,m as z,u as o,A as _,v as h,c as K,w as p,J as Be,p as Ke,k as X,o as Xe}from"./index-DMlPjiFP.js";import{F as Ge}from"./FieldsQueryParam-B36GF025.js";function Fe(s,l,n){const i=s.slice();return i[5]=l[n],i}function Le(s,l,n){const i=s.slice();return i[5]=l[n],i}function He(s,l){let n,i=l[5].code+"",f,g,d,b;function k(){return l[4](l[5])}return{key:s,first:null,c(){n=o("button"),f=_(i),g=h(),p(n,"class","tab-item"),X(n,"active",l[1]===l[5].code),this.first=n},m(v,O){c(v,n,O),a(n,f),a(n,g),d||(b=Xe(n,"click",k),d=!0)},p(v,O){l=v,O&4&&i!==(i=l[5].code+"")&&pe(f,i),O&6&&X(n,"active",l[1]===l[5].code)},d(v){v&&r(n),d=!1,b()}}}function Ve(s,l){let n,i,f,g;return i=new je({props:{content:l[5].body}}),{key:s,first:null,c(){n=o("div"),K(i.$$.fragment),f=h(),p(n,"class","tab-item"),X(n,"active",l[1]===l[5].code),this.first=n},m(d,b){c(d,n,b),z(i,n,null),a(n,f),g=!0},p(d,b){l=d;const k={};b&4&&(k.content=l[5].body),i.$set(k),(!g||b&6)&&X(n,"active",l[1]===l[5].code)},i(d){g||(J(i.$$.fragment,d),g=!0)},o(d){j(i.$$.fragment,d),g=!1},d(d){d&&r(n),Z(i)}}}function Ye(s){let l,n,i=s[0].name+"",f,g,d,b,k,v,O,R,G,A,x,be,E,P,me,Y,N=s[0].name+"",ee,fe,te,M,ae,W,le,U,ne,y,oe,ge,B,S,se,_e,ie,ke,m,ve,C,we,$e,Oe,re,Ae,ce,ye,Se,Te,de,Ce,qe,q,ue,F,he,T,L,$=[],De=new Map,Re,H,w=[],Pe=new Map,D;v=new Ne({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${s[3]}');
|
||||
|
||||
...
|
||||
|
||||
// OAuth2 authentication with a single realtime call.
|
||||
//
|
||||
// Make sure to register ${s[3]}/api/oauth2-redirect as redirect url.
|
||||
const authData = await pb.collection('${s[0].name}').authWithOAuth2({ provider: 'google' });
|
||||
|
||||
// OR authenticate with manual OAuth2 code exchange
|
||||
// const authData = await pb.collection('${s[0].name}').authWithOAuth2Code(...);
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.record.id);
|
||||
|
||||
// "logout"
|
||||
pb.authStore.clear();
|
||||
`,dart:`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
final pb = PocketBase('${s[3]}');
|
||||
|
||||
...
|
||||
|
||||
// OAuth2 authentication with a single realtime call.
|
||||
//
|
||||
// Make sure to register ${s[3]}/api/oauth2-redirect as redirect url.
|
||||
final authData = await pb.collection('${s[0].name}').authWithOAuth2('google', (url) async {
|
||||
await launchUrl(url);
|
||||
});
|
||||
|
||||
// OR authenticate with manual OAuth2 code exchange
|
||||
// final authData = await pb.collection('${s[0].name}').authWithOAuth2Code(...);
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
print(pb.authStore.isValid);
|
||||
print(pb.authStore.token);
|
||||
print(pb.authStore.record.id);
|
||||
|
||||
// "logout"
|
||||
pb.authStore.clear();
|
||||
`}}),C=new je({props:{content:"?expand=relField1,relField2.subRelField"}}),q=new Ge({props:{prefix:"record."}});let I=Q(s[2]);const Me=e=>e[5].code;for(let e=0;e<I.length;e+=1){let t=Le(s,I,e),u=Me(t);De.set(u,$[e]=He(u,t))}let V=Q(s[2]);const We=e=>e[5].code;for(let e=0;e<V.length;e+=1){let t=Fe(s,V,e),u=We(t);Pe.set(u,w[e]=Ve(u,t))}return{c(){l=o("h3"),n=_("Auth with OAuth2 ("),f=_(i),g=_(")"),d=h(),b=o("div"),b.innerHTML=`<p>Authenticate with an OAuth2 provider and returns a new auth token and record data.</p> <p>For more details please check the
|
||||
<a href="https://pocketbase.io/docs/authentication#authenticate-with-oauth2" target="_blank" rel="noopener noreferrer">OAuth2 integration documentation
|
||||
</a>.</p>`,k=h(),K(v.$$.fragment),O=h(),R=o("h6"),R.textContent="API details",G=h(),A=o("div"),x=o("strong"),x.textContent="POST",be=h(),E=o("div"),P=o("p"),me=_("/api/collections/"),Y=o("strong"),ee=_(N),fe=_("/auth-with-oauth2"),te=h(),M=o("div"),M.textContent="Body Parameters",ae=h(),W=o("table"),W.innerHTML=`<thead><tr><th>Param</th> <th>Type</th> <th width="50%">Description</th></tr></thead> <tbody><tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>provider</span></div></td> <td><span class="label">String</span></td> <td>The name of the OAuth2 client provider (eg. "google").</td></tr> <tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>code</span></div></td> <td><span class="label">String</span></td> <td>The authorization code returned from the initial request.</td></tr> <tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>codeVerifier</span></div></td> <td><span class="label">String</span></td> <td>The code verifier sent with the initial request as part of the code_challenge.</td></tr> <tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>redirectURL</span></div></td> <td><span class="label">String</span></td> <td>The redirect url sent with the initial request.</td></tr> <tr><td><div class="inline-flex"><span class="label label-warning">Optional</span> <span>createData</span></div></td> <td><span class="label">Object</span></td> <td><p>Optional data that will be used when creating the auth record on OAuth2 sign-up.</p> <p>The created auth record must comply with the same requirements and validations in the
|
||||
regular <strong>create</strong> action.
|
||||
<br/> <em>The data can only be in <code>json</code>, aka. <code>multipart/form-data</code> and files
|
||||
upload currently are not supported during OAuth2 sign-ups.</em></p></td></tr></tbody>`,le=h(),U=o("div"),U.textContent="Query parameters",ne=h(),y=o("table"),oe=o("thead"),oe.innerHTML='<tr><th>Param</th> <th>Type</th> <th width="60%">Description</th></tr>',ge=h(),B=o("tbody"),S=o("tr"),se=o("td"),se.textContent="expand",_e=h(),ie=o("td"),ie.innerHTML='<span class="label">String</span>',ke=h(),m=o("td"),ve=_(`Auto expand record relations. Ex.:
|
||||
`),K(C.$$.fragment),we=_(`
|
||||
Supports up to 6-levels depth nested relations expansion. `),$e=o("br"),Oe=_(`
|
||||
The expanded relations will be appended to the record under the
|
||||
`),re=o("code"),re.textContent="expand",Ae=_(" property (eg. "),ce=o("code"),ce.textContent='"expand": {"relField1": {...}, ...}',ye=_(`).
|
||||
`),Se=o("br"),Te=_(`
|
||||
Only the relations to which the request user has permissions to `),de=o("strong"),de.textContent="view",Ce=_(" will be expanded."),qe=h(),K(q.$$.fragment),ue=h(),F=o("div"),F.textContent="Responses",he=h(),T=o("div"),L=o("div");for(let e=0;e<$.length;e+=1)$[e].c();Re=h(),H=o("div");for(let e=0;e<w.length;e+=1)w[e].c();p(l,"class","m-b-sm"),p(b,"class","content txt-lg m-b-sm"),p(R,"class","m-b-xs"),p(x,"class","label label-primary"),p(E,"class","content"),p(A,"class","alert alert-success"),p(M,"class","section-title"),p(W,"class","table-compact table-border m-b-base"),p(U,"class","section-title"),p(y,"class","table-compact table-border m-b-base"),p(F,"class","section-title"),p(L,"class","tabs-header compact combined left"),p(H,"class","tabs-content"),p(T,"class","tabs")},m(e,t){c(e,l,t),a(l,n),a(l,f),a(l,g),c(e,d,t),c(e,b,t),c(e,k,t),z(v,e,t),c(e,O,t),c(e,R,t),c(e,G,t),c(e,A,t),a(A,x),a(A,be),a(A,E),a(E,P),a(P,me),a(P,Y),a(Y,ee),a(P,fe),c(e,te,t),c(e,M,t),c(e,ae,t),c(e,W,t),c(e,le,t),c(e,U,t),c(e,ne,t),c(e,y,t),a(y,oe),a(y,ge),a(y,B),a(B,S),a(S,se),a(S,_e),a(S,ie),a(S,ke),a(S,m),a(m,ve),z(C,m,null),a(m,we),a(m,$e),a(m,Oe),a(m,re),a(m,Ae),a(m,ce),a(m,ye),a(m,Se),a(m,Te),a(m,de),a(m,Ce),a(B,qe),z(q,B,null),c(e,ue,t),c(e,F,t),c(e,he,t),c(e,T,t),a(T,L);for(let u=0;u<$.length;u+=1)$[u]&&$[u].m(L,null);a(T,Re),a(T,H);for(let u=0;u<w.length;u+=1)w[u]&&w[u].m(H,null);D=!0},p(e,[t]){(!D||t&1)&&i!==(i=e[0].name+"")&&pe(f,i);const u={};t&9&&(u.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${e[3]}');
|
||||
|
||||
...
|
||||
|
||||
// OAuth2 authentication with a single realtime call.
|
||||
//
|
||||
// Make sure to register ${e[3]}/api/oauth2-redirect as redirect url.
|
||||
const authData = await pb.collection('${e[0].name}').authWithOAuth2({ provider: 'google' });
|
||||
|
||||
// OR authenticate with manual OAuth2 code exchange
|
||||
// const authData = await pb.collection('${e[0].name}').authWithOAuth2Code(...);
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.record.id);
|
||||
|
||||
// "logout"
|
||||
pb.authStore.clear();
|
||||
`),t&9&&(u.dart=`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
final pb = PocketBase('${e[3]}');
|
||||
|
||||
...
|
||||
|
||||
// OAuth2 authentication with a single realtime call.
|
||||
//
|
||||
// Make sure to register ${e[3]}/api/oauth2-redirect as redirect url.
|
||||
final authData = await pb.collection('${e[0].name}').authWithOAuth2('google', (url) async {
|
||||
await launchUrl(url);
|
||||
});
|
||||
|
||||
// OR authenticate with manual OAuth2 code exchange
|
||||
// final authData = await pb.collection('${e[0].name}').authWithOAuth2Code(...);
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
print(pb.authStore.isValid);
|
||||
print(pb.authStore.token);
|
||||
print(pb.authStore.record.id);
|
||||
|
||||
// "logout"
|
||||
pb.authStore.clear();
|
||||
`),v.$set(u),(!D||t&1)&&N!==(N=e[0].name+"")&&pe(ee,N),t&6&&(I=Q(e[2]),$=Ue($,t,Me,1,e,I,De,L,Ie,He,null,Le)),t&6&&(V=Q(e[2]),Qe(),w=Ue(w,t,We,1,e,V,Pe,H,Ze,Ve,null,Fe),ze())},i(e){if(!D){J(v.$$.fragment,e),J(C.$$.fragment,e),J(q.$$.fragment,e);for(let t=0;t<V.length;t+=1)J(w[t]);D=!0}},o(e){j(v.$$.fragment,e),j(C.$$.fragment,e),j(q.$$.fragment,e);for(let t=0;t<w.length;t+=1)j(w[t]);D=!1},d(e){e&&(r(l),r(d),r(b),r(k),r(O),r(R),r(G),r(A),r(te),r(M),r(ae),r(W),r(le),r(U),r(ne),r(y),r(ue),r(F),r(he),r(T)),Z(v,e),Z(C),Z(q);for(let t=0;t<$.length;t+=1)$[t].d();for(let t=0;t<w.length;t+=1)w[t].d()}}}function et(s,l,n){let i,{collection:f}=l,g=200,d=[];const b=k=>n(1,g=k.code);return s.$$set=k=>{"collection"in k&&n(0,f=k.collection)},s.$$.update=()=>{s.$$.dirty&1&&n(2,d=[{code:200,body:JSON.stringify({token:"JWT_AUTH_TOKEN",record:Be.dummyCollectionRecord(f),meta:{id:"abc123",name:"John Doe",username:"john.doe",email:"test@example.com",avatarURL:"https://example.com/avatar.png",accessToken:"...",refreshToken:"...",expiry:"2022-01-01 10:00:00.123Z",isNew:!1,rawUser:{}}},null,2)},{code:400,body:`
|
||||
{
|
||||
"status": 400,
|
||||
"message": "An error occurred while submitting the form.",
|
||||
"data": {
|
||||
"provider": {
|
||||
"code": "validation_required",
|
||||
"message": "Missing required value."
|
||||
}
|
||||
}
|
||||
}
|
||||
`}])},n(3,i=Be.getApiExampleUrl(Ke.baseURL)),[f,g,d,i,b]}class lt extends Je{constructor(l){super(),xe(this,l,et,Ye,Ee,{collection:0})}}export{lt as default};
|
||||
-136
@@ -1,136 +0,0 @@
|
||||
import{S as be,i as _e,s as ve,W as ge,X as V,h as b,d as x,t as j,a as J,I as ce,Z as de,_ as je,C as ue,$ as Qe,D as he,l as _,n as s,m as ee,u as d,v as T,A as R,c as te,w as g,J as ke,k as N,o as $e,V as Ke,Y as De,p as Xe,a0 as Me}from"./index-DMlPjiFP.js";import{F as Ze}from"./FieldsQueryParam-B36GF025.js";function Be(a,t,e){const l=a.slice();return l[4]=t[e],l}function Ie(a,t,e){const l=a.slice();return l[4]=t[e],l}function We(a,t){let e,l=t[4].code+"",h,i,c,n;function m(){return t[3](t[4])}return{key:a,first:null,c(){e=d("button"),h=R(l),i=T(),g(e,"class","tab-item"),N(e,"active",t[1]===t[4].code),this.first=e},m(v,C){_(v,e,C),s(e,h),s(e,i),c||(n=$e(e,"click",m),c=!0)},p(v,C){t=v,C&4&&l!==(l=t[4].code+"")&&ce(h,l),C&6&&N(e,"active",t[1]===t[4].code)},d(v){v&&b(e),c=!1,n()}}}function Fe(a,t){let e,l,h,i;return l=new ge({props:{content:t[4].body}}),{key:a,first:null,c(){e=d("div"),te(l.$$.fragment),h=T(),g(e,"class","tab-item"),N(e,"active",t[1]===t[4].code),this.first=e},m(c,n){_(c,e,n),ee(l,e,null),s(e,h),i=!0},p(c,n){t=c;const m={};n&4&&(m.content=t[4].body),l.$set(m),(!i||n&6)&&N(e,"active",t[1]===t[4].code)},i(c){i||(J(l.$$.fragment,c),i=!0)},o(c){j(l.$$.fragment,c),i=!1},d(c){c&&b(e),x(l)}}}function ze(a){let t,e,l,h,i,c,n,m=a[0].name+"",v,C,F,B,I,D,Q,M,U,y,O,q,k,L,Y,A,X,E,o,$,P,z,u,p,S,w,Z,we,Te,Pe,pe,Oe,ye,le,fe,oe,me,G,ae,K=[],Se=new Map,qe,ne,H=[],Ce=new Map,se;P=new ge({props:{content:"?expand=relField1,relField2.subRelField"}}),le=new Ze({props:{prefix:"record."}});let re=V(a[2]);const Ae=r=>r[4].code;for(let r=0;r<re.length;r+=1){let f=Ie(a,re,r),W=Ae(f);Se.set(W,K[r]=We(W,f))}let ie=V(a[2]);const Re=r=>r[4].code;for(let r=0;r<ie.length;r+=1){let f=Be(a,ie,r),W=Re(f);Ce.set(W,H[r]=Fe(W,f))}return{c(){t=d("div"),e=d("strong"),e.textContent="POST",l=T(),h=d("div"),i=d("p"),c=R("/api/collections/"),n=d("strong"),v=R(m),C=R("/auth-with-otp"),F=T(),B=d("div"),B.textContent="Body Parameters",I=T(),D=d("table"),D.innerHTML='<thead><tr><th>Param</th> <th>Type</th> <th width="50%">Description</th></tr></thead> <tbody><tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>otpId</span></div></td> <td><span class="label">String</span></td> <td>The id of the OTP request.</td></tr> <tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>password</span></div></td> <td><span class="label">String</span></td> <td>The one-time password.</td></tr></tbody>',Q=T(),M=d("div"),M.textContent="Query parameters",U=T(),y=d("table"),O=d("thead"),O.innerHTML='<tr><th>Param</th> <th>Type</th> <th width="60%">Description</th></tr>',q=T(),k=d("tbody"),L=d("tr"),Y=d("td"),Y.textContent="expand",A=T(),X=d("td"),X.innerHTML='<span class="label">String</span>',E=T(),o=d("td"),$=R(`Auto expand record relations. Ex.:
|
||||
`),te(P.$$.fragment),z=R(`
|
||||
Supports up to 6-levels depth nested relations expansion. `),u=d("br"),p=R(`
|
||||
The expanded relations will be appended to the record under the
|
||||
`),S=d("code"),S.textContent="expand",w=R(" property (eg. "),Z=d("code"),Z.textContent='"expand": {"relField1": {...}, ...}',we=R(`).
|
||||
`),Te=d("br"),Pe=R(`
|
||||
Only the relations to which the request user has permissions to `),pe=d("strong"),pe.textContent="view",Oe=R(" will be expanded."),ye=T(),te(le.$$.fragment),fe=T(),oe=d("div"),oe.textContent="Responses",me=T(),G=d("div"),ae=d("div");for(let r=0;r<K.length;r+=1)K[r].c();qe=T(),ne=d("div");for(let r=0;r<H.length;r+=1)H[r].c();g(e,"class","label label-primary"),g(h,"class","content"),g(t,"class","alert alert-success"),g(B,"class","section-title"),g(D,"class","table-compact table-border m-b-base"),g(M,"class","section-title"),g(y,"class","table-compact table-border m-b-base"),g(oe,"class","section-title"),g(ae,"class","tabs-header compact combined left"),g(ne,"class","tabs-content"),g(G,"class","tabs")},m(r,f){_(r,t,f),s(t,e),s(t,l),s(t,h),s(h,i),s(i,c),s(i,n),s(n,v),s(i,C),_(r,F,f),_(r,B,f),_(r,I,f),_(r,D,f),_(r,Q,f),_(r,M,f),_(r,U,f),_(r,y,f),s(y,O),s(y,q),s(y,k),s(k,L),s(L,Y),s(L,A),s(L,X),s(L,E),s(L,o),s(o,$),ee(P,o,null),s(o,z),s(o,u),s(o,p),s(o,S),s(o,w),s(o,Z),s(o,we),s(o,Te),s(o,Pe),s(o,pe),s(o,Oe),s(k,ye),ee(le,k,null),_(r,fe,f),_(r,oe,f),_(r,me,f),_(r,G,f),s(G,ae);for(let W=0;W<K.length;W+=1)K[W]&&K[W].m(ae,null);s(G,qe),s(G,ne);for(let W=0;W<H.length;W+=1)H[W]&&H[W].m(ne,null);se=!0},p(r,[f]){(!se||f&1)&&m!==(m=r[0].name+"")&&ce(v,m),f&6&&(re=V(r[2]),K=de(K,f,Ae,1,r,re,Se,ae,je,We,null,Ie)),f&6&&(ie=V(r[2]),ue(),H=de(H,f,Re,1,r,ie,Ce,ne,Qe,Fe,null,Be),he())},i(r){if(!se){J(P.$$.fragment,r),J(le.$$.fragment,r);for(let f=0;f<ie.length;f+=1)J(H[f]);se=!0}},o(r){j(P.$$.fragment,r),j(le.$$.fragment,r);for(let f=0;f<H.length;f+=1)j(H[f]);se=!1},d(r){r&&(b(t),b(F),b(B),b(I),b(D),b(Q),b(M),b(U),b(y),b(fe),b(oe),b(me),b(G)),x(P),x(le);for(let f=0;f<K.length;f+=1)K[f].d();for(let f=0;f<H.length;f+=1)H[f].d()}}}function Ge(a,t,e){let{collection:l}=t,h=200,i=[];const c=n=>e(1,h=n.code);return a.$$set=n=>{"collection"in n&&e(0,l=n.collection)},a.$$.update=()=>{a.$$.dirty&1&&e(2,i=[{code:200,body:JSON.stringify({token:"JWT_TOKEN",record:ke.dummyCollectionRecord(l)},null,2)},{code:400,body:`
|
||||
{
|
||||
"status": 400,
|
||||
"message": "Failed to authenticate.",
|
||||
"data": {
|
||||
"otpId": {
|
||||
"code": "validation_required",
|
||||
"message": "Missing required value."
|
||||
}
|
||||
}
|
||||
}
|
||||
`}])},[l,h,i,c]}class xe extends be{constructor(t){super(),_e(this,t,Ge,ze,ve,{collection:0})}}function Ue(a,t,e){const l=a.slice();return l[4]=t[e],l}function He(a,t,e){const l=a.slice();return l[4]=t[e],l}function Le(a,t){let e,l=t[4].code+"",h,i,c,n;function m(){return t[3](t[4])}return{key:a,first:null,c(){e=d("button"),h=R(l),i=T(),g(e,"class","tab-item"),N(e,"active",t[1]===t[4].code),this.first=e},m(v,C){_(v,e,C),s(e,h),s(e,i),c||(n=$e(e,"click",m),c=!0)},p(v,C){t=v,C&4&&l!==(l=t[4].code+"")&&ce(h,l),C&6&&N(e,"active",t[1]===t[4].code)},d(v){v&&b(e),c=!1,n()}}}function Ye(a,t){let e,l,h,i;return l=new ge({props:{content:t[4].body}}),{key:a,first:null,c(){e=d("div"),te(l.$$.fragment),h=T(),g(e,"class","tab-item"),N(e,"active",t[1]===t[4].code),this.first=e},m(c,n){_(c,e,n),ee(l,e,null),s(e,h),i=!0},p(c,n){t=c;const m={};n&4&&(m.content=t[4].body),l.$set(m),(!i||n&6)&&N(e,"active",t[1]===t[4].code)},i(c){i||(J(l.$$.fragment,c),i=!0)},o(c){j(l.$$.fragment,c),i=!1},d(c){c&&b(e),x(l)}}}function et(a){let t,e,l,h,i,c,n,m=a[0].name+"",v,C,F,B,I,D,Q,M,U,y,O,q=[],k=new Map,L,Y,A=[],X=new Map,E,o=V(a[2]);const $=u=>u[4].code;for(let u=0;u<o.length;u+=1){let p=He(a,o,u),S=$(p);k.set(S,q[u]=Le(S,p))}let P=V(a[2]);const z=u=>u[4].code;for(let u=0;u<P.length;u+=1){let p=Ue(a,P,u),S=z(p);X.set(S,A[u]=Ye(S,p))}return{c(){t=d("div"),e=d("strong"),e.textContent="POST",l=T(),h=d("div"),i=d("p"),c=R("/api/collections/"),n=d("strong"),v=R(m),C=R("/request-otp"),F=T(),B=d("div"),B.textContent="Body Parameters",I=T(),D=d("table"),D.innerHTML='<thead><tr><th>Param</th> <th>Type</th> <th width="50%">Description</th></tr></thead> <tbody><tr><td><div class="inline-flex"><span class="label label-success">Required</span> <span>email</span></div></td> <td><span class="label">String</span></td> <td>The auth record email address to send the OTP request (if exists).</td></tr></tbody>',Q=T(),M=d("div"),M.textContent="Responses",U=T(),y=d("div"),O=d("div");for(let u=0;u<q.length;u+=1)q[u].c();L=T(),Y=d("div");for(let u=0;u<A.length;u+=1)A[u].c();g(e,"class","label label-primary"),g(h,"class","content"),g(t,"class","alert alert-success"),g(B,"class","section-title"),g(D,"class","table-compact table-border m-b-base"),g(M,"class","section-title"),g(O,"class","tabs-header compact combined left"),g(Y,"class","tabs-content"),g(y,"class","tabs")},m(u,p){_(u,t,p),s(t,e),s(t,l),s(t,h),s(h,i),s(i,c),s(i,n),s(n,v),s(i,C),_(u,F,p),_(u,B,p),_(u,I,p),_(u,D,p),_(u,Q,p),_(u,M,p),_(u,U,p),_(u,y,p),s(y,O);for(let S=0;S<q.length;S+=1)q[S]&&q[S].m(O,null);s(y,L),s(y,Y);for(let S=0;S<A.length;S+=1)A[S]&&A[S].m(Y,null);E=!0},p(u,[p]){(!E||p&1)&&m!==(m=u[0].name+"")&&ce(v,m),p&6&&(o=V(u[2]),q=de(q,p,$,1,u,o,k,O,je,Le,null,He)),p&6&&(P=V(u[2]),ue(),A=de(A,p,z,1,u,P,X,Y,Qe,Ye,null,Ue),he())},i(u){if(!E){for(let p=0;p<P.length;p+=1)J(A[p]);E=!0}},o(u){for(let p=0;p<A.length;p+=1)j(A[p]);E=!1},d(u){u&&(b(t),b(F),b(B),b(I),b(D),b(Q),b(M),b(U),b(y));for(let p=0;p<q.length;p+=1)q[p].d();for(let p=0;p<A.length;p+=1)A[p].d()}}}function tt(a,t,e){let{collection:l}=t,h=200,i=[];const c=n=>e(1,h=n.code);return a.$$set=n=>{"collection"in n&&e(0,l=n.collection)},e(2,i=[{code:200,body:JSON.stringify({otpId:ke.randomString(15)},null,2)},{code:400,body:`
|
||||
{
|
||||
"status": 400,
|
||||
"message": "An error occurred while validating the submitted data.",
|
||||
"data": {
|
||||
"email": {
|
||||
"code": "validation_is_email",
|
||||
"message": "Must be a valid email address."
|
||||
}
|
||||
}
|
||||
}
|
||||
`},{code:429,body:`
|
||||
{
|
||||
"status": 429,
|
||||
"message": "You've send too many OTP requests, please try again later.",
|
||||
"data": {}
|
||||
}
|
||||
`}]),[l,h,i,c]}class lt extends be{constructor(t){super(),_e(this,t,tt,et,ve,{collection:0})}}function Ve(a,t,e){const l=a.slice();return l[5]=t[e],l[7]=e,l}function Je(a,t,e){const l=a.slice();return l[5]=t[e],l[7]=e,l}function Ne(a){let t,e,l,h,i;function c(){return a[4](a[7])}return{c(){t=d("button"),e=d("div"),e.textContent=`${a[5].title}`,l=T(),g(e,"class","txt"),g(t,"class","tab-item"),N(t,"active",a[1]==a[7])},m(n,m){_(n,t,m),s(t,e),s(t,l),h||(i=$e(t,"click",c),h=!0)},p(n,m){a=n,m&2&&N(t,"active",a[1]==a[7])},d(n){n&&b(t),h=!1,i()}}}function Ee(a){let t,e,l,h;var i=a[5].component;function c(n,m){return{props:{collection:n[0]}}}return i&&(e=Me(i,c(a))),{c(){t=d("div"),e&&te(e.$$.fragment),l=T(),g(t,"class","tab-item"),N(t,"active",a[1]==a[7])},m(n,m){_(n,t,m),e&&ee(e,t,null),s(t,l),h=!0},p(n,m){if(i!==(i=n[5].component)){if(e){ue();const v=e;j(v.$$.fragment,1,0,()=>{x(v,1)}),he()}i?(e=Me(i,c(n)),te(e.$$.fragment),J(e.$$.fragment,1),ee(e,t,l)):e=null}else if(i){const v={};m&1&&(v.collection=n[0]),e.$set(v)}(!h||m&2)&&N(t,"active",n[1]==n[7])},i(n){h||(e&&J(e.$$.fragment,n),h=!0)},o(n){e&&j(e.$$.fragment,n),h=!1},d(n){n&&b(t),e&&x(e)}}}function ot(a){var Y,A,X,E;let t,e,l=a[0].name+"",h,i,c,n,m,v,C,F,B,I,D,Q,M,U;v=new Ke({props:{js:`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${a[2]}');
|
||||
|
||||
...
|
||||
|
||||
// send OTP email to the provided auth record
|
||||
const req = await pb.collection('${(Y=a[0])==null?void 0:Y.name}').requestOTP('test@example.com');
|
||||
|
||||
// ... show a screen/popup to enter the password from the email ...
|
||||
|
||||
// authenticate with the requested OTP id and the email password
|
||||
const authData = await pb.collection('${(A=a[0])==null?void 0:A.name}').authWithOTP(
|
||||
req.otpId,
|
||||
"YOUR_OTP",
|
||||
);
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.record.id);
|
||||
|
||||
// "logout"
|
||||
pb.authStore.clear();
|
||||
`,dart:`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${a[2]}');
|
||||
|
||||
...
|
||||
|
||||
// send OTP email to the provided auth record
|
||||
final req = await pb.collection('${(X=a[0])==null?void 0:X.name}').requestOTP('test@example.com');
|
||||
|
||||
// ... show a screen/popup to enter the password from the email ...
|
||||
|
||||
// authenticate with the requested OTP id and the email password
|
||||
final authData = await pb.collection('${(E=a[0])==null?void 0:E.name}').authWithOTP(
|
||||
req.otpId,
|
||||
"YOUR_OTP",
|
||||
);
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
print(pb.authStore.isValid);
|
||||
print(pb.authStore.token);
|
||||
print(pb.authStore.record.id);
|
||||
|
||||
// "logout"
|
||||
pb.authStore.clear();
|
||||
`}});let y=V(a[3]),O=[];for(let o=0;o<y.length;o+=1)O[o]=Ne(Je(a,y,o));let q=V(a[3]),k=[];for(let o=0;o<q.length;o+=1)k[o]=Ee(Ve(a,q,o));const L=o=>j(k[o],1,1,()=>{k[o]=null});return{c(){t=d("h3"),e=R("Auth with OTP ("),h=R(l),i=R(")"),c=T(),n=d("div"),n.innerHTML=`<p>Authenticate with an one-time password (OTP).</p> <p>Note that when requesting an OTP we return an <code>otpId</code> even if a user with the provided email
|
||||
doesn't exist as a very basic enumeration protection.</p>`,m=T(),te(v.$$.fragment),C=T(),F=d("h6"),F.textContent="API details",B=T(),I=d("div"),D=d("div");for(let o=0;o<O.length;o+=1)O[o].c();Q=T(),M=d("div");for(let o=0;o<k.length;o+=1)k[o].c();g(t,"class","m-b-sm"),g(n,"class","content txt-lg m-b-sm"),g(F,"class","m-b-xs"),g(D,"class","tabs-header compact"),g(M,"class","tabs-content"),g(I,"class","tabs")},m(o,$){_(o,t,$),s(t,e),s(t,h),s(t,i),_(o,c,$),_(o,n,$),_(o,m,$),ee(v,o,$),_(o,C,$),_(o,F,$),_(o,B,$),_(o,I,$),s(I,D);for(let P=0;P<O.length;P+=1)O[P]&&O[P].m(D,null);s(I,Q),s(I,M);for(let P=0;P<k.length;P+=1)k[P]&&k[P].m(M,null);U=!0},p(o,[$]){var z,u,p,S;(!U||$&1)&&l!==(l=o[0].name+"")&&ce(h,l);const P={};if($&5&&(P.js=`
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
const pb = new PocketBase('${o[2]}');
|
||||
|
||||
...
|
||||
|
||||
// send OTP email to the provided auth record
|
||||
const req = await pb.collection('${(z=o[0])==null?void 0:z.name}').requestOTP('test@example.com');
|
||||
|
||||
// ... show a screen/popup to enter the password from the email ...
|
||||
|
||||
// authenticate with the requested OTP id and the email password
|
||||
const authData = await pb.collection('${(u=o[0])==null?void 0:u.name}').authWithOTP(
|
||||
req.otpId,
|
||||
"YOUR_OTP",
|
||||
);
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
console.log(pb.authStore.isValid);
|
||||
console.log(pb.authStore.token);
|
||||
console.log(pb.authStore.record.id);
|
||||
|
||||
// "logout"
|
||||
pb.authStore.clear();
|
||||
`),$&5&&(P.dart=`
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
final pb = PocketBase('${o[2]}');
|
||||
|
||||
...
|
||||
|
||||
// send OTP email to the provided auth record
|
||||
final req = await pb.collection('${(p=o[0])==null?void 0:p.name}').requestOTP('test@example.com');
|
||||
|
||||
// ... show a screen/popup to enter the password from the email ...
|
||||
|
||||
// authenticate with the requested OTP id and the email password
|
||||
final authData = await pb.collection('${(S=o[0])==null?void 0:S.name}').authWithOTP(
|
||||
req.otpId,
|
||||
"YOUR_OTP",
|
||||
);
|
||||
|
||||
// after the above you can also access the auth data from the authStore
|
||||
print(pb.authStore.isValid);
|
||||
print(pb.authStore.token);
|
||||
print(pb.authStore.record.id);
|
||||
|
||||
// "logout"
|
||||
pb.authStore.clear();
|
||||
`),v.$set(P),$&10){y=V(o[3]);let w;for(w=0;w<y.length;w+=1){const Z=Je(o,y,w);O[w]?O[w].p(Z,$):(O[w]=Ne(Z),O[w].c(),O[w].m(D,null))}for(;w<O.length;w+=1)O[w].d(1);O.length=y.length}if($&11){q=V(o[3]);let w;for(w=0;w<q.length;w+=1){const Z=Ve(o,q,w);k[w]?(k[w].p(Z,$),J(k[w],1)):(k[w]=Ee(Z),k[w].c(),J(k[w],1),k[w].m(M,null))}for(ue(),w=q.length;w<k.length;w+=1)L(w);he()}},i(o){if(!U){J(v.$$.fragment,o);for(let $=0;$<q.length;$+=1)J(k[$]);U=!0}},o(o){j(v.$$.fragment,o),k=k.filter(Boolean);for(let $=0;$<k.length;$+=1)j(k[$]);U=!1},d(o){o&&(b(t),b(c),b(n),b(m),b(C),b(F),b(B),b(I)),x(v,o),De(O,o),De(k,o)}}}function at(a,t,e){let l,{collection:h}=t;const i=[{title:"OTP Request",component:lt},{title:"OTP Auth",component:xe}];let c=0;const n=m=>e(1,c=m);return a.$$set=m=>{"collection"in m&&e(0,h=m.collection)},e(2,l=ke.getApiExampleUrl(Xe.baseURL)),[h,c,l,i,n]}class it extends be{constructor(t){super(),_e(this,t,at,ot,ve,{collection:0})}}export{it as default};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user