Espaces personnels et favoris : API par utilisateur, /api/me.id, tests
- Chaque route de schémas passe par la vue de l'utilisateur connecté (storage.Space) : arborescence commune, .perso et .favoris. - /api/public résout aussi les liens des espaces personnels. - /api/me renvoie id (dossier de l'utilisateur : email OIDC, sinon « local »). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0174sFqehj4uGqJHQZvWy1gn
This commit is contained in:
1 parent
781fc8f5f5
commit
824871bd24
5 files changed
+512
-27
No files matched your search
+20
-2
@@ -90,7 +90,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
(&api.Handler{Store: store, MaxBody: maxMB << 20, Update: checker}).Register(mux)
|
spaces := storage.NewSpaces(store)
|
||||||
|
(&api.Handler{Store: store, Spaces: spaces, User: userID, MaxBody: maxMB << 20, Update: checker}).Register(mux)
|
||||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
|
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||||
_, _ = w.Write([]byte("ok"))
|
_, _ = w.Write([]byte("ok"))
|
||||||
})
|
})
|
||||||
@@ -168,11 +169,28 @@ func authenticate(ctx context.Context, mux *http.ServeMux, dataDir string) (http
|
|||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
_ = json.NewEncoder(w).Encode(m)
|
// id : identifiant de l'utilisateur, qui nomme son espace personnel.
|
||||||
|
_ = json.NewEncoder(w).Encode(struct {
|
||||||
|
auth.Me
|
||||||
|
ID string `json:"id"`
|
||||||
|
}{m, userID(r)})
|
||||||
})
|
})
|
||||||
return h, nil
|
return h, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// userID identifie l'utilisateur d'une requête pour son espace personnel et
|
||||||
|
// ses favoris : email OIDC (sinon sub), utilisateur unique storage.LocalUser
|
||||||
|
// en authentification locale ou sans authentification.
|
||||||
|
func userID(r *http.Request) string {
|
||||||
|
if u := auth.UserFrom(r.Context()); u != nil {
|
||||||
|
if u.Email != "" {
|
||||||
|
return storage.UserID(u.Email)
|
||||||
|
}
|
||||||
|
return storage.UserID("sub-" + u.Sub)
|
||||||
|
}
|
||||||
|
return storage.LocalUser
|
||||||
|
}
|
||||||
|
|
||||||
// oidcConfig lit les variables OIDC_* et SESSION_*.
|
// oidcConfig lit les variables OIDC_* et SESSION_*.
|
||||||
func oidcConfig(dataDir string) (auth.Config, error) {
|
func oidcConfig(dataDir string) (auth.Config, error) {
|
||||||
cfg := auth.Config{
|
cfg := auth.Config{
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func TestAuthenticateModes(t *testing.T) {
|
|||||||
t.Setenv("AUTH_MODE", "")
|
t.Setenv("AUTH_MODE", "")
|
||||||
t.Setenv("AUTH_USER", "")
|
t.Setenv("AUTH_USER", "")
|
||||||
h, err := authenticate(context.Background(), http.NewServeMux(), t.TempDir())
|
h, err := authenticate(context.Background(), http.NewServeMux(), t.TempDir())
|
||||||
if err != nil || me(t, h, "") != `{"mode":"none"}` {
|
if err != nil || me(t, h, "") != `{"mode":"none","id":"local"}` {
|
||||||
t.Fatalf("%v %s", err, me(t, h, ""))
|
t.Fatalf("%v %s", err, me(t, h, ""))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+133
-24
@@ -26,6 +26,10 @@
|
|||||||
// GET /api/settings / PUT {"title","channel"} paramètres (titre, logo, canal)
|
// GET /api/settings / PUT {"title","channel"} paramètres (titre, logo, canal)
|
||||||
// GET /api/logo / PUT (image/png) / DELETE logo 64x64
|
// GET /api/logo / PUT (image/png) / DELETE logo 64x64
|
||||||
// GET /api/update dernière version d'Excalidraw sur npm
|
// GET /api/update dernière version d'Excalidraw sur npm
|
||||||
|
//
|
||||||
|
// Les chemins sont vus par l'utilisateur connecté (storage.Space) : l'arborescence
|
||||||
|
// commune, plus ".perso/..." (son espace personnel) et ".favoris" (ses favoris,
|
||||||
|
// liens symboliques vers les originaux ; DELETE .favoris/x retire le lien).
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -45,11 +49,28 @@ import (
|
|||||||
|
|
||||||
// Handler regroupe les routes de l'API.
|
// Handler regroupe les routes de l'API.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
Store *storage.Store
|
Store *storage.Store // arborescence commune : bibliothèque, paramètres, logo
|
||||||
|
Spaces *storage.Spaces // schémas : arborescence commune, espaces personnels, favoris
|
||||||
|
User func(*http.Request) string // identifiant de l'utilisateur (storage.UserID)
|
||||||
MaxBody int64 // taille max d'un schéma (images embarquées comprises)
|
MaxBody int64 // taille max d'un schéma (images embarquées comprises)
|
||||||
Update *update.Checker // nil : vérification des mises à jour désactivée
|
Update *update.Checker // nil : vérification des mises à jour désactivée
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// space renvoie la vue de l'utilisateur de la requête (espace personnel et
|
||||||
|
// favoris) ; utilisateur unique LocalUser sans OIDC.
|
||||||
|
func (h *Handler) space(w http.ResponseWriter, r *http.Request) (*storage.Space, bool) {
|
||||||
|
id := storage.LocalUser
|
||||||
|
if h.User != nil {
|
||||||
|
id = h.User(r)
|
||||||
|
}
|
||||||
|
u, err := h.Spaces.For(id)
|
||||||
|
if err != nil {
|
||||||
|
fail(w, err)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return u, true
|
||||||
|
}
|
||||||
|
|
||||||
// Register enregistre les routes sur mux (motifs Go 1.22+).
|
// Register enregistre les routes sur mux (motifs Go 1.22+).
|
||||||
func (h *Handler) Register(mux *http.ServeMux) {
|
func (h *Handler) Register(mux *http.ServeMux) {
|
||||||
mux.HandleFunc("GET /api/list", h.list)
|
mux.HandleFunc("GET /api/list", h.list)
|
||||||
@@ -138,7 +159,11 @@ func (h *Handler) decode(w http.ResponseWriter, r *http.Request, v any) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) list(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) list(w http.ResponseWriter, r *http.Request) {
|
||||||
entries, err := h.Store.List(r.URL.Query().Get("path"))
|
u, ok := h.space(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
entries, err := u.List(r.URL.Query().Get("path"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail(w, err)
|
fail(w, err)
|
||||||
return
|
return
|
||||||
@@ -147,9 +172,13 @@ func (h *Handler) list(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) search(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) search(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := h.space(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
q := r.URL.Query()
|
q := r.URL.Query()
|
||||||
f := storage.Filter{Tag: q.Get("tag"), Favorite: q.Get("fav") == "true"}
|
f := storage.Filter{Tag: q.Get("tag"), Favorite: q.Get("fav") == "true"}
|
||||||
entries, err := h.Store.Search(q.Get("q"), f, 200)
|
entries, err := u.Search(q.Get("q"), f, 200)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail(w, err)
|
fail(w, err)
|
||||||
return
|
return
|
||||||
@@ -158,7 +187,11 @@ func (h *Handler) search(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) tags(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) tags(w http.ResponseWriter, r *http.Request) {
|
||||||
tags, err := h.Store.Tags()
|
u, ok := h.space(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tags, err := u.Tags()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail(w, err)
|
fail(w, err)
|
||||||
return
|
return
|
||||||
@@ -187,11 +220,15 @@ func (h *Handler) decodeMeta(w http.ResponseWriter, r *http.Request) (metaReq, b
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) setFavorite(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) setFavorite(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := h.space(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
req, ok := h.decodeMeta(w, r)
|
req, ok := h.decodeMeta(w, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := h.Store.SetFavorite(req.Paths, req.Favorite); err != nil {
|
if err := u.SetFavorite(req.Paths, req.Favorite); err != nil {
|
||||||
fail(w, err)
|
fail(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -199,11 +236,15 @@ func (h *Handler) setFavorite(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) editTags(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) editTags(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := h.space(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
req, ok := h.decodeMeta(w, r)
|
req, ok := h.decodeMeta(w, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := h.Store.EditTags(req.Paths, req.Add, req.Remove); err != nil {
|
if err := u.EditTags(req.Paths, req.Add, req.Remove); err != nil {
|
||||||
fail(w, err)
|
fail(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -211,7 +252,11 @@ func (h *Handler) editTags(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) readFile(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) readFile(w http.ResponseWriter, r *http.Request) {
|
||||||
data, etag, err := h.Store.Read(r.URL.Query().Get("path"))
|
u, ok := h.space(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, etag, err := u.Read(r.URL.Query().Get("path"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail(w, err)
|
fail(w, err)
|
||||||
return
|
return
|
||||||
@@ -227,13 +272,17 @@ func (h *Handler) readFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) writeFile(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) writeFile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := h.space(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
data, err := h.readBody(w, r)
|
data, err := h.readBody(w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail(w, err)
|
fail(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
create := r.Header.Get("If-None-Match") == "*"
|
create := r.Header.Get("If-None-Match") == "*"
|
||||||
entry, etag, err := h.Store.Write(r.URL.Query().Get("path"), data, r.Header.Get("If-Match"), create)
|
entry, etag, err := u.Write(r.URL.Query().Get("path"), data, r.Header.Get("If-Match"), create)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail(w, err)
|
fail(w, err)
|
||||||
return
|
return
|
||||||
@@ -252,11 +301,15 @@ type moveReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) mkdir(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) mkdir(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := h.space(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
var req pathReq
|
var req pathReq
|
||||||
if !h.decode(w, r, &req) {
|
if !h.decode(w, r, &req) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
entry, err := h.Store.Mkdir(req.Path)
|
entry, err := u.Mkdir(req.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail(w, err)
|
fail(w, err)
|
||||||
return
|
return
|
||||||
@@ -265,11 +318,15 @@ func (h *Handler) mkdir(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) move(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) move(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := h.space(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
var req moveReq
|
var req moveReq
|
||||||
if !h.decode(w, r, &req) {
|
if !h.decode(w, r, &req) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
entry, err := h.Store.Move(req.From, req.To)
|
entry, err := u.Move(req.From, req.To)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail(w, err)
|
fail(w, err)
|
||||||
return
|
return
|
||||||
@@ -285,6 +342,10 @@ type moveManyReq struct {
|
|||||||
// moveMany renvoie toujours 200 avec le détail par élément : le client
|
// moveMany renvoie toujours 200 avec le détail par élément : le client
|
||||||
// affiche les échecs partiels (ex. nom déjà pris dans la destination).
|
// affiche les échecs partiels (ex. nom déjà pris dans la destination).
|
||||||
func (h *Handler) moveMany(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) moveMany(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := h.space(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
var req moveManyReq
|
var req moveManyReq
|
||||||
if !h.decode(w, r, &req) {
|
if !h.decode(w, r, &req) {
|
||||||
return
|
return
|
||||||
@@ -293,7 +354,7 @@ func (h *Handler) moveMany(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeErr(w, http.StatusBadRequest, "entre 1 et 1000 chemins attendus")
|
writeErr(w, http.StatusBadRequest, "entre 1 et 1000 chemins attendus")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res, err := h.Store.MoveMany(req.Paths, req.Dir)
|
res, err := u.MoveMany(req.Paths, req.Dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail(w, err)
|
fail(w, err)
|
||||||
return
|
return
|
||||||
@@ -302,7 +363,11 @@ func (h *Handler) moveMany(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) readThumb(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) readThumb(w http.ResponseWriter, r *http.Request) {
|
||||||
data, mod, err := h.Store.ReadThumb(r.URL.Query().Get("path"))
|
u, ok := h.space(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, mod, err := u.ReadThumb(r.URL.Query().Get("path"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Pas de mise en cache d'une absence : la miniature va être générée.
|
// Pas de mise en cache d'une absence : la miniature va être générée.
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
@@ -317,9 +382,13 @@ func (h *Handler) readThumb(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) writeThumb(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) writeThumb(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := h.space(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
data, err := io.ReadAll(http.MaxBytesReader(w, r.Body, storage.MaxThumbSize))
|
data, err := io.ReadAll(http.MaxBytesReader(w, r.Body, storage.MaxThumbSize))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = h.Store.WriteThumb(r.URL.Query().Get("path"), data)
|
err = u.WriteThumb(r.URL.Query().Get("path"), data)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail(w, err)
|
fail(w, err)
|
||||||
@@ -329,11 +398,15 @@ func (h *Handler) writeThumb(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) copy(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) copy(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := h.space(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
var req moveReq
|
var req moveReq
|
||||||
if !h.decode(w, r, &req) {
|
if !h.decode(w, r, &req) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
entry, err := h.Store.Copy(req.From, req.To)
|
entry, err := u.Copy(req.From, req.To)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail(w, err)
|
fail(w, err)
|
||||||
return
|
return
|
||||||
@@ -342,8 +415,12 @@ func (h *Handler) copy(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) delete(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) delete(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := h.space(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
q := r.URL.Query()
|
q := r.URL.Query()
|
||||||
if err := h.Store.Delete(q.Get("path"), q.Get("recursive") == "true"); err != nil {
|
if err := u.Delete(q.Get("path"), q.Get("recursive") == "true"); err != nil {
|
||||||
fail(w, err)
|
fail(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -351,7 +428,11 @@ func (h *Handler) delete(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) history(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) history(w http.ResponseWriter, r *http.Request) {
|
||||||
vs, err := h.Store.Versions(r.URL.Query().Get("path"))
|
u, ok := h.space(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vs, err := u.Versions(r.URL.Query().Get("path"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail(w, err)
|
fail(w, err)
|
||||||
return
|
return
|
||||||
@@ -361,8 +442,12 @@ func (h *Handler) history(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) readVersion(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) readVersion(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := h.space(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
q := r.URL.Query()
|
q := r.URL.Query()
|
||||||
data, err := h.Store.ReadVersion(q.Get("path"), q.Get("id"))
|
data, err := u.ReadVersion(q.Get("path"), q.Get("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail(w, err)
|
fail(w, err)
|
||||||
return
|
return
|
||||||
@@ -374,11 +459,15 @@ func (h *Handler) readVersion(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) snapshot(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) snapshot(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := h.space(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
var req pathReq
|
var req pathReq
|
||||||
if !h.decode(w, r, &req) {
|
if !h.decode(w, r, &req) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
v, err := h.Store.Snapshot(req.Path)
|
v, err := u.Snapshot(req.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail(w, err)
|
fail(w, err)
|
||||||
return
|
return
|
||||||
@@ -392,11 +481,15 @@ type restoreReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) restore(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) restore(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := h.space(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
var req restoreReq
|
var req restoreReq
|
||||||
if !h.decode(w, r, &req) {
|
if !h.decode(w, r, &req) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
entry, etag, err := h.Store.Restore(req.Path, req.ID)
|
entry, etag, err := u.Restore(req.Path, req.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail(w, err)
|
fail(w, err)
|
||||||
return
|
return
|
||||||
@@ -406,7 +499,11 @@ func (h *Handler) restore(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) shares(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) shares(w http.ResponseWriter, r *http.Request) {
|
||||||
l, err := h.Store.Shares(r.URL.Query().Get("path"))
|
u, ok := h.space(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l, err := u.Shares(r.URL.Query().Get("path"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail(w, err)
|
fail(w, err)
|
||||||
return
|
return
|
||||||
@@ -421,6 +518,10 @@ type shareReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) createShare(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) createShare(w http.ResponseWriter, r *http.Request) {
|
||||||
|
u, ok := h.space(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
var req shareReq
|
var req shareReq
|
||||||
if !h.decode(w, r, &req) {
|
if !h.decode(w, r, &req) {
|
||||||
return
|
return
|
||||||
@@ -429,7 +530,7 @@ func (h *Handler) createShare(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeErr(w, http.StatusBadRequest, "durée de 0 (sans expiration) à 3650 jours")
|
writeErr(w, http.StatusBadRequest, "durée de 0 (sans expiration) à 3650 jours")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sh, err := h.Store.CreateShare(req.Path, time.Duration(req.Days)*24*time.Hour)
|
sh, err := u.CreateShare(req.Path, time.Duration(req.Days)*24*time.Hour)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail(w, err)
|
fail(w, err)
|
||||||
return
|
return
|
||||||
@@ -438,7 +539,11 @@ func (h *Handler) createShare(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) revokeShare(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) revokeShare(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := h.Store.RevokeShare(r.URL.Query().Get("id")); err != nil {
|
u, ok := h.space(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := u.RevokeShare(r.URL.Query().Get("id")); err != nil {
|
||||||
fail(w, err)
|
fail(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -446,7 +551,11 @@ func (h *Handler) revokeShare(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) revokeAllShares(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) revokeAllShares(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := h.Store.RevokeAllShares(); err != nil {
|
u, ok := h.space(w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := u.RevokeAllShares(); err != nil {
|
||||||
fail(w, err)
|
fail(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -458,7 +567,7 @@ func (h *Handler) revokeAllShares(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (h *Handler) public(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) public(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
w.Header().Set("X-Robots-Tag", "noindex, nofollow")
|
w.Header().Set("X-Robots-Tag", "noindex, nofollow")
|
||||||
sh, data, err := h.Store.ReadShared(r.PathValue("token"))
|
sh, data, err := h.Spaces.ReadShared(r.PathValue("token"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, storage.ErrNotFound) {
|
if errors.Is(err, storage.ErrNotFound) {
|
||||||
writeErr(w, http.StatusNotFound, "lien invalide, expiré ou révoqué")
|
writeErr(w, http.StatusNotFound, "lien invalide, expiré ou révoqué")
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/vblogio/excalidraw-go/internal/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
const doc = `{"type":"excalidraw","version":2,"elements":[]}`
|
||||||
|
|
||||||
|
// L'utilisateur vient de l'en-tête X-User (à la place de la session OIDC).
|
||||||
|
func newServer(t *testing.T) *httptest.Server {
|
||||||
|
t.Helper()
|
||||||
|
st, err := storage.New(t.TempDir())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
(&Handler{
|
||||||
|
Store: st,
|
||||||
|
Spaces: storage.NewSpaces(st),
|
||||||
|
User: func(r *http.Request) string { return storage.UserID(r.Header.Get("X-User")) },
|
||||||
|
MaxBody: 1 << 20,
|
||||||
|
}).Register(mux)
|
||||||
|
srv := httptest.NewServer(mux)
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
|
return srv
|
||||||
|
}
|
||||||
|
|
||||||
|
func call(t *testing.T, srv *httptest.Server, user, method, url, body string, out any) int {
|
||||||
|
t.Helper()
|
||||||
|
req, _ := http.NewRequest(method, srv.URL+url, strings.NewReader(body))
|
||||||
|
req.Header.Set("X-User", user)
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
if out != nil {
|
||||||
|
_ = json.NewDecoder(res.Body).Decode(out)
|
||||||
|
}
|
||||||
|
return res.StatusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFavoritesPerUser(t *testing.T) {
|
||||||
|
srv := newServer(t)
|
||||||
|
for _, p := range []string{"a.excalidraw", ".perso/mine.excalidraw"} {
|
||||||
|
if c := call(t, srv, "alice@x.fr", "PUT", "/api/file?path="+p, doc, nil); c != 200 {
|
||||||
|
t.Fatalf("écriture %s : %d", p, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c := call(t, srv, "alice@x.fr", "POST", "/api/meta/favorite", `{"paths":["a.excalidraw",".perso/mine.excalidraw"],"favorite":true}`, nil); c != 204 {
|
||||||
|
t.Fatalf("favori : %d", c)
|
||||||
|
}
|
||||||
|
var favs []storage.Entry
|
||||||
|
call(t, srv, "alice@x.fr", "GET", "/api/list?path=.favoris", "", &favs)
|
||||||
|
if len(favs) != 2 || favs[0].Path != "a.excalidraw" || favs[1].Path != ".perso/mine.excalidraw" {
|
||||||
|
t.Fatalf("favoris d'alice = %+v", favs)
|
||||||
|
}
|
||||||
|
call(t, srv, "bob@x.fr", "GET", "/api/list?path=.favoris", "", &favs)
|
||||||
|
if len(favs) != 0 {
|
||||||
|
t.Fatalf("favoris de bob = %+v", favs)
|
||||||
|
}
|
||||||
|
var root []storage.Entry
|
||||||
|
call(t, srv, "bob@x.fr", "GET", "/api/list?path=", "", &root)
|
||||||
|
if len(root) != 3 || root[0].Special != "favoris" || root[2].Favorite {
|
||||||
|
t.Fatalf("racine de bob = %+v", root)
|
||||||
|
}
|
||||||
|
if c := call(t, srv, "bob@x.fr", "GET", "/api/file?path=.perso/mine.excalidraw", "", nil); c != 404 {
|
||||||
|
t.Fatalf("bob lit l'espace d'alice : %d", c)
|
||||||
|
}
|
||||||
|
// Supprimer depuis Favoris ne retire que le lien.
|
||||||
|
call(t, srv, "alice@x.fr", "DELETE", "/api/entry?path=.favoris/a.excalidraw", "", nil)
|
||||||
|
if c := call(t, srv, "alice@x.fr", "GET", "/api/file?path=a.excalidraw", "", nil); c != 200 {
|
||||||
|
t.Fatalf("original supprimé avec le lien : %d", c)
|
||||||
|
}
|
||||||
|
call(t, srv, "alice@x.fr", "GET", "/api/list?path=.favoris", "", &favs)
|
||||||
|
if len(favs) != 1 {
|
||||||
|
t.Fatalf("favoris après retrait = %+v", favs)
|
||||||
|
}
|
||||||
|
// Écrire dans .favoris est refusé.
|
||||||
|
if c := call(t, srv, "alice@x.fr", "PUT", "/api/file?path=.favoris/x.excalidraw", doc, nil); c != 400 {
|
||||||
|
t.Fatalf("écriture dans Favoris : %d", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUserID(t *testing.T) {
|
||||||
|
for in, want := range map[string]string{
|
||||||
|
" Cedric.Killy@VLab.bzh ": "cedric.killy@vlab.bzh",
|
||||||
|
"jean dupont/../x": "jean_dupont_.._x",
|
||||||
|
"..": "_.",
|
||||||
|
"": LocalUser,
|
||||||
|
} {
|
||||||
|
if got := UserID(in); got != want {
|
||||||
|
t.Errorf("UserID(%q) = %q ; attendu %q", in, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSpaces(t *testing.T) *Spaces {
|
||||||
|
t.Helper()
|
||||||
|
return NewSpaces(newStore(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
func space(t *testing.T, sp *Spaces, id string) *Space {
|
||||||
|
t.Helper()
|
||||||
|
u, err := sp.For(id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
func write(t *testing.T, u *Space, p string) {
|
||||||
|
t.Helper()
|
||||||
|
if _, _, err := u.Write(p, []byte(doc), "", true); err != nil {
|
||||||
|
t.Fatalf("écriture de %s : %v", p, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func favPaths(t *testing.T, u *Space) []string {
|
||||||
|
t.Helper()
|
||||||
|
es, err := u.Favorites()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
out := []string{}
|
||||||
|
for _, e := range es {
|
||||||
|
if e.Broken {
|
||||||
|
out = append(out, "!"+e.Path)
|
||||||
|
} else {
|
||||||
|
out = append(out, e.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func same(a, b []string) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range a {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSpacesListAndPrivacy(t *testing.T) {
|
||||||
|
sp := newSpaces(t)
|
||||||
|
a, b := space(t, sp, "a@x.fr"), space(t, sp, "b@x.fr")
|
||||||
|
write(t, a, ".perso/secret.excalidraw")
|
||||||
|
write(t, a, "commun.excalidraw")
|
||||||
|
|
||||||
|
l, err := a.List("")
|
||||||
|
if err != nil || len(l) != 3 || l[0].Special != "favoris" || l[1].Special != "perso" || l[2].Path != "commun.excalidraw" {
|
||||||
|
t.Fatalf("racine = %+v, %v", l, err)
|
||||||
|
}
|
||||||
|
if l, _ := a.List(".perso"); len(l) != 1 || l[0].Path != ".perso/secret.excalidraw" {
|
||||||
|
t.Fatalf("espace personnel de a = %+v", l)
|
||||||
|
}
|
||||||
|
if l, _ := b.List(".perso"); len(l) != 0 {
|
||||||
|
t.Fatalf("b voit l'espace de a : %+v", l)
|
||||||
|
}
|
||||||
|
if _, _, err := b.Read(".perso/secret.excalidraw"); !errors.Is(err, ErrNotFound) {
|
||||||
|
t.Fatalf("b lit le schéma de a : %v", err)
|
||||||
|
}
|
||||||
|
// Ni l'arborescence commune ni la recherche ne montrent .users.
|
||||||
|
if _, _, err := b.Read(".users/a@x.fr/home/secret.excalidraw"); !errors.Is(err, ErrInvalidPath) {
|
||||||
|
t.Fatalf("accès direct à .users : %v", err)
|
||||||
|
}
|
||||||
|
if res, _ := b.Search("secret", Filter{}, 10); len(res) != 0 {
|
||||||
|
t.Fatalf("recherche de b = %+v", res)
|
||||||
|
}
|
||||||
|
if res, _ := a.Search("secret", Filter{}, 10); len(res) != 1 || res[0].Path != ".perso/secret.excalidraw" {
|
||||||
|
t.Fatalf("recherche de a = %+v", res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSpacesFavorites(t *testing.T) {
|
||||||
|
sp := newSpaces(t)
|
||||||
|
a, b := space(t, sp, "a@x.fr"), space(t, sp, "b@x.fr")
|
||||||
|
write(t, a, "d/s.excalidraw")
|
||||||
|
write(t, a, ".perso/p.excalidraw")
|
||||||
|
if err := a.SetFavorite([]string{"d"}, true); !errors.Is(err, ErrInvalidPath) {
|
||||||
|
t.Fatalf("dossier en favori : %v", err)
|
||||||
|
}
|
||||||
|
if err := a.SetFavorite([]string{"d/s.excalidraw", ".perso/p.excalidraw"}, true); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := b.SetFavorite([]string{"d/s.excalidraw"}, true); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := favPaths(t, a); !same(got, []string{".perso/p.excalidraw", "d/s.excalidraw"}) {
|
||||||
|
t.Fatalf("favoris de a = %v", got)
|
||||||
|
}
|
||||||
|
if l, _ := a.List("d"); len(l) != 1 || !l[0].Favorite {
|
||||||
|
t.Fatalf("étoile absente : %+v", l)
|
||||||
|
}
|
||||||
|
if res, _ := a.Search("", Filter{Favorite: true}, 10); len(res) != 2 {
|
||||||
|
t.Fatalf("filtre favoris = %+v", res)
|
||||||
|
}
|
||||||
|
// Le lien est relatif et vise l'original.
|
||||||
|
fi, err := os.Lstat(filepath.Join(sp.favDir("a@x.fr"), "s.excalidraw"))
|
||||||
|
if err != nil || fi.Mode()&os.ModeSymlink == 0 {
|
||||||
|
t.Fatalf("lien symbolique attendu : %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renommage du dossier : les favoris des deux utilisateurs suivent.
|
||||||
|
if _, err := a.Move("d", "e"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := favPaths(t, b); !same(got, []string{"e/s.excalidraw"}) {
|
||||||
|
t.Fatalf("favoris de b après renommage = %v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passage dans l'espace personnel de a : a garde son favori, b le perd.
|
||||||
|
if _, err := a.Move("e/s.excalidraw", ".perso/s.excalidraw"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := favPaths(t, a); !same(got, []string{".perso/p.excalidraw", ".perso/s.excalidraw"}) {
|
||||||
|
t.Fatalf("favoris de a après déplacement = %v", got)
|
||||||
|
}
|
||||||
|
if got := favPaths(t, b); len(got) != 0 {
|
||||||
|
t.Fatalf("b garde un favori vers l'espace de a : %v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retour dans l'arborescence commune par MoveMany.
|
||||||
|
res, err := a.MoveMany([]string{".perso/p.excalidraw"}, "")
|
||||||
|
if err != nil || res[0].To != "p.excalidraw" || res[0].Err() != nil {
|
||||||
|
t.Fatalf("MoveMany = %+v, %v", res, err)
|
||||||
|
}
|
||||||
|
if got := favPaths(t, a); !same(got, []string{"p.excalidraw", ".perso/s.excalidraw"}) {
|
||||||
|
t.Fatalf("favoris de a après MoveMany = %v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suppression : le favori disparaît.
|
||||||
|
if err := a.Delete("p.excalidraw", false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := favPaths(t, a); !same(got, []string{".perso/s.excalidraw"}) {
|
||||||
|
t.Fatalf("favoris de a après suppression = %v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original supprimé hors de l'application : favori cassé, retirable.
|
||||||
|
if err := os.Remove(filepath.Join(a.home.root, "s.excalidraw")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := favPaths(t, a); !same(got, []string{"!.favoris/s.excalidraw"}) {
|
||||||
|
t.Fatalf("favori cassé = %v", got)
|
||||||
|
}
|
||||||
|
if err := a.Delete(".favoris/s.excalidraw", false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := favPaths(t, a); len(got) != 0 {
|
||||||
|
t.Fatalf("favori cassé non retiré : %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSpacesFavoriteNames(t *testing.T) {
|
||||||
|
sp := newSpaces(t)
|
||||||
|
a := space(t, sp, "a@x.fr")
|
||||||
|
write(t, a, "x/s.excalidraw")
|
||||||
|
write(t, a, "y/s.excalidraw")
|
||||||
|
if err := a.SetFavorite([]string{"x/s.excalidraw", "y/s.excalidraw", "x/s.excalidraw"}, true); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
es, _ := a.Favorites()
|
||||||
|
if len(es) != 2 || es[0].Link == es[1].Link {
|
||||||
|
t.Fatalf("favoris homonymes = %+v", es)
|
||||||
|
}
|
||||||
|
if err := a.SetFavorite([]string{"y/s.excalidraw"}, false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := favPaths(t, a); !same(got, []string{"x/s.excalidraw"}) {
|
||||||
|
t.Fatalf("après retrait = %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSpacesSeedFromMeta(t *testing.T) {
|
||||||
|
sp := newSpaces(t)
|
||||||
|
write(t, space(t, sp, "a@x.fr"), "old.excalidraw")
|
||||||
|
if err := sp.Main.SetFavorite([]string{"old.excalidraw"}, true); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Un nouvel utilisateur reprend les favoris communs, une seule fois.
|
||||||
|
c := space(t, sp, "c@x.fr")
|
||||||
|
if got := favPaths(t, c); !same(got, []string{"old.excalidraw"}) {
|
||||||
|
t.Fatalf("favoris repris = %v", got)
|
||||||
|
}
|
||||||
|
if err := c.SetFavorite([]string{"old.excalidraw"}, false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
sp.homes = map[string]*Store{} // redémarrage
|
||||||
|
if got := favPaths(t, space(t, sp, "c@x.fr")); len(got) != 0 {
|
||||||
|
t.Fatalf("favoris repris deux fois : %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSpacesSharesAndCrossMove(t *testing.T) {
|
||||||
|
sp := newSpaces(t)
|
||||||
|
a := space(t, sp, "a@x.fr")
|
||||||
|
write(t, a, ".perso/p.excalidraw")
|
||||||
|
if err := a.EditTags([]string{".perso/p.excalidraw"}, []string{"NSX"}, nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := a.home.Snapshot("p.excalidraw"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
sh, err := a.CreateShare(".perso/p.excalidraw", 0)
|
||||||
|
if err != nil || sh.Path != ".perso/p.excalidraw" {
|
||||||
|
t.Fatalf("partage = %+v, %v", sh, err)
|
||||||
|
}
|
||||||
|
if got, _, err := sp.ReadShared(sh.Token); err != nil || got.Path != "p.excalidraw" {
|
||||||
|
t.Fatalf("lien personnel : %+v, %v", got, err)
|
||||||
|
}
|
||||||
|
// Vers l'arborescence commune : tags, historique et lien suivent.
|
||||||
|
if _, err := a.Move(".perso/p.excalidraw", "q.excalidraw"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if l, _ := a.List(""); len(l) != 3 || len(l[2].Tags) != 1 {
|
||||||
|
t.Fatalf("tags après déplacement : %+v", l)
|
||||||
|
}
|
||||||
|
if vs, err := a.Versions("q.excalidraw"); err != nil || len(vs) != 1 {
|
||||||
|
t.Fatalf("historique après déplacement : %+v, %v", vs, err)
|
||||||
|
}
|
||||||
|
if got, _, err := sp.ReadShared(sh.Token); err != nil || got.Path != "q.excalidraw" {
|
||||||
|
t.Fatalf("lien après déplacement : %+v, %v", got, err)
|
||||||
|
}
|
||||||
|
if tags, _ := a.Tags(); len(tags) != 1 || tags[0].Count != 1 {
|
||||||
|
t.Fatalf("tags = %+v", tags)
|
||||||
|
}
|
||||||
|
// Révocation globale : clé changée, liens personnels compris.
|
||||||
|
sh2, _ := a.CreateShare("q.excalidraw", 0)
|
||||||
|
write(t, a, ".perso/r.excalidraw")
|
||||||
|
sh3, _ := a.CreateShare(".perso/r.excalidraw", 0)
|
||||||
|
if err := a.RevokeAllShares(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, tok := range []string{sh2.Token, sh3.Token} {
|
||||||
|
if _, _, err := sp.ReadShared(tok); !errors.Is(err, ErrNotFound) {
|
||||||
|
t.Fatalf("lien encore valide après révocation : %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user