From 66fef0ae3e5f452b064dc14332bf5897788af656 Mon Sep 17 00:00:00 2001 From: Alfonso Cantos Date: Fri, 19 Dec 2025 13:29:08 +0100 Subject: [PATCH 1/7] feat: apikey used to connect database --- Makefile | 2 +- api/0_buildv2.go | 69 +++++++++++++++++++++++++++++++++ api/0_helpers.go | 5 +++ api/0_interceptors.go | 27 +++++++++++++ api/acceptance_test.go | 4 +- api/auth_test.go | 70 ++++++++++++++++++++++++++++++++++ bootstrap/bootstrap.go | 3 +- configuration/configuration.go | 2 + 8 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 api/0_buildv2.go create mode 100644 api/auth_test.go diff --git a/Makefile b/Makefile index c478ccc..7cf2495 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ FLAGS = -ldflags "\ " test: - go test -cover ./... + go test -v ./... run: STATICS=statics/www/ go run $(FLAGS) ./cmd/inceptiondb/... diff --git a/api/0_buildv2.go b/api/0_buildv2.go new file mode 100644 index 0000000..14c157b --- /dev/null +++ b/api/0_buildv2.go @@ -0,0 +1,69 @@ +package api + +import ( + "net/http" + + "github.com/fulldump/box" + "github.com/fulldump/box/boxopenapi" + + "github.com/fulldump/inceptiondb/api/apicollectionv1" + "github.com/fulldump/inceptiondb/service" +) + +func BuildV2(s service.Servicer, staticsDir, version string, apiKey, apiSecret string) *box.B { // TODO: remove datadir + + b := box.NewBox() + + v2 := b.Resource("/v2") + v2.WithInterceptors( + box.SetResponseHeader("Content-Type", "application/json"), + Authenticate(apiKey, apiSecret), + ) + + apicollectionv1.BuildV1Collection(v2, s). + WithInterceptors( + injectServicer(s), + ) + + b.Resource("/v2/*"). + WithActions(box.AnyMethod(func(w http.ResponseWriter) interface{} { + w.WriteHeader(http.StatusNotImplemented) + return PrettyError{ + Message: "not implemented", + Description: "this endpoint does not exist, please check the documentation", + } + })) + + b.Resource("/release"). + WithActions(box.Get(func() string { + return version + })) + + spec := boxopenapi.Spec(b) + spec.Info.Title = "InceptionDB" + spec.Info.Description = "A durable in-memory database to store JSON documents." + spec.Info.Contact = &boxopenapi.Contact{ + Url: "https://github.com/fulldump/inceptiondb/issues/new", + } + b.Handle("GET", "/openapi.json", func(r *http.Request) any { + + spec.Servers = []boxopenapi.Server{ + { + Url: "https://" + r.Host, + }, + { + Url: "http://" + r.Host, + }, + } + + return spec + }) + + // // Mount statics + // b.Resource("/*"). + // WithActions( + // box.Get(statics.ServeStatics(staticsDir)).WithName("serveStatics"), + // ) + + return b +} diff --git a/api/0_helpers.go b/api/0_helpers.go index a313a0f..afcc163 100644 --- a/api/0_helpers.go +++ b/api/0_helpers.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" "github.com/fulldump/box" @@ -50,6 +51,10 @@ func (p PrettyError) MarshalJSON() ([]byte, error) { }) } +func (p PrettyError) MarshalTo(w io.Writer) error { + return json.NewEncoder(w).Encode(p) +} + func InterceptorUnavailable(db *database.Database) box.I { return func(next box.H) box.H { return func(ctx context.Context) { diff --git a/api/0_interceptors.go b/api/0_interceptors.go index 9a9fb7c..9fd209c 100644 --- a/api/0_interceptors.go +++ b/api/0_interceptors.go @@ -12,6 +12,33 @@ import ( "github.com/fulldump/box" ) +func Authenticate(apiKey, apiSecret string) box.I { + return func(next box.H) box.H { + return func(ctx context.Context) { + + if apiKey == "" && apiSecret == "" { + next(ctx) + return + } + + r := box.GetRequest(ctx) + key := r.Header.Get("X-Api-Key") + secret := r.Header.Get("X-Api-Secret") + + if key != apiKey || secret != apiSecret { + w := box.GetResponse(ctx) + w.WriteHeader(http.StatusUnauthorized) + PrettyError{ + Message: "Unauthorized", + Description: "Invalid X-Api-Key or X-Api-Secret", + }.MarshalTo(w) + return + } + next(ctx) + } + } +} + func RecoverFromPanic(next box.H) box.H { return func(ctx context.Context) { defer func() { diff --git a/api/acceptance_test.go b/api/acceptance_test.go index 76e2311..be56dc6 100644 --- a/api/acceptance_test.go +++ b/api/acceptance_test.go @@ -23,7 +23,7 @@ func TestAcceptance(t *testing.T) { s := service.NewService(db) - b := Build(s, "", "test") + b := BuildV2(s, "", "test", "", "") b.WithInterceptors( InterceptorUnavailable(db), RecoverFromPanic, @@ -33,7 +33,7 @@ func TestAcceptance(t *testing.T) { api := apitest.NewWithHandler(b) service.Acceptance(a, func(method, path string) *apitest.Request { - return api.Request(method, "/v1"+path) + return api.Request(method, "/v2"+path) }) }) diff --git a/api/auth_test.go b/api/auth_test.go new file mode 100644 index 0000000..68df57c --- /dev/null +++ b/api/auth_test.go @@ -0,0 +1,70 @@ +package api + +import ( + "net/http" + "testing" + + "github.com/fulldump/apitest" + "github.com/fulldump/biff" + + "github.com/fulldump/inceptiondb/database" + "github.com/fulldump/inceptiondb/service" +) + +func TestAuthentication(t *testing.T) { + + biff.Alternative("Authentication", func(a *biff.A) { + + db := database.NewDatabase(&database.Config{ + Dir: t.TempDir(), + }) + + s := service.NewService(db) + + apiKey := "my-key" + apiSecret := "my-secret" + + b := BuildV2(s, "", "test", apiKey, apiSecret) + b.WithInterceptors( + PrettyErrorInterceptor, + ) + + api := apitest.NewWithHandler(b) + + a.Alternative("Missing headers", func(a *biff.A) { + resp := api.Request("GET", "/v2/collections").Do() + biff.AssertEqual(resp.StatusCode, http.StatusUnauthorized) + biff.AssertEqualJson(resp.BodyJson(), map[string]any{ + "error": map[string]any{ + "message": "Unauthorized", + "description": "Invalid X-Api-Key or X-Api-Secret", + }, + }) + }) + + a.Alternative("Wrong Key", func(a *biff.A) { + resp := api.Request("GET", "/v2/collections"). + WithHeader("X-Api-Key", "wrong-key"). + WithHeader("X-Api-Secret", apiSecret). + Do() + biff.AssertEqual(resp.StatusCode, http.StatusUnauthorized) + }) + + a.Alternative("Wrong Secret", func(a *biff.A) { + resp := api.Request("GET", "/v2/collections"). + WithHeader("X-Api-Key", apiKey). + WithHeader("X-Api-Secret", "wrong-secret"). + Do() + biff.AssertEqual(resp.StatusCode, http.StatusUnauthorized) + }) + + a.Alternative("Correct credentials", func(a *biff.A) { + resp := api.Request("GET", "/v2/collections"). + WithHeader("X-Api-Key", apiKey). + WithHeader("X-Api-Secret", apiSecret). + Do() + biff.AssertEqual(resp.StatusCode, http.StatusOK) + }) + + }) +} diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 3dbd859..0878282 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -28,7 +28,8 @@ func Bootstrap(c *configuration.Configuration) (start, stop func()) { Dir: c.Dir, }) - b := api.Build(service.NewService(db), c.Statics, VERSION) + // b := api.Build(service.NewService(db), c.Statics, VERSION) + b := api.BuildV2(service.NewService(db), c.Statics, VERSION, c.ApiKey, c.ApiSecret) if c.EnableCompression { b.WithInterceptors(api.Compression) } diff --git a/configuration/configuration.go b/configuration/configuration.go index 9032ca9..9753541 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -10,4 +10,6 @@ type Configuration struct { ShowBanner bool `usage:"show big banner"` ShowConfig bool `usage:"print config"` EnableCompression bool `usage:"enable http compression (gzip)"` + ApiKey string `usage:"API Key for v2 authentication"` + ApiSecret string `usage:"API Secret for v2 authentication"` } From 3bc6c7d981165bb76a77a192d3306efe850b1fbd Mon Sep 17 00:00:00 2001 From: Alfonso Cantos Date: Fri, 19 Dec 2025 13:35:13 +0100 Subject: [PATCH 2/7] warning, if no credentials has been set --- cmd/inceptiondb/main.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/inceptiondb/main.go b/cmd/inceptiondb/main.go index 5119084..79eba9b 100644 --- a/cmd/inceptiondb/main.go +++ b/cmd/inceptiondb/main.go @@ -42,6 +42,11 @@ func main() { e.Encode(c) } + if c.ApiKey == "" || c.ApiSecret == "" { + fmt.Println("WARNING: ApiKey and ApiSecret are not set, authentication will be disabled") + fmt.Println(" -------------------------------") + } + start, _ := bootstrap.Bootstrap(c) start() } From eae05cdb995faef1320f5d2b80a185492053afd7 Mon Sep 17 00:00:00 2001 From: Alfonso Cantos Date: Fri, 19 Dec 2025 14:05:58 +0100 Subject: [PATCH 3/7] retrocompatibility and hide UI by config --- Makefile | 2 +- api/0_build.go | 24 ++++++++---- api/0_buildv2.go | 69 ---------------------------------- api/acceptance_test.go | 4 +- api/auth_test.go | 10 ++--- bootstrap/bootstrap.go | 2 +- configuration/configuration.go | 1 + 7 files changed, 27 insertions(+), 85 deletions(-) delete mode 100644 api/0_buildv2.go diff --git a/Makefile b/Makefile index 7cf2495..c5763c4 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ FLAGS = -ldflags "\ " test: - go test -v ./... + go test ./... run: STATICS=statics/www/ go run $(FLAGS) ./cmd/inceptiondb/... diff --git a/api/0_build.go b/api/0_build.go index d238bba..f50038d 100644 --- a/api/0_build.go +++ b/api/0_build.go @@ -12,12 +12,20 @@ import ( "github.com/fulldump/inceptiondb/statics" ) -func Build(s service.Servicer, staticsDir, version string) *box.B { // TODO: remove datadir +func Build(s service.Servicer, staticsDir, version, apiKey, apiSecret string, hideUI bool) *box.B { // TODO: remove datadir b := box.NewBox() v1 := b.Resource("/v1") - v1.WithInterceptors(box.SetResponseHeader("Content-Type", "application/json")) + v1.WithInterceptors( + box.SetResponseHeader("Content-Type", "application/json"), + ) + + if apiKey != "" && apiSecret != "" { + v1.WithInterceptors( + Authenticate(apiKey, apiSecret), + ) + } apicollectionv1.BuildV1Collection(v1, s). WithInterceptors( @@ -58,11 +66,13 @@ func Build(s service.Servicer, staticsDir, version string) *box.B { // TODO: rem return spec }) - // Mount statics - b.Resource("/*"). - WithActions( - box.Get(statics.ServeStatics(staticsDir)).WithName("serveStatics"), - ) + if !hideUI { + // Mount statics + b.Resource("/*"). + WithActions( + box.Get(statics.ServeStatics(staticsDir)).WithName("serveStatics"), + ) + } return b } diff --git a/api/0_buildv2.go b/api/0_buildv2.go deleted file mode 100644 index 14c157b..0000000 --- a/api/0_buildv2.go +++ /dev/null @@ -1,69 +0,0 @@ -package api - -import ( - "net/http" - - "github.com/fulldump/box" - "github.com/fulldump/box/boxopenapi" - - "github.com/fulldump/inceptiondb/api/apicollectionv1" - "github.com/fulldump/inceptiondb/service" -) - -func BuildV2(s service.Servicer, staticsDir, version string, apiKey, apiSecret string) *box.B { // TODO: remove datadir - - b := box.NewBox() - - v2 := b.Resource("/v2") - v2.WithInterceptors( - box.SetResponseHeader("Content-Type", "application/json"), - Authenticate(apiKey, apiSecret), - ) - - apicollectionv1.BuildV1Collection(v2, s). - WithInterceptors( - injectServicer(s), - ) - - b.Resource("/v2/*"). - WithActions(box.AnyMethod(func(w http.ResponseWriter) interface{} { - w.WriteHeader(http.StatusNotImplemented) - return PrettyError{ - Message: "not implemented", - Description: "this endpoint does not exist, please check the documentation", - } - })) - - b.Resource("/release"). - WithActions(box.Get(func() string { - return version - })) - - spec := boxopenapi.Spec(b) - spec.Info.Title = "InceptionDB" - spec.Info.Description = "A durable in-memory database to store JSON documents." - spec.Info.Contact = &boxopenapi.Contact{ - Url: "https://github.com/fulldump/inceptiondb/issues/new", - } - b.Handle("GET", "/openapi.json", func(r *http.Request) any { - - spec.Servers = []boxopenapi.Server{ - { - Url: "https://" + r.Host, - }, - { - Url: "http://" + r.Host, - }, - } - - return spec - }) - - // // Mount statics - // b.Resource("/*"). - // WithActions( - // box.Get(statics.ServeStatics(staticsDir)).WithName("serveStatics"), - // ) - - return b -} diff --git a/api/acceptance_test.go b/api/acceptance_test.go index be56dc6..c4a8dd0 100644 --- a/api/acceptance_test.go +++ b/api/acceptance_test.go @@ -23,7 +23,7 @@ func TestAcceptance(t *testing.T) { s := service.NewService(db) - b := BuildV2(s, "", "test", "", "") + b := Build(s, "", "test", "", "", false) b.WithInterceptors( InterceptorUnavailable(db), RecoverFromPanic, @@ -33,7 +33,7 @@ func TestAcceptance(t *testing.T) { api := apitest.NewWithHandler(b) service.Acceptance(a, func(method, path string) *apitest.Request { - return api.Request(method, "/v2"+path) + return api.Request(method, "/v1"+path) }) }) diff --git a/api/auth_test.go b/api/auth_test.go index 68df57c..b3a5d2c 100644 --- a/api/auth_test.go +++ b/api/auth_test.go @@ -24,7 +24,7 @@ func TestAuthentication(t *testing.T) { apiKey := "my-key" apiSecret := "my-secret" - b := BuildV2(s, "", "test", apiKey, apiSecret) + b := Build(s, "", "test", apiKey, apiSecret, false) b.WithInterceptors( PrettyErrorInterceptor, ) @@ -32,7 +32,7 @@ func TestAuthentication(t *testing.T) { api := apitest.NewWithHandler(b) a.Alternative("Missing headers", func(a *biff.A) { - resp := api.Request("GET", "/v2/collections").Do() + resp := api.Request("GET", "/v1/collections").Do() biff.AssertEqual(resp.StatusCode, http.StatusUnauthorized) biff.AssertEqualJson(resp.BodyJson(), map[string]any{ "error": map[string]any{ @@ -43,7 +43,7 @@ func TestAuthentication(t *testing.T) { }) a.Alternative("Wrong Key", func(a *biff.A) { - resp := api.Request("GET", "/v2/collections"). + resp := api.Request("GET", "/v1/collections"). WithHeader("X-Api-Key", "wrong-key"). WithHeader("X-Api-Secret", apiSecret). Do() @@ -51,7 +51,7 @@ func TestAuthentication(t *testing.T) { }) a.Alternative("Wrong Secret", func(a *biff.A) { - resp := api.Request("GET", "/v2/collections"). + resp := api.Request("GET", "/v1/collections"). WithHeader("X-Api-Key", apiKey). WithHeader("X-Api-Secret", "wrong-secret"). Do() @@ -59,7 +59,7 @@ func TestAuthentication(t *testing.T) { }) a.Alternative("Correct credentials", func(a *biff.A) { - resp := api.Request("GET", "/v2/collections"). + resp := api.Request("GET", "/v1/collections"). WithHeader("X-Api-Key", apiKey). WithHeader("X-Api-Secret", apiSecret). Do() diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 0878282..b9fe9da 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -29,7 +29,7 @@ func Bootstrap(c *configuration.Configuration) (start, stop func()) { }) // b := api.Build(service.NewService(db), c.Statics, VERSION) - b := api.BuildV2(service.NewService(db), c.Statics, VERSION, c.ApiKey, c.ApiSecret) + b := api.Build(service.NewService(db), c.Statics, VERSION, c.ApiKey, c.ApiSecret, c.HideUI) if c.EnableCompression { b.WithInterceptors(api.Compression) } diff --git a/configuration/configuration.go b/configuration/configuration.go index 9753541..503b16a 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -12,4 +12,5 @@ type Configuration struct { EnableCompression bool `usage:"enable http compression (gzip)"` ApiKey string `usage:"API Key for v2 authentication"` ApiSecret string `usage:"API Secret for v2 authentication"` + HideUI bool `usage:"do not serve UI"` } From 3f32cca10eed0e907a2c58368f4993f4c238243a Mon Sep 17 00:00:00 2001 From: Alfonso Cantos Date: Sat, 20 Dec 2025 12:33:19 +0100 Subject: [PATCH 4/7] feat: let the user set api-key and secret in the UI --- statics/www/index.html | 7743 ++++++++++++++++++++-------------------- 1 file changed, 3838 insertions(+), 3905 deletions(-) diff --git a/statics/www/index.html b/statics/www/index.html index eb989ae..ff50e31 100644 --- a/statics/www/index.html +++ b/statics/www/index.html @@ -1,24 +1,29 @@ + InceptionDB Console - + - + - + - + - - + + + -
- -
-
-
-

{{ selectedActivityEntry.label || 'Request details' }}

-

- {{ selectedActivityEntry.method }} -   - {{ selectedActivityEntry.url }} -  (Status {{ selectedActivityEntry.statusCode }}) -

+
+ +
+
+
+

{{ + selectedActivityEntry.label || 'Request details' }}

+

+ {{ selectedActivityEntry.method + }} +   + {{ selectedActivityEntry.url }} +  (Status {{ selectedActivityEntry.statusCode }}) +

+
-
-
-

Request headers

-
    -
  • - {{ header.key }}: - {{ header.value }} -
  • -
-
-
-

Request body

-
{{ activityRequestBody(selectedActivityEntry) }}
-
-
-

Request formats

-
-
-
+
+

Request body

+
{{ activityRequestBody(selectedActivityEntry) }}
+
+
+

Request formats

+
+
+ + : 'text-slate-400 hover:text-slate-200 hover:bg-slate-900/40'" @click="selectedActivityRequestTab = tab.id"> + {{ tab.label }} + +
+
{{ activeActivityRequestTabContent }}
-
{{ activeActivityRequestTabContent }}
+
+
+ Started {{ formatActivityTime(selectedActivityEntry.startedAt) }} + Duration {{ + formatDuration(selectedActivityEntry.durationMs) }} +
+
+ +
-
- Started {{ formatActivityTime(selectedActivityEntry.startedAt) }} - Duration {{ formatDuration(selectedActivityEntry.durationMs) }} -
-
- - -
-
-
-
-
+ + +
+
+

Authentication Required

+

This database requires an API Key and API Secret to continue.

-
-

API status

-
- - - {{ connectionStatusLabel }} - - + +
+
+ +
-

- {{ connectionStatusDescription }} -

-
- -
-
- - +
+ +
-
+ +
+ -

{{ createForm.error }}

- -
-
-
Loading collections…
-
{{ collectionsError }}
- -
- - -
-
-

Welcome to InceptionDB

-

Select or create a collection in the sidebar to start querying, inserting, or deleting documents.

+
- -
-
-
-

Collection {{ selectedCollection.name }}

-

- Total: {{ selectedCollection.total }} - Last query: {{ queryStats.elapsed }} - Documents listed: {{ queryStats.returned }} +

+ +
+ + +
+
+

Welcome to InceptionDB

+

Select or create a collection in the sidebar to start querying, + inserting, or deleting documents.

+
-
- - +
+
+
+

Collection {{ selectedCollection.name }} +

+

+ Total: {{ selectedCollection.total + }} + Last query: {{ queryStats.elapsed }} + Documents listed: {{ queryStats.returned }} +

+
+
+ + +
+
+ +
+
+
+

Quick document lookup

+

Find a document by ID without changing your current + filters.

- -
- - +
+ + + +
+
+

{{ quickSearch.error }}

+
+
+
+

Document {{ quickSearch.result.id }} +

+

Source: {{ + quickSearchSourceLabel }}

+
+
+ +
+
{{ quickSearchResultText }}
+
+

+ No document with ID "{{ quickSearch.lastCheckedId }}" was found. +

+
-
+
+
+
- From (inclusive) -
- -
+ + +

{{ filterError }}

+
- To (exclusive) -
- -
+ +
- -
-
+
+ + +
-
-
-
+
-

- Results - -  🛈 - -

-
-
-
- - + From + (inclusive) +
+
-
- +
+
+ To + (exclusive) +
+
-

{{ exportState.error }}

-

{{ exportState.progress }}

+
+
-
-
Running query…
-
{{ queryError }}
-
No documents found.
-
-
-
- -
- skip - + +
+
+
+
+

+ Results + +  🛈 + +

+
+
+
+ +
-
- limit - +
+
- +

{{ + exportState.error }}

+

{{ + exportState.progress }}

-

{{ pageInfo }}

-
-
-
-
-

Document #{{ offset + idx + 1 }}

-

ID: {{ documentId(row) }}

+
+
+
Running query…
+
{{ queryError }}
+
No documents + found.
+
+
+
+ +
+ skip +
-
- - +
+ limit +
+
-
- - +

{{ pageInfo }}

+
+
+
+
+
+

Document #{{ + offset + idx + 1 }}

+

+ ID: {{ documentId(row) }}

+
+
+ + +
+
+
+ + +

+ Set a field to null to remove it. +

+
+ + +
+

+ {{ editingDocuments[documentId(row)].error }} +

+

+ {{ editingDocuments[documentId(row)].success }} +

+
+
{{ formatDocument(row) }}
+

+ The document must include an "id" field to enable editing or + deletion. +

+
+
+
+
+ + + + + + + + + + + + + + + +
# + + {{ column }} + + Actions
{{ + offset + idx + 1 }} + {{ + formatTableValue(row, column) }} + +
+ + + +
+
+
+
+
+

+ Edit document {{ editing.id }}

+

Document #{{ editing.position + }}

+
+

- Set a field to null to remove it. + Set a field to null to + remove it.

- -
-

- {{ editingDocuments[documentId(row)].error }} +

+ {{ editing.state.error }}

-

- {{ editingDocuments[documentId(row)].success }} +

+ {{ editing.state.success }}

-
{{ formatDocument(row) }}
-

- The document must include an "id" field to enable editing or deletion. -

-
-
-
- - - - - - - - - - - - - - - -
# - {{ column }} - Actions
{{ offset + idx + 1 }} - {{ formatTableValue(row, column) }} - -
- - - -
-
-
-
-
-

Edit document {{ editing.id }}

-

Document #{{ editing.position }}

-
- -

- Set a field to null to remove it. -

-
- -
-

- {{ editing.state.error }} -

-

- {{ editing.state.success }} -

-
-
-
-
- -
- skip - -
-
- limit - -
- +

{{ pageInfo }}

-

{{ pageInfo }}

-
-
-
- -
-
-

Estimated storage usage returned by the size endpoint.

- -
-

- Select a collection to inspect its metrics. -

-

Loading metrics…

-

{{ sizeMetrics.error }}

-
-

- Updated at {{ sizeMetricsUpdatedLabel }} +

+
+ +
+
+

Estimated storage usage returned by the size + endpoint.

+ +
+

+ Select a collection to inspect its metrics.

-
-
-
{{ entry.label }}
-
{{ entry.value }}
-
-
-
-

No metrics reported for this collection.

-

Metrics will appear after refreshing.

-
-
- -
- -
-
-

- Documents missing fields will receive these values when inserted. +

Loading metrics…

- -
-
-

Special values

-
    -
  • - uuid() - Generate a unique identifier. -
  • -
  • - unixnano() - Insert the current timestamp in nanoseconds. -
  • -
  • - auto() - Assign an auto-incrementing number within the collection. -
  • -
-
- -
- - -
-

{{ defaultsForm.error }}

-

{{ defaultsForm.success }}

-
-
- -
- -
- -
- +

{{ sizeMetrics.error + }}

+
+

+ Updated at {{ sizeMetricsUpdatedLabel }} +

+
+
+
{{ entry.label }}
+
{{ + entry.value }}
+
+
+
+

No metrics reported + for this collection.

+

Metrics will appear after refreshing.

-

{{ insertForm.error }}

-

{{ insertForm.success }}

-
-
- -
-

- Upload a CSV file to insert multiple documents at once. The first row should contain the field names. -

-
-
- - +
+ +
+
+

+ Documents missing fields will receive these values when inserted. +

+
-
- - +
+

Special + values

+
    +
  • + uuid() + Generate a unique identifier. +
  • +
  • + unixnano() + Insert the current timestamp in nanoseconds. +
  • +
  • + auto() + Assign an auto-incrementing number within the collection. +
  • +
-
- - + +
+ +
+

{{ defaultsForm.error }} +

+

{{ + defaultsForm.success }}

-

Selected file: {{ csvImportForm.fileName }}

-
- - -
-

{{ csvImportForm.error }}

-

{{ csvImportForm.success }}

-

{{ csvImportForm.progress }}

-
-
- -
-
- -
-

{{ indexMessages.error }}

-

{{ indexMessages.success }}

-
-
- - -
-
- - +
+ +
+ +
+
-
+

{{ insertForm.error }}

+

{{ + insertForm.success }}

+
+
+ +
+ +
+

+ Upload a CSV file to insert multiple documents at once. The first row should + contain the field names. +

+
+
+ + +
- - + +
- -
-
- - -

- Separate fields with commas. Prefix with "-" for descending order. -

+ +
- -
+

Selected file: {{ + csvImportForm.fileName }}

- -
- -
Loading indexes…
-
This collection has no indexes yet.
-
    -
  • -
    -
    -
    - {{ index.name }} - - {{ index.type === 'btree' ? 'B-Tree' : 'Map' }} - -
    -

    Field: {{ index.field }}

    -

    Fields: {{ Array.isArray(index.fields) ? index.fields.join(', ') : '' }}

    -
    - Unique - Sparse -
    +

    {{ csvImportForm.error + }}

    +

    {{ + csvImportForm.success }}

    +

    {{ + csvImportForm.progress }}

    +
    +
    + +
    + +
    +
    + +
    +

    {{ indexMessages.error + }}

    +

    {{ + indexMessages.success }}

    +
    +
    + + +
    +
    + + +
    +
    +
    + +
    -
    +
    +
    + + +

    + Separate fields with commas. Prefix with "-" for descending order. +

    +
    + + +
    +
    + +
    -
  • -
+ +
Loading indexes…
+
This collection + has no indexes yet.
+
    +
  • +
    +
    +
    + {{ index.name }} + + {{ index.type === 'btree' ? 'B-Tree' : 'Map' }} + +
    +

    Field: + {{ index.field }}

    +

    Fields: {{ + Array.isArray(index.fields) ? index.fields.join(', ') : '' }} +

    +
    + Unique + Sparse +
    +
    + +
    +
  • +
+
-
-
-
-
-
-
+
- - - - + return { + // state + collections, + collectionsLoading, + collectionsError, + selectedCollection, + selectedCollectionName, + indexes, + indexesLoading, + filterText, + filterError, + queryRows, + queryLoading, + queryError, + queryStats, + resultsViewMode, + selectedIndexName, + activeIndex, + mapValue, + reverse, + rangeFrom, + rangeTo, + editingDocuments, + tableColumns, + openEditingRows, + indexForm, + indexMessages, + insertForm, + csvImportForm, + defaultsForm, + defaultsHelpOpen, + collapsibleCards, + createForm, + connectionStatus, + activityLog, + activityDetailOpen, + selectedActivityEntry, + exportState, + sizeMetrics, + quickSearch, + quickSearchResultText, + quickSearchSourceLabel, + sizeMetricsEntries, + sizeMetricsUpdatedLabel, + connectionStatusLabel, + connectionStatusDescription, + connectionStatusBadgeClass, + connectionStatusDotClass, + connectionStatusButtonLabel, + connectionStatusChecking, + disablePrev, + disableNext, + skip, + limit, + offset, + pageInfo, + selectResultsView, + // methods + prettyTotal, + isSelected, + toggleCreateForm, + toggleCard, + toggleIndexForm, + selectCollection, + runQuery, + lookupDocument, + applyQuickSearchResult, + exportResults, + nextPage, + prevPage, + commitSkip, + commitLimit, + documentId, + canEditDocument, + isEditingRow, + openEditRow, + closeEditRow, + saveEditRow, + onEditDraftInput, + createIndex, + removeIndex, + resetDefaultsForm, + saveDefaults, + insertDocument, + onCsvFileChange, + resetCsvImportForm, + importCsv, + createCollection, + dropCollection, + deleteRow, + canDeleteRow, + formatDocument, + formatTableValue, + refreshConnectionStatus, + refreshSizeMetrics, + formatActivityTime, + formatDuration, + activityStatusLabel, + activityStatusBadgeClass, + clearActivityLog, + activityMarkerClass, + activityMarkerTitle, + activityRequestHeaders, + activityRequestBody, + activityRequestTabEntries, + selectedActivityRequestTab, + activeActivityRequestTabContent, + buildCurlCommand, + activityModal, + modalMode, + openActivityDetail, + closeActivityDetail, + closeActivityModal, + authModal, + authForm, + openAuthModal, + closeAuthModal, + saveAuth, + }; + }, + }).mount('#app'); + + - + + \ No newline at end of file From ac8216cf9649ab7f94c8b24d24cf8637c655a301 Mon Sep 17 00:00:00 2001 From: Alfonso Cantos Date: Sat, 20 Dec 2025 12:33:40 +0100 Subject: [PATCH 5/7] fix: log no api-key has been set --- cmd/inceptiondb/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/inceptiondb/main.go b/cmd/inceptiondb/main.go index 79eba9b..53a0489 100644 --- a/cmd/inceptiondb/main.go +++ b/cmd/inceptiondb/main.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "fmt" + "log" "os" "github.com/fulldump/goconfig" @@ -43,8 +44,7 @@ func main() { } if c.ApiKey == "" || c.ApiSecret == "" { - fmt.Println("WARNING: ApiKey and ApiSecret are not set, authentication will be disabled") - fmt.Println(" -------------------------------") + log.Println("ApiKey and ApiSecret are not set, authentication will be disabled") } start, _ := bootstrap.Bootstrap(c) From 5f8c0362613b82d1b7a125809e302a0497d0d46b Mon Sep 17 00:00:00 2001 From: Alfonso Cantos Date: Sat, 20 Dec 2025 12:34:14 +0100 Subject: [PATCH 6/7] fix: do not marshal error here. --- api/0_build.go | 7 ++++--- api/0_interceptors.go | 7 +------ 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/api/0_build.go b/api/0_build.go index f50038d..d72e69b 100644 --- a/api/0_build.go +++ b/api/0_build.go @@ -2,6 +2,7 @@ package api import ( "context" + "errors" "net/http" "github.com/fulldump/box" @@ -12,14 +13,14 @@ import ( "github.com/fulldump/inceptiondb/statics" ) +var ErrUnauthorized = errors.New("unauthorized") + func Build(s service.Servicer, staticsDir, version, apiKey, apiSecret string, hideUI bool) *box.B { // TODO: remove datadir b := box.NewBox() v1 := b.Resource("/v1") - v1.WithInterceptors( - box.SetResponseHeader("Content-Type", "application/json"), - ) + v1.WithInterceptors(box.SetResponseHeader("Content-Type", "application/json")) if apiKey != "" && apiSecret != "" { v1.WithInterceptors( diff --git a/api/0_interceptors.go b/api/0_interceptors.go index 9fd209c..a7103cd 100644 --- a/api/0_interceptors.go +++ b/api/0_interceptors.go @@ -26,12 +26,7 @@ func Authenticate(apiKey, apiSecret string) box.I { secret := r.Header.Get("X-Api-Secret") if key != apiKey || secret != apiSecret { - w := box.GetResponse(ctx) - w.WriteHeader(http.StatusUnauthorized) - PrettyError{ - Message: "Unauthorized", - Description: "Invalid X-Api-Key or X-Api-Secret", - }.MarshalTo(w) + box.SetError(ctx, ErrUnauthorized) return } next(ctx) From f157305b1ae70b239a4a85bd2fead6ba386c5d92 Mon Sep 17 00:00:00 2001 From: Alfonso Cantos Date: Sat, 20 Dec 2025 13:16:58 +0100 Subject: [PATCH 7/7] fix: marshal Unauthrized error --- api/0_build.go | 3 --- api/0_helpers.go | 11 +++++++++++ api/0_interceptors.go | 3 +++ api/auth_test.go | 4 ++-- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/api/0_build.go b/api/0_build.go index d72e69b..961a05a 100644 --- a/api/0_build.go +++ b/api/0_build.go @@ -2,7 +2,6 @@ package api import ( "context" - "errors" "net/http" "github.com/fulldump/box" @@ -13,8 +12,6 @@ import ( "github.com/fulldump/inceptiondb/statics" ) -var ErrUnauthorized = errors.New("unauthorized") - func Build(s service.Servicer, staticsDir, version, apiKey, apiSecret string, hideUI bool) *box.B { // TODO: remove datadir b := box.NewBox() diff --git a/api/0_helpers.go b/api/0_helpers.go index afcc163..b4dad9c 100644 --- a/api/0_helpers.go +++ b/api/0_helpers.go @@ -84,6 +84,17 @@ func PrettyErrorInterceptor(next box.H) box.H { } w := box.GetResponse(ctx) + if err == ErrUnauthorized { + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]interface{}{ + "error": map[string]interface{}{ + "message": err.Error(), + "description": fmt.Sprintf("user is not authenticated"), + }, + }) + return + } + if err == box.ErrResourceNotFound { w.WriteHeader(http.StatusNotFound) json.NewEncoder(w).Encode(map[string]interface{}{ diff --git a/api/0_interceptors.go b/api/0_interceptors.go index a7103cd..c14d2fb 100644 --- a/api/0_interceptors.go +++ b/api/0_interceptors.go @@ -2,6 +2,7 @@ package api import ( "context" + "errors" "fmt" "log" "net/http" @@ -12,6 +13,8 @@ import ( "github.com/fulldump/box" ) +var ErrUnauthorized = errors.New("unauthorized") + func Authenticate(apiKey, apiSecret string) box.I { return func(next box.H) box.H { return func(ctx context.Context) { diff --git a/api/auth_test.go b/api/auth_test.go index b3a5d2c..da09803 100644 --- a/api/auth_test.go +++ b/api/auth_test.go @@ -36,8 +36,8 @@ func TestAuthentication(t *testing.T) { biff.AssertEqual(resp.StatusCode, http.StatusUnauthorized) biff.AssertEqualJson(resp.BodyJson(), map[string]any{ "error": map[string]any{ - "message": "Unauthorized", - "description": "Invalid X-Api-Key or X-Api-Secret", + "message": "unauthorized", + "description": "user is not authenticated", }, }) })