diff --git a/internal/api/config_settings.go b/internal/api/config_settings.go index 4e11c66c7..6f03d0bc2 100644 --- a/internal/api/config_settings.go +++ b/internal/api/config_settings.go @@ -50,7 +50,7 @@ func GetSettings(router *gin.RouterGroup) { // @Produce json // @Success 200 {object} customize.Settings // @Failure 400,401,403,404,500 {object} i18n.Response -// @Param settings body customize.Settings true "user settings" +// @Param settings body customize.Settings true "user settings" // @Router /api/v1/settings [post] func SaveSettings(router *gin.RouterGroup) { router.POST("/settings", func(c *gin.Context) { diff --git a/internal/api/echo.go b/internal/api/echo.go index 43f3f2460..9bae3524f 100644 --- a/internal/api/echo.go +++ b/internal/api/echo.go @@ -10,21 +10,22 @@ import ( // Echo returns the request and response headers as JSON if debug mode is enabled. // -// The supported request methods are: -// -// - GET -// - POST -// - PUT -// - PATCH -// - HEAD -// - OPTIONS -// - DELETE -// - CONNECT -// - TRACE -// -// ANY /api/v1/echo +// @Summary returns the request and response headers as JSON if debug mode is enabled +// @Id Echo +// @Success 200 +// @Router /api/v1/echo [get] func Echo(router *gin.RouterGroup) { - router.Any("/echo", func(c *gin.Context) { + methods := []string{ + http.MethodGet, + http.MethodHead, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + http.MethodConnect, + http.MethodTrace, + } + router.Match(methods, "/echo", func(c *gin.Context) { // Abort if debug mode is disabled. if !get.Config().Debug() { AbortFeatureDisabled(c) diff --git a/internal/api/markers.go b/internal/api/markers.go index 636408bef..5ed32e904 100644 --- a/internal/api/markers.go +++ b/internal/api/markers.go @@ -75,7 +75,7 @@ func findFileMarker(c *gin.Context) (file *entity.File, marker *entity.Marker, e // // See internal/form/marker.go for the values required to create a new marker. // -// @Tags Files +// @Tags Files // @Router /api/v1/markers [post] func CreateMarker(router *gin.RouterGroup) { router.POST("/markers", func(c *gin.Context) { diff --git a/internal/api/options.go b/internal/api/options.go new file mode 100644 index 000000000..33031e483 --- /dev/null +++ b/internal/api/options.go @@ -0,0 +1,19 @@ +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// Options returns an empty response to handle CORS preflight requests. +// +// @Summary returns CORS headers with an empty response body +// @Id Options +// @Success 204 +// @Router /api/v1/{any} [options] +func Options(router *gin.RouterGroup) { + router.OPTIONS("/*any", func(c *gin.Context) { + c.Status(http.StatusNoContent) + }) +} diff --git a/internal/api/swagger.json b/internal/api/swagger.json index dcf7c1f57..973a1ebfb 100644 --- a/internal/api/swagger.json +++ b/internal/api/swagger.json @@ -1714,6 +1714,17 @@ } } }, + "/api/v1/echo": { + "get": { + "summary": "returns the request and response headers as JSON if debug mode is enabled", + "operationId": "Echo", + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/api/v1/errors": { "get": { "produces": [ @@ -5270,6 +5281,17 @@ "responses": {} } }, + "/api/v1/{any}": { + "options": { + "summary": "returns CORS headers with an empty response body", + "operationId": "Options", + "responses": { + "204": { + "description": "No Content" + } + } + } + }, "/api/v1/{entity}/{uid}/links": { "post": { "tags": [ @@ -7560,19 +7582,24 @@ "type": "object", "properties": { "drawing": { - "type": "number" + "type": "number", + "format": "float32" }, "hentai": { - "type": "number" + "type": "number", + "format": "float32" }, "neutral": { - "type": "number" + "type": "number", + "format": "float32" }, "porn": { - "type": "number" + "type": "number", + "format": "float32" }, "sexy": { - "type": "number" + "type": "number", + "format": "float32" } } }, @@ -8223,9 +8250,138 @@ } } }, - "time.Duration": { + "tensorflow.ColorChannelOrder": { "type": "integer", "enum": [ + 0 + ], + "x-enum-varnames": [ + "UndefinedOrder" + ] + }, + "tensorflow.Interval": { + "type": "object", + "properties": { + "end": { + "type": "number" + }, + "mean": { + "type": "number" + }, + "start": { + "type": "number" + }, + "stdDev": { + "type": "number" + } + } + }, + "tensorflow.ModelInfo": { + "type": "object", + "properties": { + "input": { + "$ref": "#/definitions/tensorflow.PhotoInput" + }, + "output": { + "$ref": "#/definitions/tensorflow.ModelOutput" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "tensorflow.ModelOutput": { + "type": "object", + "properties": { + "index": { + "type": "integer" + }, + "logits": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "outputs": { + "type": "integer" + } + } + }, + "tensorflow.PhotoInput": { + "type": "object", + "properties": { + "height": { + "type": "integer" + }, + "index": { + "type": "integer" + }, + "inputOrder": { + "$ref": "#/definitions/tensorflow.ColorChannelOrder" + }, + "intervals": { + "type": "array", + "items": { + "$ref": "#/definitions/tensorflow.Interval" + } + }, + "name": { + "type": "string" + }, + "resizeOperation": { + "$ref": "#/definitions/tensorflow.ResizeOperation" + }, + "width": { + "type": "integer" + } + } + }, + "tensorflow.ResizeOperation": { + "type": "integer", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-varnames": [ + "UndefinedResizeOperation", + "ResizeBreakAspectRatio", + "CenterCrop", + "Padding" + ] + }, + "time.Duration": { + "type": "integer", + "format": "int64", + "enum": [ + -9223372036854775808, + 9223372036854775807, + 1, + 1000, + 1000000, + 1000000000, + 60000000000, + 3600000000000, + -9223372036854775808, + 9223372036854775807, + 1, + 1000, + 1000000, + 1000000000, + 60000000000, + 3600000000000, + -9223372036854775808, + 9223372036854775807, + 1, + 1000, + 1000000, + 1000000000, + 60000000000, + 3600000000000, -9223372036854775808, 9223372036854775807, 1, @@ -8236,6 +8392,30 @@ 3600000000000 ], "x-enum-varnames": [ + "minDuration", + "maxDuration", + "Nanosecond", + "Microsecond", + "Millisecond", + "Second", + "Minute", + "Hour", + "minDuration", + "maxDuration", + "Nanosecond", + "Microsecond", + "Millisecond", + "Second", + "Minute", + "Hour", + "minDuration", + "maxDuration", + "Nanosecond", + "Microsecond", + "Millisecond", + "Second", + "Minute", + "Hour", "minDuration", "maxDuration", "Nanosecond", @@ -8433,7 +8613,8 @@ "items": { "type": "array", "items": { - "type": "number" + "type": "number", + "format": "float64" } } } @@ -8498,9 +8679,15 @@ "Service": { "$ref": "#/definitions/vision.Service" }, + "default": { + "type": "boolean" + }, "disabled": { "type": "boolean" }, + "meta": { + "$ref": "#/definitions/tensorflow.ModelInfo" + }, "name": { "type": "string" }, diff --git a/internal/api/users_password.go b/internal/api/users_password.go index 95b708b70..b2b0f5400 100644 --- a/internal/api/users_password.go +++ b/internal/api/users_password.go @@ -19,7 +19,7 @@ import ( // UpdateUserPassword changes the password of the currently authenticated user. // // @Tags Users, Authentication -// @Router /api/v1/users/{uid}/password [put] +// @Router /api/v1/users/{uid}/password [put] func UpdateUserPassword(router *gin.RouterGroup) { router.PUT("/users/:uid/password", func(c *gin.Context) { conf := get.Config() diff --git a/internal/api/users_sessions.go b/internal/api/users_sessions.go index 4bf25b2fe..4d1e65a43 100644 --- a/internal/api/users_sessions.go +++ b/internal/api/users_sessions.go @@ -19,7 +19,7 @@ import ( // FindUserSessions finds user sessions and returns them as JSON. // // @Tags Users, Authentication -// @Router /api/v1/users/{uid}/sessions [get] +// @Router /api/v1/users/{uid}/sessions [get] func FindUserSessions(router *gin.RouterGroup) { router.GET("/users/:uid/sessions", func(c *gin.Context) { // Check if the session user is has user management privileges. diff --git a/internal/api/users_update.go b/internal/api/users_update.go index 2513ec2e1..b6dfe0e7d 100644 --- a/internal/api/users_update.go +++ b/internal/api/users_update.go @@ -19,7 +19,7 @@ import ( // UpdateUser updates the profile information of the currently authenticated user. // // @Tags Users -// @Router /api/v1/users/{uid} [put] +// @Router /api/v1/users/{uid} [put] func UpdateUser(router *gin.RouterGroup) { router.PUT("/users/:uid", func(c *gin.Context) { conf := get.Config() diff --git a/internal/api/users_upload.go b/internal/api/users_upload.go index 78ce387a6..58fbd8802 100644 --- a/internal/api/users_upload.go +++ b/internal/api/users_upload.go @@ -28,7 +28,7 @@ import ( // UploadUserFiles adds files to the user upload folder, from where they can be moved and indexed. // // @Tags Users, Files -// @Router /users/{uid}/upload/{token} [post] +// @Router /users/{uid}/upload/{token} [post] func UploadUserFiles(router *gin.RouterGroup) { router.POST("/users/:uid/upload/:token", func(c *gin.Context) { conf := get.Config() diff --git a/internal/server/api.go b/internal/server/api.go index bdab284c9..f3e11867c 100644 --- a/internal/server/api.go +++ b/internal/server/api.go @@ -24,11 +24,14 @@ var Api = func(conf *config.Config) gin.HandlerFunc { if origin := conf.CORSOrigin(); origin != "" { c.Header(header.AccessControlAllowOrigin, origin) - // Add additional information to preflight OPTION requests. + // Handle OPTIONS preflight requests by adding CORS headers + // and aborting the request with HTTP status code 204. if c.Request.Method == http.MethodOptions { c.Header(header.AccessControlAllowHeaders, conf.CORSHeaders()) c.Header(header.AccessControlAllowMethods, conf.CORSMethods()) c.Header(header.AccessControlMaxAge, header.DefaultAccessControlMaxAge) + c.AbortWithStatus(http.StatusNoContent) + return } } } diff --git a/internal/server/routes.go b/internal/server/routes.go index 0a1a46536..bb78f5601 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -202,5 +202,6 @@ func registerRoutes(router *gin.Engine, conf *config.Config) { api.Connect(APIv1) api.WebSocket(APIv1) api.GetMetrics(APIv1) + api.Options(APIv1) api.Echo(APIv1) }