mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
API: Add OPTIONS wildcard handler to serve CORS preflight requests #5133
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -50,7 +50,7 @@ func GetSettings(router *gin.RouterGroup) {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} customize.Settings
|
// @Success 200 {object} customize.Settings
|
||||||
// @Failure 400,401,403,404,500 {object} i18n.Response
|
// @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]
|
// @Router /api/v1/settings [post]
|
||||||
func SaveSettings(router *gin.RouterGroup) {
|
func SaveSettings(router *gin.RouterGroup) {
|
||||||
router.POST("/settings", func(c *gin.Context) {
|
router.POST("/settings", func(c *gin.Context) {
|
||||||
|
|||||||
@@ -10,21 +10,22 @@ import (
|
|||||||
|
|
||||||
// Echo returns the request and response headers as JSON if debug mode is enabled.
|
// Echo returns the request and response headers as JSON if debug mode is enabled.
|
||||||
//
|
//
|
||||||
// The supported request methods are:
|
// @Summary returns the request and response headers as JSON if debug mode is enabled
|
||||||
//
|
// @Id Echo
|
||||||
// - GET
|
// @Success 200
|
||||||
// - POST
|
// @Router /api/v1/echo [get]
|
||||||
// - PUT
|
|
||||||
// - PATCH
|
|
||||||
// - HEAD
|
|
||||||
// - OPTIONS
|
|
||||||
// - DELETE
|
|
||||||
// - CONNECT
|
|
||||||
// - TRACE
|
|
||||||
//
|
|
||||||
// ANY /api/v1/echo
|
|
||||||
func Echo(router *gin.RouterGroup) {
|
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.
|
// Abort if debug mode is disabled.
|
||||||
if !get.Config().Debug() {
|
if !get.Config().Debug() {
|
||||||
AbortFeatureDisabled(c)
|
AbortFeatureDisabled(c)
|
||||||
|
|||||||
@@ -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.
|
// See internal/form/marker.go for the values required to create a new marker.
|
||||||
//
|
//
|
||||||
// @Tags Files
|
// @Tags Files
|
||||||
// @Router /api/v1/markers [post]
|
// @Router /api/v1/markers [post]
|
||||||
func CreateMarker(router *gin.RouterGroup) {
|
func CreateMarker(router *gin.RouterGroup) {
|
||||||
router.POST("/markers", func(c *gin.Context) {
|
router.POST("/markers", func(c *gin.Context) {
|
||||||
|
|||||||
19
internal/api/options.go
Normal file
19
internal/api/options.go
Normal file
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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": {
|
"/api/v1/errors": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
@@ -5270,6 +5281,17 @@
|
|||||||
"responses": {}
|
"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": {
|
"/api/v1/{entity}/{uid}/links": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -7560,19 +7582,24 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"drawing": {
|
"drawing": {
|
||||||
"type": "number"
|
"type": "number",
|
||||||
|
"format": "float32"
|
||||||
},
|
},
|
||||||
"hentai": {
|
"hentai": {
|
||||||
"type": "number"
|
"type": "number",
|
||||||
|
"format": "float32"
|
||||||
},
|
},
|
||||||
"neutral": {
|
"neutral": {
|
||||||
"type": "number"
|
"type": "number",
|
||||||
|
"format": "float32"
|
||||||
},
|
},
|
||||||
"porn": {
|
"porn": {
|
||||||
"type": "number"
|
"type": "number",
|
||||||
|
"format": "float32"
|
||||||
},
|
},
|
||||||
"sexy": {
|
"sexy": {
|
||||||
"type": "number"
|
"type": "number",
|
||||||
|
"format": "float32"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -8223,9 +8250,138 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"time.Duration": {
|
"tensorflow.ColorChannelOrder": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"enum": [
|
"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,
|
-9223372036854775808,
|
||||||
9223372036854775807,
|
9223372036854775807,
|
||||||
1,
|
1,
|
||||||
@@ -8236,6 +8392,30 @@
|
|||||||
3600000000000
|
3600000000000
|
||||||
],
|
],
|
||||||
"x-enum-varnames": [
|
"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",
|
"minDuration",
|
||||||
"maxDuration",
|
"maxDuration",
|
||||||
"Nanosecond",
|
"Nanosecond",
|
||||||
@@ -8433,7 +8613,8 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "number"
|
"type": "number",
|
||||||
|
"format": "float64"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8498,9 +8679,15 @@
|
|||||||
"Service": {
|
"Service": {
|
||||||
"$ref": "#/definitions/vision.Service"
|
"$ref": "#/definitions/vision.Service"
|
||||||
},
|
},
|
||||||
|
"default": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"disabled": {
|
"disabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"meta": {
|
||||||
|
"$ref": "#/definitions/tensorflow.ModelInfo"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
// UpdateUserPassword changes the password of the currently authenticated user.
|
// UpdateUserPassword changes the password of the currently authenticated user.
|
||||||
//
|
//
|
||||||
// @Tags Users, Authentication
|
// @Tags Users, Authentication
|
||||||
// @Router /api/v1/users/{uid}/password [put]
|
// @Router /api/v1/users/{uid}/password [put]
|
||||||
func UpdateUserPassword(router *gin.RouterGroup) {
|
func UpdateUserPassword(router *gin.RouterGroup) {
|
||||||
router.PUT("/users/:uid/password", func(c *gin.Context) {
|
router.PUT("/users/:uid/password", func(c *gin.Context) {
|
||||||
conf := get.Config()
|
conf := get.Config()
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
// FindUserSessions finds user sessions and returns them as JSON.
|
// FindUserSessions finds user sessions and returns them as JSON.
|
||||||
//
|
//
|
||||||
// @Tags Users, Authentication
|
// @Tags Users, Authentication
|
||||||
// @Router /api/v1/users/{uid}/sessions [get]
|
// @Router /api/v1/users/{uid}/sessions [get]
|
||||||
func FindUserSessions(router *gin.RouterGroup) {
|
func FindUserSessions(router *gin.RouterGroup) {
|
||||||
router.GET("/users/:uid/sessions", func(c *gin.Context) {
|
router.GET("/users/:uid/sessions", func(c *gin.Context) {
|
||||||
// Check if the session user is has user management privileges.
|
// Check if the session user is has user management privileges.
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
// UpdateUser updates the profile information of the currently authenticated user.
|
// UpdateUser updates the profile information of the currently authenticated user.
|
||||||
//
|
//
|
||||||
// @Tags Users
|
// @Tags Users
|
||||||
// @Router /api/v1/users/{uid} [put]
|
// @Router /api/v1/users/{uid} [put]
|
||||||
func UpdateUser(router *gin.RouterGroup) {
|
func UpdateUser(router *gin.RouterGroup) {
|
||||||
router.PUT("/users/:uid", func(c *gin.Context) {
|
router.PUT("/users/:uid", func(c *gin.Context) {
|
||||||
conf := get.Config()
|
conf := get.Config()
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import (
|
|||||||
// UploadUserFiles adds files to the user upload folder, from where they can be moved and indexed.
|
// UploadUserFiles adds files to the user upload folder, from where they can be moved and indexed.
|
||||||
//
|
//
|
||||||
// @Tags Users, Files
|
// @Tags Users, Files
|
||||||
// @Router /users/{uid}/upload/{token} [post]
|
// @Router /users/{uid}/upload/{token} [post]
|
||||||
func UploadUserFiles(router *gin.RouterGroup) {
|
func UploadUserFiles(router *gin.RouterGroup) {
|
||||||
router.POST("/users/:uid/upload/:token", func(c *gin.Context) {
|
router.POST("/users/:uid/upload/:token", func(c *gin.Context) {
|
||||||
conf := get.Config()
|
conf := get.Config()
|
||||||
|
|||||||
@@ -24,11 +24,14 @@ var Api = func(conf *config.Config) gin.HandlerFunc {
|
|||||||
if origin := conf.CORSOrigin(); origin != "" {
|
if origin := conf.CORSOrigin(); origin != "" {
|
||||||
c.Header(header.AccessControlAllowOrigin, 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 {
|
if c.Request.Method == http.MethodOptions {
|
||||||
c.Header(header.AccessControlAllowHeaders, conf.CORSHeaders())
|
c.Header(header.AccessControlAllowHeaders, conf.CORSHeaders())
|
||||||
c.Header(header.AccessControlAllowMethods, conf.CORSMethods())
|
c.Header(header.AccessControlAllowMethods, conf.CORSMethods())
|
||||||
c.Header(header.AccessControlMaxAge, header.DefaultAccessControlMaxAge)
|
c.Header(header.AccessControlMaxAge, header.DefaultAccessControlMaxAge)
|
||||||
|
c.AbortWithStatus(http.StatusNoContent)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,5 +202,6 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
|||||||
api.Connect(APIv1)
|
api.Connect(APIv1)
|
||||||
api.WebSocket(APIv1)
|
api.WebSocket(APIv1)
|
||||||
api.GetMetrics(APIv1)
|
api.GetMetrics(APIv1)
|
||||||
|
api.Options(APIv1)
|
||||||
api.Echo(APIv1)
|
api.Echo(APIv1)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user