mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
API: Update endpoints to return HTTP 201 when a new resource was created
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/photoprism/photoprism/internal/photoprism/get"
|
"github.com/photoprism/photoprism/internal/photoprism/get"
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
|
"github.com/photoprism/photoprism/pkg/http/header"
|
||||||
"github.com/photoprism/photoprism/pkg/i18n"
|
"github.com/photoprism/photoprism/pkg/i18n"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -114,6 +115,7 @@ func GetAlbum(router *gin.RouterGroup) {
|
|||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} entity.Album
|
// @Success 200 {object} entity.Album
|
||||||
|
// @Success 201 {object} entity.Album
|
||||||
// @Failure 400,401,403,429,500 {object} i18n.Response
|
// @Failure 400,401,403,429,500 {object} i18n.Response
|
||||||
// @Param album body form.Album true "properties of the album to be created (currently supports Title and Favorite)"
|
// @Param album body form.Album true "properties of the album to be created (currently supports Title and Favorite)"
|
||||||
// @Router /api/v1/albums [post]
|
// @Router /api/v1/albums [post]
|
||||||
@@ -141,6 +143,8 @@ func CreateAlbum(router *gin.RouterGroup) {
|
|||||||
album := entity.NewUserAlbum(frm.AlbumTitle, entity.AlbumManual, conf.Settings().Albums.Order.Album, s.UserUID)
|
album := entity.NewUserAlbum(frm.AlbumTitle, entity.AlbumManual, conf.Settings().Albums.Order.Album, s.UserUID)
|
||||||
album.AlbumFavorite = frm.AlbumFavorite
|
album.AlbumFavorite = frm.AlbumFavorite
|
||||||
|
|
||||||
|
status := http.StatusOK
|
||||||
|
|
||||||
// Existing album?
|
// Existing album?
|
||||||
if found := album.Find(); found == nil {
|
if found := album.Find(); found == nil {
|
||||||
// Not found, create new album.
|
// Not found, create new album.
|
||||||
@@ -150,6 +154,7 @@ func CreateAlbum(router *gin.RouterGroup) {
|
|||||||
AbortUnexpectedError(c)
|
AbortUnexpectedError(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
status = http.StatusCreated
|
||||||
} else {
|
} else {
|
||||||
// Exists, restore if necessary.
|
// Exists, restore if necessary.
|
||||||
album = found
|
album = found
|
||||||
@@ -169,8 +174,13 @@ func CreateAlbum(router *gin.RouterGroup) {
|
|||||||
// Update album YAML backup.
|
// Update album YAML backup.
|
||||||
SaveAlbumYaml(*album)
|
SaveAlbumYaml(*album)
|
||||||
|
|
||||||
|
// Add location header if newly created.
|
||||||
|
if status == http.StatusCreated {
|
||||||
|
header.SetLocation(c, c.FullPath(), album.AlbumUID)
|
||||||
|
}
|
||||||
|
|
||||||
// Return as JSON.
|
// Return as JSON.
|
||||||
c.JSON(http.StatusOK, album)
|
c.JSON(status, album)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,10 @@ func TestCreateAlbum(t *testing.T) {
|
|||||||
assert.Equal(t, "new-created-album", val.String())
|
assert.Equal(t, "new-created-album", val.String())
|
||||||
val2 := gjson.Get(r.Body.String(), "Favorite")
|
val2 := gjson.Get(r.Body.String(), "Favorite")
|
||||||
assert.Equal(t, "true", val2.String())
|
assert.Equal(t, "true", val2.String())
|
||||||
assert.Equal(t, http.StatusOK, r.Code)
|
uid := gjson.Get(r.Body.String(), "UID").String()
|
||||||
|
assert.NotEmpty(t, uid)
|
||||||
|
assert.Equal(t, "/api/v1/albums/"+uid, r.Header().Get("Location"))
|
||||||
|
assert.Equal(t, http.StatusCreated, r.Code)
|
||||||
})
|
})
|
||||||
t.Run("Invalid", func(t *testing.T) {
|
t.Run("Invalid", func(t *testing.T) {
|
||||||
app, router, _ := NewApiTest()
|
app, router, _ := NewApiTest()
|
||||||
@@ -53,8 +56,10 @@ func TestUpdateAlbum(t *testing.T) {
|
|||||||
app, router, _ := NewApiTest()
|
app, router, _ := NewApiTest()
|
||||||
CreateAlbum(router)
|
CreateAlbum(router)
|
||||||
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Title": "Update", "Description": "To be updated", "Notes": "", "Favorite": true}`)
|
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Title": "Update", "Description": "To be updated", "Notes": "", "Favorite": true}`)
|
||||||
assert.Equal(t, http.StatusOK, r.Code)
|
assert.Equal(t, http.StatusCreated, r.Code)
|
||||||
uid := gjson.Get(r.Body.String(), "UID").String()
|
uid := gjson.Get(r.Body.String(), "UID").String()
|
||||||
|
assert.NotEmpty(t, uid)
|
||||||
|
assert.Equal(t, "/api/v1/albums/"+uid, r.Header().Get("Location"))
|
||||||
|
|
||||||
t.Run("Successful", func(t *testing.T) {
|
t.Run("Successful", func(t *testing.T) {
|
||||||
app, router, _ := NewApiTest()
|
app, router, _ := NewApiTest()
|
||||||
@@ -85,8 +90,10 @@ func TestDeleteAlbum(t *testing.T) {
|
|||||||
createApp, createRouter, _ := NewApiTest()
|
createApp, createRouter, _ := NewApiTest()
|
||||||
CreateAlbum(createRouter)
|
CreateAlbum(createRouter)
|
||||||
createResp := PerformRequestWithBody(createApp, "POST", "/api/v1/albums", `{"Title": "Delete", "Description": "To be deleted", "Notes": "", "Favorite": true}`)
|
createResp := PerformRequestWithBody(createApp, "POST", "/api/v1/albums", `{"Title": "Delete", "Description": "To be deleted", "Notes": "", "Favorite": true}`)
|
||||||
assert.Equal(t, http.StatusOK, createResp.Code)
|
assert.Equal(t, http.StatusCreated, createResp.Code)
|
||||||
albumUid := gjson.Get(createResp.Body.String(), "UID").String()
|
albumUid := gjson.Get(createResp.Body.String(), "UID").String()
|
||||||
|
assert.NotEmpty(t, albumUid)
|
||||||
|
assert.Equal(t, "/api/v1/albums/"+albumUid, createResp.Header().Get("Location"))
|
||||||
|
|
||||||
t.Run("ExistingAlbum", func(t *testing.T) {
|
t.Run("ExistingAlbum", func(t *testing.T) {
|
||||||
app, router, _ := NewApiTest()
|
app, router, _ := NewApiTest()
|
||||||
@@ -124,7 +131,7 @@ func TestDeleteAlbum(t *testing.T) {
|
|||||||
DeleteAlbum(router)
|
DeleteAlbum(router)
|
||||||
|
|
||||||
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Title": "ForceDelete", "Favorite": false}`)
|
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Title": "ForceDelete", "Favorite": false}`)
|
||||||
assert.Equal(t, http.StatusOK, r.Code)
|
assert.Equal(t, http.StatusCreated, r.Code)
|
||||||
uid := gjson.Get(r.Body.String(), "UID").String()
|
uid := gjson.Get(r.Body.String(), "UID").String()
|
||||||
slug := gjson.Get(r.Body.String(), "Slug").String()
|
slug := gjson.Get(r.Body.String(), "Slug").String()
|
||||||
|
|
||||||
@@ -132,9 +139,11 @@ func TestDeleteAlbum(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusOK, delResp.Code)
|
assert.Equal(t, http.StatusOK, delResp.Code)
|
||||||
|
|
||||||
recreateResp := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Title": "ForceDelete", "Favorite": false}`)
|
recreateResp := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Title": "ForceDelete", "Favorite": false}`)
|
||||||
assert.Equal(t, http.StatusOK, recreateResp.Code)
|
assert.Equal(t, http.StatusCreated, recreateResp.Code)
|
||||||
newUID := gjson.Get(recreateResp.Body.String(), "UID").String()
|
newUID := gjson.Get(recreateResp.Body.String(), "UID").String()
|
||||||
newSlug := gjson.Get(recreateResp.Body.String(), "Slug").String()
|
newSlug := gjson.Get(recreateResp.Body.String(), "Slug").String()
|
||||||
|
assert.NotEmpty(t, newUID)
|
||||||
|
assert.Equal(t, "/api/v1/albums/"+newUID, recreateResp.Header().Get("Location"))
|
||||||
|
|
||||||
assert.NotEmpty(t, uid)
|
assert.NotEmpty(t, uid)
|
||||||
assert.NotEmpty(t, newUID)
|
assert.NotEmpty(t, newUID)
|
||||||
@@ -192,8 +201,10 @@ func TestAddPhotosToAlbum(t *testing.T) {
|
|||||||
app, router, _ := NewApiTest()
|
app, router, _ := NewApiTest()
|
||||||
CreateAlbum(router)
|
CreateAlbum(router)
|
||||||
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Title": "Add photos", "Description": "", "Notes": "", "Favorite": true}`)
|
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Title": "Add photos", "Description": "", "Notes": "", "Favorite": true}`)
|
||||||
assert.Equal(t, http.StatusOK, r.Code)
|
assert.Equal(t, http.StatusCreated, r.Code)
|
||||||
uid := gjson.Get(r.Body.String(), "UID").String()
|
uid := gjson.Get(r.Body.String(), "UID").String()
|
||||||
|
assert.NotEmpty(t, uid)
|
||||||
|
assert.Equal(t, "/api/v1/albums/"+uid, r.Header().Get("Location"))
|
||||||
|
|
||||||
t.Run("AddMultiplePhotos", func(t *testing.T) {
|
t.Run("AddMultiplePhotos", func(t *testing.T) {
|
||||||
app, router, _ := NewApiTest()
|
app, router, _ := NewApiTest()
|
||||||
@@ -263,8 +274,10 @@ func TestRemovePhotosFromAlbum(t *testing.T) {
|
|||||||
AddPhotosToAlbum(router)
|
AddPhotosToAlbum(router)
|
||||||
|
|
||||||
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Title": "Remove photos", "Description": "", "Notes": "", "Favorite": true}`)
|
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Title": "Remove photos", "Description": "", "Notes": "", "Favorite": true}`)
|
||||||
assert.Equal(t, http.StatusOK, r.Code)
|
assert.Equal(t, http.StatusCreated, r.Code)
|
||||||
uid := gjson.Get(r.Body.String(), "UID").String()
|
uid := gjson.Get(r.Body.String(), "UID").String()
|
||||||
|
assert.NotEmpty(t, uid)
|
||||||
|
assert.Equal(t, "/api/v1/albums/"+uid, r.Header().Get("Location"))
|
||||||
|
|
||||||
r2 := PerformRequestWithBody(app, "POST", "/api/v1/albums/"+uid+"/photos", `{"photos": ["ps6sg6be2lvl0y12", "ps6sg6be2lvl0y11"]}`)
|
r2 := PerformRequestWithBody(app, "POST", "/api/v1/albums/"+uid+"/photos", `{"photos": ["ps6sg6be2lvl0y12", "ps6sg6be2lvl0y11"]}`)
|
||||||
assert.Equal(t, http.StatusOK, r2.Code)
|
assert.Equal(t, http.StatusOK, r2.Code)
|
||||||
@@ -311,7 +324,7 @@ func TestCloneAlbums(t *testing.T) {
|
|||||||
app, router, _ := NewApiTest()
|
app, router, _ := NewApiTest()
|
||||||
CreateAlbum(router)
|
CreateAlbum(router)
|
||||||
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Title": "Update", "Description": "To be updated", "Notes": "", "Favorite": true}`)
|
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Title": "Update", "Description": "To be updated", "Notes": "", "Favorite": true}`)
|
||||||
assert.Equal(t, http.StatusOK, r.Code)
|
assert.Equal(t, http.StatusCreated, r.Code)
|
||||||
uid := gjson.Get(r.Body.String(), "UID").String()
|
uid := gjson.Get(r.Body.String(), "UID").String()
|
||||||
|
|
||||||
t.Run("CloneEmptyAlbum", func(t *testing.T) {
|
t.Run("CloneEmptyAlbum", func(t *testing.T) {
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ func TestBatchAlbumsDelete(t *testing.T) {
|
|||||||
app, router, _ := NewApiTest()
|
app, router, _ := NewApiTest()
|
||||||
CreateAlbum(router)
|
CreateAlbum(router)
|
||||||
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Title": "BatchDelete", "Description": "To be deleted", "Notes": "", "Favorite": true}`)
|
r := PerformRequestWithBody(app, "POST", "/api/v1/albums", `{"Title": "BatchDelete", "Description": "To be deleted", "Notes": "", "Favorite": true}`)
|
||||||
assert.Equal(t, http.StatusOK, r.Code)
|
assert.Equal(t, http.StatusCreated, r.Code)
|
||||||
uid := gjson.Get(r.Body.String(), "UID").String()
|
uid := gjson.Get(r.Body.String(), "UID").String()
|
||||||
|
|
||||||
t.Run("Success", func(t *testing.T) {
|
t.Run("Success", func(t *testing.T) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/photoprism/photoprism/internal/photoprism/get"
|
"github.com/photoprism/photoprism/internal/photoprism/get"
|
||||||
"github.com/photoprism/photoprism/internal/thumb/crop"
|
"github.com/photoprism/photoprism/internal/thumb/crop"
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
|
"github.com/photoprism/photoprism/pkg/http/header"
|
||||||
"github.com/photoprism/photoprism/pkg/i18n"
|
"github.com/photoprism/photoprism/pkg/i18n"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -75,8 +76,10 @@ 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]
|
// @Produce json
|
||||||
|
// @Success 201 {object} entity.Marker
|
||||||
|
// @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) {
|
||||||
s := Auth(c, acl.ResourceFiles, acl.ActionUpdate)
|
s := Auth(c, acl.ResourceFiles, acl.ActionUpdate)
|
||||||
@@ -164,8 +167,9 @@ func CreateMarker(router *gin.RouterGroup) {
|
|||||||
// Display success message.
|
// Display success message.
|
||||||
event.SuccessMsg(i18n.MsgChangesSaved)
|
event.SuccessMsg(i18n.MsgChangesSaved)
|
||||||
|
|
||||||
// Return new marker.
|
// Return new marker with location header.
|
||||||
c.JSON(http.StatusOK, marker)
|
header.SetLocation(c, c.FullPath(), marker.MarkerUID)
|
||||||
|
c.JSON(http.StatusCreated, marker)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,10 @@ func TestCreateMarker(t *testing.T) {
|
|||||||
r = PerformRequestWithBody(app, "POST", u, string(b))
|
r = PerformRequestWithBody(app, "POST", u, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, r.Code)
|
assert.Equal(t, http.StatusCreated, r.Code)
|
||||||
|
newUID := gjson.Get(r.Body.String(), "UID").String()
|
||||||
|
assert.NotEmpty(t, newUID)
|
||||||
|
assert.Equal(t, "/api/v1/markers/"+newUID, r.Header().Get("Location"))
|
||||||
})
|
})
|
||||||
t.Run("SuccessWithName", func(t *testing.T) {
|
t.Run("SuccessWithName", func(t *testing.T) {
|
||||||
app, router, _ := NewApiTest()
|
app, router, _ := NewApiTest()
|
||||||
@@ -95,7 +98,10 @@ func TestCreateMarker(t *testing.T) {
|
|||||||
r = PerformRequestWithBody(app, "POST", u, string(b))
|
r = PerformRequestWithBody(app, "POST", u, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, http.StatusOK, r.Code)
|
assert.Equal(t, http.StatusCreated, r.Code)
|
||||||
|
newUID := gjson.Get(r.Body.String(), "UID").String()
|
||||||
|
assert.NotEmpty(t, newUID)
|
||||||
|
assert.Equal(t, "/api/v1/markers/"+newUID, r.Header().Get("Location"))
|
||||||
})
|
})
|
||||||
t.Run("InvalidArea", func(t *testing.T) {
|
t.Run("InvalidArea", func(t *testing.T) {
|
||||||
app, router, _ := NewApiTest()
|
app, router, _ := NewApiTest()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -15,6 +16,7 @@ import (
|
|||||||
"github.com/photoprism/photoprism/internal/workers"
|
"github.com/photoprism/photoprism/internal/workers"
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
"github.com/photoprism/photoprism/pkg/fs"
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
|
"github.com/photoprism/photoprism/pkg/http/header"
|
||||||
"github.com/photoprism/photoprism/pkg/i18n"
|
"github.com/photoprism/photoprism/pkg/i18n"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -126,7 +128,7 @@ func GetServiceFolders(router *gin.RouterGroup) {
|
|||||||
// @Tags Services
|
// @Tags Services
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} entity.Service
|
// @Success 201 {object} entity.Service
|
||||||
// @Failure 400,401,403,429 {object} i18n.Response
|
// @Failure 400,401,403,429 {object} i18n.Response
|
||||||
// @Param service body form.Service true "properties of the service to be created"
|
// @Param service body form.Service true "properties of the service to be created"
|
||||||
// @Router /api/v1/services [post]
|
// @Router /api/v1/services [post]
|
||||||
@@ -166,7 +168,9 @@ func AddService(router *gin.RouterGroup) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, m)
|
// Return new service with location header.
|
||||||
|
header.SetLocation(c, c.FullPath(), strconv.FormatUint(uint64(m.ID), 10))
|
||||||
|
c.JSON(http.StatusCreated, m)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -77,7 +78,10 @@ func TestCreateService(t *testing.T) {
|
|||||||
"SyncPath": "", "SyncInterval": 3, "SyncUpload": false, "SyncDownload": false, "SyncFilenames": false, "SyncRaw": false}`)
|
"SyncPath": "", "SyncInterval": 3, "SyncUpload": false, "SyncDownload": false, "SyncFilenames": false, "SyncRaw": false}`)
|
||||||
val := gjson.Get(r.Body.String(), "AccOwner")
|
val := gjson.Get(r.Body.String(), "AccOwner")
|
||||||
assert.Equal(t, "Test", val.String())
|
assert.Equal(t, "Test", val.String())
|
||||||
assert.Equal(t, http.StatusOK, r.Code)
|
id := gjson.Get(r.Body.String(), "ID").Int()
|
||||||
|
assert.NotZero(t, id)
|
||||||
|
assert.Equal(t, fmt.Sprintf("/api/v1/services/%d", id), r.Header().Get("Location"))
|
||||||
|
assert.Equal(t, http.StatusCreated, r.Code)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +97,7 @@ func TestUpdateService(t *testing.T) {
|
|||||||
assert.Equal(t, int64(5), val2.Int())
|
assert.Equal(t, int64(5), val2.Int())
|
||||||
val3 := gjson.Get(r.Body.String(), "AccName")
|
val3 := gjson.Get(r.Body.String(), "AccName")
|
||||||
assert.Equal(t, "Dummy-Webdav", val3.String())
|
assert.Equal(t, "Dummy-Webdav", val3.String())
|
||||||
assert.Equal(t, http.StatusOK, r.Code)
|
assert.Equal(t, http.StatusCreated, r.Code)
|
||||||
id := gjson.Get(r.Body.String(), "ID").String()
|
id := gjson.Get(r.Body.String(), "ID").String()
|
||||||
|
|
||||||
t.Run("Success", func(t *testing.T) {
|
t.Run("Success", func(t *testing.T) {
|
||||||
@@ -132,8 +136,10 @@ func TestDeleteService(t *testing.T) {
|
|||||||
r := PerformRequestWithBody(app, "POST", "/api/v1/services", `{"AccName": "DeleteTest", "AccOwner": "TestDelete", "AccUrl": "http://dummy-webdav/", "AccType": "webdav",
|
r := PerformRequestWithBody(app, "POST", "/api/v1/services", `{"AccName": "DeleteTest", "AccOwner": "TestDelete", "AccUrl": "http://dummy-webdav/", "AccType": "webdav",
|
||||||
"AccKey": "123", "AccUser": "admin", "AccPass": "photoprism", "AccError": "", "AccShare": false, "AccSync": false, "RetryLimit": 3, "SharePath": "", "ShareSize": "", "ShareExpires": 0,
|
"AccKey": "123", "AccUser": "admin", "AccPass": "photoprism", "AccError": "", "AccShare": false, "AccSync": false, "RetryLimit": 3, "SharePath": "", "ShareSize": "", "ShareExpires": 0,
|
||||||
"SyncPath": "", "SyncInterval": 5, "SyncUpload": false, "SyncDownload": false, "SyncFilenames": false, "SyncRaw": false}`)
|
"SyncPath": "", "SyncInterval": 5, "SyncUpload": false, "SyncDownload": false, "SyncFilenames": false, "SyncRaw": false}`)
|
||||||
assert.Equal(t, http.StatusOK, r.Code)
|
assert.Equal(t, http.StatusCreated, r.Code)
|
||||||
id := gjson.Get(r.Body.String(), "ID").String()
|
id := gjson.Get(r.Body.String(), "ID").String()
|
||||||
|
assert.NotEmpty(t, id)
|
||||||
|
assert.Equal(t, "/api/v1/services/"+id, r.Header().Get("Location"))
|
||||||
|
|
||||||
t.Run("Success", func(t *testing.T) {
|
t.Run("Success", func(t *testing.T) {
|
||||||
app, router, _ := NewApiTest()
|
app, router, _ := NewApiTest()
|
||||||
|
|||||||
@@ -5009,6 +5009,12 @@
|
|||||||
"$ref": "#/definitions/entity.Album"
|
"$ref": "#/definitions/entity.Album"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/entity.Album"
|
||||||
|
}
|
||||||
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Bad Request",
|
"description": "Bad Request",
|
||||||
"schema": {
|
"schema": {
|
||||||
@@ -8282,7 +8288,17 @@
|
|||||||
},
|
},
|
||||||
"/api/v1/markers": {
|
"/api/v1/markers": {
|
||||||
"post": {
|
"post": {
|
||||||
"responses": {},
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": "Created",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/entity.Marker"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
"Files"
|
"Files"
|
||||||
]
|
]
|
||||||
@@ -10030,8 +10046,8 @@
|
|||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"201": {
|
||||||
"description": "OK",
|
"description": "Created",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/entity.Service"
|
"$ref": "#/definitions/entity.Service"
|
||||||
}
|
}
|
||||||
@@ -11457,8 +11473,8 @@
|
|||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"201": {
|
||||||
"description": "OK",
|
"description": "Created",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/entity.Passcode"
|
"$ref": "#/definitions/entity.Passcode"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import (
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param uid path string true "user uid"
|
// @Param uid path string true "user uid"
|
||||||
// @Param request body form.Passcode true "passcode setup (password required)"
|
// @Param request body form.Passcode true "passcode setup (password required)"
|
||||||
// @Success 200 {object} entity.Passcode
|
// @Success 201 {object} entity.Passcode
|
||||||
// @Failure 400,401,403,429 {object} i18n.Response
|
// @Failure 400,401,403,429 {object} i18n.Response
|
||||||
// @Router /api/v1/users/{uid}/passcode [post]
|
// @Router /api/v1/users/{uid}/passcode [post]
|
||||||
func CreateUserPasscode(router *gin.RouterGroup) {
|
func CreateUserPasscode(router *gin.RouterGroup) {
|
||||||
@@ -84,7 +84,8 @@ func CreateUserPasscode(router *gin.RouterGroup) {
|
|||||||
|
|
||||||
event.AuditInfo([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.Passcode, authn.Created}, s.RefID)
|
event.AuditInfo([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.Passcode, authn.Created}, s.RefID)
|
||||||
|
|
||||||
c.JSON(http.StatusOK, passcode)
|
header.SetLocation(c)
|
||||||
|
c.JSON(http.StatusCreated, passcode)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,8 @@ func TestCreateUserPasscode(t *testing.T) {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
} else {
|
} else {
|
||||||
r := AuthenticatedRequestWithBody(app, "POST", "/api/v1/users/uqxetse3cy5eo9z2/passcode", string(pcStr), sessId)
|
r := AuthenticatedRequestWithBody(app, "POST", "/api/v1/users/uqxetse3cy5eo9z2/passcode", string(pcStr), sessId)
|
||||||
assert.Equal(t, http.StatusOK, r.Code)
|
assert.Equal(t, http.StatusCreated, r.Code)
|
||||||
|
assert.Equal(t, "/api/v1/users/uqxetse3cy5eo9z2/passcode", r.Header().Get("Location"))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -248,7 +249,7 @@ func TestUserPasscode(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
r := AuthenticatedRequestWithBody(app, "POST", "/api/v1/users/uqxetse3cy5eo9z2/passcode", string(pcStr), sessId)
|
r := AuthenticatedRequestWithBody(app, "POST", "/api/v1/users/uqxetse3cy5eo9z2/passcode", string(pcStr), sessId)
|
||||||
assert.Equal(t, http.StatusOK, r.Code)
|
assert.Equal(t, http.StatusCreated, r.Code)
|
||||||
|
|
||||||
secret := gjson.Get(r.Body.String(), "Secret").String()
|
secret := gjson.Get(r.Body.String(), "Secret").String()
|
||||||
activatedAt := gjson.Get(r.Body.String(), "ActivatedAt").String()
|
activatedAt := gjson.Get(r.Body.String(), "ActivatedAt").String()
|
||||||
|
|||||||
60
pkg/http/header/location.go
Normal file
60
pkg/http/header/location.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package header
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetLocation adds a Location header with a relative path based on the provided segments.
|
||||||
|
// When the first segment is non-empty it is treated as the base path;
|
||||||
|
// otherwise the request URL path is used.
|
||||||
|
func SetLocation(c *gin.Context, segments ...string) {
|
||||||
|
// Return if context is missing.
|
||||||
|
if c == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
base := ""
|
||||||
|
|
||||||
|
if len(segments) > 0 && segments[0] != "" {
|
||||||
|
base = segments[0]
|
||||||
|
segments = segments[1:]
|
||||||
|
} else if c.Request != nil && c.Request.URL != nil {
|
||||||
|
base = c.Request.URL.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return if base is missing.
|
||||||
|
if base == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compose redirect location string.
|
||||||
|
prefixSlash := strings.HasPrefix(base, "/")
|
||||||
|
base = strings.Trim(base, "/")
|
||||||
|
|
||||||
|
parts := make([]string, 0, 1+len(segments))
|
||||||
|
if base != "" {
|
||||||
|
parts = append(parts, base)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, segment := range segments {
|
||||||
|
segment = strings.Trim(segment, "/")
|
||||||
|
if segment == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts = append(parts, segment)
|
||||||
|
}
|
||||||
|
|
||||||
|
location := strings.Join(parts, "/")
|
||||||
|
if prefixSlash {
|
||||||
|
location = "/" + location
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Location header to response.
|
||||||
|
if location == "" && prefixSlash {
|
||||||
|
c.Header(Location, "/")
|
||||||
|
} else {
|
||||||
|
c.Header(Location, location)
|
||||||
|
}
|
||||||
|
}
|
||||||
43
pkg/http/header/location_test.go
Normal file
43
pkg/http/header/location_test.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package header
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSetLocationWithBase(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
c.Request = httptest.NewRequest("POST", "/api/v1/albums", nil)
|
||||||
|
|
||||||
|
SetLocation(c, "/api/v1/albums", "abc123")
|
||||||
|
|
||||||
|
assert.Equal(t, "/api/v1/albums/abc123", c.Writer.Header().Get(Location))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetLocationUsesRequestPath(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
c.Request = httptest.NewRequest("POST", "/api/v1/services", nil)
|
||||||
|
|
||||||
|
SetLocation(c, "", "99")
|
||||||
|
|
||||||
|
assert.Equal(t, "/api/v1/services/99", c.Writer.Header().Get(Location))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetLocationTrimsSegments(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := gin.CreateTestContext(w)
|
||||||
|
c.Request = httptest.NewRequest("POST", "/api/v1/markers/", nil)
|
||||||
|
|
||||||
|
SetLocation(c, "/api/v1/markers/", "/m1/")
|
||||||
|
|
||||||
|
assert.Equal(t, "/api/v1/markers/m1", c.Writer.Header().Get(Location))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetLocationEmpty(t *testing.T) {
|
||||||
|
SetLocation(nil)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user