Merge pull request #2560 from pedropintosilva/feat/add-userextrainfo-to-checkfileinfo

feat(collaboration): add UserExtraInfo with avatar and mail to CheckFileInfo
This commit is contained in:
Jörn Friedrich Dreyer
2026-04-15 16:07:42 +02:00
committed by GitHub
11 changed files with 376 additions and 33 deletions

View File

@@ -3,5 +3,3 @@ module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT
go 1.23.4
require github.com/gosexy/gettext v0.0.0-20160830220431-74466a0a0c4a // go-xgettext
require github.com/jessevdk/go-flags v1.6.1 // indirect

View File

@@ -169,6 +169,74 @@ func (_c *FileConnectorService_DeleteFile_Call) RunAndReturn(run func(ctx contex
return _c
}
// GetAvatar provides a mock function for the type FileConnectorService
func (_mock *FileConnectorService) GetAvatar(ctx context.Context, userID string) (*connector.ConnectorResponse, error) {
ret := _mock.Called(ctx, userID)
if len(ret) == 0 {
panic("no return value specified for GetAvatar")
}
var r0 *connector.ConnectorResponse
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*connector.ConnectorResponse, error)); ok {
return returnFunc(ctx, userID)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string) *connector.ConnectorResponse); ok {
r0 = returnFunc(ctx, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*connector.ConnectorResponse)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = returnFunc(ctx, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// FileConnectorService_GetAvatar_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAvatar'
type FileConnectorService_GetAvatar_Call struct {
*mock.Call
}
// GetAvatar is a helper method to define mock.On call
// - ctx context.Context
// - userID string
func (_e *FileConnectorService_Expecter) GetAvatar(ctx interface{}, userID interface{}) *FileConnectorService_GetAvatar_Call {
return &FileConnectorService_GetAvatar_Call{Call: _e.mock.On("GetAvatar", ctx, userID)}
}
func (_c *FileConnectorService_GetAvatar_Call) Run(run func(ctx context.Context, userID string)) *FileConnectorService_GetAvatar_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *FileConnectorService_GetAvatar_Call) Return(connectorResponse *connector.ConnectorResponse, err error) *FileConnectorService_GetAvatar_Call {
_c.Call.Return(connectorResponse, err)
return _c
}
func (_c *FileConnectorService_GetAvatar_Call) RunAndReturn(run func(ctx context.Context, userID string) (*connector.ConnectorResponse, error)) *FileConnectorService_GetAvatar_Call {
_c.Call.Return(run)
return _c
}
// GetLock provides a mock function for the type FileConnectorService
func (_mock *FileConnectorService) GetLock(ctx context.Context) (*connector.ConnectorResponse, error) {
ret := _mock.Called(ctx)

View File

@@ -23,6 +23,7 @@ import (
"github.com/opencloud-eu/reva/v2/pkg/store"
"github.com/spf13/cobra"
"go-micro.dev/v4/selector"
microstore "go-micro.dev/v4/store"
)
@@ -138,7 +139,7 @@ func Server(cfg *config.Config) *cobra.Command {
// start HTTP server
httpServer, err := http.Server(
http.Adapter(connector.NewHttpAdapter(gatewaySelector, cfg, st)),
http.Adapter(connector.NewHttpAdapter(gatewaySelector, cfg, st, selector.NewSelector(selector.Registry(registry.GetRegistry())))),
http.Logger(logger),
http.Config(cfg),
http.Context(ctx),

View File

@@ -8,11 +8,15 @@ import (
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strings"
"time"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"go-micro.dev/v4/selector"
appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
auth "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
@@ -94,23 +98,29 @@ type FileConnectorService interface {
// In case of conflict, this method will return the actual lockId in
// the file as second return value.
RenameFile(ctx context.Context, lockID, target string) (*ConnectorResponse, error)
// GetAvatar fetches the user's avatar image from the Graph API.
// The response Body contains the raw image bytes ([]byte) and the
// Headers contain Content-Type and Cache-Control.
GetAvatar(ctx context.Context, userID string) (*ConnectorResponse, error)
}
// FileConnector implements the "File" endpoint.
// Currently, it handles file locks and getting the file info.
// Note that operations might return any kind of error, not just ConnectorError
type FileConnector struct {
gws pool.Selectable[gatewayv1beta1.GatewayAPIClient]
cfg *config.Config
store microstore.Store
gws pool.Selectable[gatewayv1beta1.GatewayAPIClient]
cfg *config.Config
store microstore.Store
graphSelector selector.Selector
}
// NewFileConnector creates a new file connector
func NewFileConnector(gws pool.Selectable[gatewayv1beta1.GatewayAPIClient], cfg *config.Config, st microstore.Store) *FileConnector {
func NewFileConnector(gws pool.Selectable[gatewayv1beta1.GatewayAPIClient], cfg *config.Config, st microstore.Store, graphSelector selector.Selector) *FileConnector {
return &FileConnector{
gws: gws,
cfg: cfg,
store: st,
gws: gws,
cfg: cfg,
store: st,
graphSelector: graphSelector,
}
}
@@ -1303,6 +1313,18 @@ func (f *FileConnector) CheckFileInfo(ctx context.Context) (*ConnectorResponse,
}
}
if !isPublicShare && !isAnonymousUser {
extraInfo := &fileinfo.UserExtraInfo{
Mail: user.GetMail(),
}
// Build a WOPI-proxied avatar URL so Collabora can load it via img.src
// without needing auth headers (the token is in the query string).
if avatarURL, err := f.createAvatarURL(wopiContext, collaborationURL, user.GetId().GetOpaqueId()); err == nil {
extraInfo.Avatar = avatarURL
}
infoMap[fileinfo.KeyUserExtraInfo] = extraInfo
}
// if the file content is empty and a template reference is set, add the template source URL
if wopiContext.TemplateReference != nil && statRes.GetInfo().GetSize() == 0 {
if tu, err := f.createDownloadURL(wopiContext, collaborationURL); err == nil {
@@ -1340,6 +1362,22 @@ func (f *FileConnector) createDownloadURL(wopiContext middleware.WopiContext, co
return downloadURL.String(), nil
}
// createAvatarURL builds a WOPI-proxied avatar URL for the given user.
// The collaboration service's /wopi/avatars/ endpoint will fetch the avatar
// from the Graph API using the WOPI token for authentication.
func (f *FileConnector) createAvatarURL(wopiContext middleware.WopiContext, collaborationURL *url.URL, userID string) (string, error) {
token, _, err := middleware.GenerateWopiToken(wopiContext, f.cfg, f.store)
if err != nil {
return "", err
}
avatarURL := *collaborationURL
avatarURL.Path = path.Join(collaborationURL.Path, "wopi/avatars/", userID)
q := avatarURL.Query()
q.Add("access_token", token)
avatarURL.RawQuery = q.Encode()
return avatarURL.String(), nil
}
func createHostUrl(mode string, u *url.URL, appName string, info *providerv1beta1.ResourceInfo) string {
webUrl := createAppExternalURL(u, appName, info)
addURLParams(webUrl, map[string]string{"view_mode": mode})
@@ -1512,3 +1550,70 @@ func (f *FileConnector) getScopeByKeyPrefix(scopes map[string]*auth.Scope, keyPr
}
return fmt.Errorf("scope %s not found", keyPrefix)
}
// GetAvatar fetches the user's avatar image from the Graph API.
// The returned ConnectorResponse carries the raw image bytes in Body ([]byte)
// and Content-Type / Cache-Control in Headers.
func (f *FileConnector) GetAvatar(ctx context.Context, userID string) (*ConnectorResponse, error) {
logger := zerolog.Ctx(ctx)
wopiContext, err := middleware.WopiContextFromCtx(ctx)
if err != nil {
logger.Error().Err(err).Msg("GetAvatar: missing WOPI context")
return nil, NewConnectorError(http.StatusUnauthorized, "missing WOPI context")
}
lgClient, err := f.setupLibregraphClient(ctx, wopiContext.AccessToken)
if err != nil {
logger.Error().Err(err).Msg("GetAvatar: failed to setup libregraph client")
return nil, NewConnectorError(http.StatusInternalServerError, "failed to setup libregraph client")
}
photoFile, httpResp, err := lgClient.UserPhotoApi.GetUserPhoto(ctx, userID).Execute()
if err != nil {
logger.Error().Err(err).Msg("GetAvatar: failed to fetch avatar from Graph API")
if httpResp != nil {
return nil, NewConnectorError(httpResp.StatusCode, http.StatusText(httpResp.StatusCode))
}
return nil, NewConnectorError(http.StatusBadGateway, "failed to fetch avatar")
}
defer photoFile.Close()
data, err := io.ReadAll(photoFile)
if err != nil {
logger.Error().Err(err).Msg("GetAvatar: failed to read avatar body")
return nil, NewConnectorError(http.StatusInternalServerError, "failed to read avatar body")
}
headers := map[string]string{
"Cache-Control": "public, max-age=300",
}
if ct := httpResp.Header.Get("Content-Type"); ct != "" {
headers["Content-Type"] = ct
}
return &ConnectorResponse{
Status: http.StatusOK,
Headers: headers,
Body: data,
}, nil
}
func (f *FileConnector) setupLibregraphClient(_ context.Context, cs3token string) (*libregraph.APIClient, error) {
next, err := f.graphSelector.Select("eu.opencloud.web.graph")
if err != nil {
return nil, err
}
node, err := next()
if err != nil {
return nil, err
}
lgconf := libregraph.NewConfiguration()
lgconf.Servers = libregraph.ServerConfigurations{
{
URL: fmt.Sprintf("%s://%s/graph", node.Metadata["protocol"], node.Address),
},
}
lgconf.DefaultHeader = map[string]string{ctxpkg.TokenHeader: cs3token}
return libregraph.NewAPIClient(lgconf), nil
}

View File

@@ -65,7 +65,7 @@ var _ = Describe("FileConnector", func() {
gatewaySelector = mocks.NewSelectable[gateway.GatewayAPIClient](GinkgoT())
gatewaySelector.On("Next").Return(gatewayClient, nil)
fc = connector.NewFileConnector(gatewaySelector, cfg, nil)
fc = connector.NewFileConnector(gatewaySelector, cfg, nil, nil)
wopiCtx = middleware.WopiContext{
// a real token is needed for the PutRelativeFileSuggested tests
@@ -1996,13 +1996,24 @@ var _ = Describe("FileConnector", func() {
EnableInsertRemoteImage: true,
EnableInsertRemoteFile: true,
IsAdminUser: true,
UserExtraInfo: &fileinfo.UserExtraInfo{
Mail: "shaft@example.com",
},
}
response, err := fc.CheckFileInfo(ctx)
Expect(err).ToNot(HaveOccurred())
Expect(response.Status).To(Equal(200))
Expect(response.Body.(*fileinfo.Collabora)).To(Equal(expectedFileInfo))
body := response.Body.(*fileinfo.Collabora)
// Avatar URL contains a dynamic WOPI token, so check prefix separately
Expect(body.UserExtraInfo).ToNot(BeNil())
Expect(body.UserExtraInfo.Avatar).To(ContainSubstring("/wopi/avatars/aabbcc?access_token="))
Expect(body.UserExtraInfo.Mail).To(Equal("shaft@example.com"))
// Clear dynamic fields for struct comparison
body.UserExtraInfo.Avatar = ""
expectedFileInfo.UserExtraInfo.Avatar = ""
Expect(body).To(Equal(expectedFileInfo))
})
It("Stat success with template", func() {
wopiCtx.TemplateReference = &providerv1beta1.Reference{
@@ -2097,4 +2108,18 @@ var _ = Describe("FileConnector", func() {
Expect(templateSource).To(HavePrefix(expectedTemplateSource))
})
})
Describe("GetAvatar", func() {
It("No valid context returns Unauthorized error", func() {
// GetAvatar returns before touching the gateway selector.
gatewaySelector.EXPECT().Next().Unset()
ctx := context.Background()
response, err := fc.GetAvatar(ctx, "user-123")
Expect(response).To(BeNil())
Expect(err).To(HaveOccurred())
var connErr *connector.ConnectorError
Expect(errors.As(err, &connErr)).To(BeTrue())
Expect(connErr.HttpCodeOut).To(Equal(401))
})
})
})

View File

@@ -1,5 +1,13 @@
package fileinfo
// UserExtraInfo contains additional user info shared across collaborative
// editing views, such as the user's avatar image and email.
// https://sdk.collaboraonline.com/docs/advanced_integration.html#userextrainfo
type UserExtraInfo struct {
Avatar string `json:"avatar,omitempty"`
Mail string `json:"mail,omitempty"`
}
// Collabora fileInfo properties
//
// Collabora WOPI check file info specification:
@@ -64,7 +72,8 @@ type Collabora struct {
IsAnonymousUser bool `json:"IsAnonymousUser,omitempty"`
// JSON object that contains additional info about the user, namely the avatar image.
//UserExtraInfo -> requires definition, currently not used
// Shared among all views in collaborative editing sessions.
UserExtraInfo *UserExtraInfo `json:"UserExtraInfo,omitempty"`
// JSON object that contains additional info about the user, but unlike the UserExtraInfo it is not shared among the views in collaborative editing sessions.
//UserPrivateInfo -> requires definition, currently not used
@@ -135,7 +144,8 @@ func (cinfo *Collabora) SetProperties(props map[string]interface{}) {
cinfo.SaveAsPostmessage = value.(bool)
case KeyEnableOwnerTermination:
cinfo.EnableOwnerTermination = value.(bool)
//UserExtraInfo -> requires definition, currently not used
case KeyUserExtraInfo:
cinfo.UserExtraInfo = value.(*UserExtraInfo)
//UserPrivateInfo -> requires definition, currently not used
case KeyWatermarkText:
cinfo.WatermarkText = value.(string)

View File

@@ -114,7 +114,7 @@ const (
KeyDownloadAsPostMessage = "DownloadAsPostMessage"
KeySaveAsPostmessage = "SaveAsPostmessage"
KeyEnableOwnerTermination = "EnableOwnerTermination"
//KeyUserExtraInfo -> requires definition, currently not used
KeyUserExtraInfo = "UserExtraInfo"
//KeyUserPrivateInfo -> requires definition, currently not used
KeyWatermarkText = "WatermarkText"

View File

@@ -6,12 +6,15 @@ import (
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
"github.com/opencloud-eu/opencloud/services/collaboration/pkg/config"
"github.com/opencloud-eu/opencloud/services/collaboration/pkg/connector/utf7"
"github.com/opencloud-eu/opencloud/services/collaboration/pkg/locks"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/rs/zerolog"
"go-micro.dev/v4/selector"
microstore "go-micro.dev/v4/store"
)
@@ -45,10 +48,10 @@ type HttpAdapter struct {
// NewHttpAdapter will create a new HTTP adapter. A new connector using the
// provided gateway API client and configuration will be used in the adapter
func NewHttpAdapter(gws pool.Selectable[gatewayv1beta1.GatewayAPIClient], cfg *config.Config, st microstore.Store) *HttpAdapter {
func NewHttpAdapter(gws pool.Selectable[gatewayv1beta1.GatewayAPIClient], cfg *config.Config, st microstore.Store, graphSelector selector.Selector) *HttpAdapter {
httpAdapter := &HttpAdapter{
con: NewConnector(
NewFileConnector(gws, cfg, st),
NewFileConnector(gws, cfg, st, graphSelector),
NewContentConnector(gws, cfg),
),
}
@@ -301,6 +304,50 @@ func (h *HttpAdapter) RenameFile(w http.ResponseWriter, r *http.Request) {
h.writeConnectorResponse(w, r, response)
}
// GetAvatar proxies the user's avatar from the Graph API.
// The WOPI token in the query string provides authentication (validated by
// the WopiContextAuthMiddleware). Collabora loads avatars via img.src which
// is a plain browser GET — it cannot send auth headers.
func (h *HttpAdapter) GetAvatar(w http.ResponseWriter, r *http.Request) {
logger := zerolog.Ctx(r.Context())
userID := chi.URLParam(r, "userID")
if userID == "" {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
fileCon := h.con.GetFileConnector()
response, err := fileCon.GetAvatar(r.Context(), userID)
if err != nil {
var connErr *ConnectorError
if errors.As(err, &connErr) {
http.Error(w, http.StatusText(connErr.HttpCodeOut), connErr.HttpCodeOut)
} else {
logger.Error().Err(err).Msg("GetAvatar: failed to fetch avatar")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
return
}
h.writeConnectorAvatarResponse(w, r, response)
}
func (h *HttpAdapter) writeConnectorAvatarResponse(w http.ResponseWriter, r *http.Request, response *ConnectorResponse) {
data, _ := response.Body.([]byte)
for key, value := range response.Headers {
w.Header().Set(key, value)
}
w.WriteHeader(response.Status)
written, err := w.Write(data)
if err != nil {
logger := zerolog.Ctx(r.Context())
logger.Error().
Err(err).
Int("TotalBytes", len(data)).
Int("WrittenBytes", written).
Msg("failed to write avatar contents in the HTTP response")
}
}
func (h *HttpAdapter) writeConnectorResponse(w http.ResponseWriter, r *http.Request, response *ConnectorResponse) {
jsonBody := []byte{}
if response.Body != nil {

View File

@@ -1,6 +1,7 @@
package connector_test
import (
"context"
"encoding/json"
"errors"
"io"
@@ -8,6 +9,7 @@ import (
"strings"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/go-chi/chi/v5"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/opencloud-eu/opencloud/services/collaboration/mocks"
@@ -538,6 +540,72 @@ var _ = Describe("HttpAdapter", func() {
})
})
Describe("GetAvatar", func() {
It("Missing userID returns 400", func() {
// No chi route context means chi.URLParam returns ""
req := httptest.NewRequest("GET", "/wopi/avatars/", nil)
w := httptest.NewRecorder()
httpAdapter.GetAvatar(w, req)
Expect(w.Result().StatusCode).To(Equal(400))
})
It("ConnectorError propagates the status code", func() {
req := httptest.NewRequest("GET", "/wopi/avatars/user-123", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("userID", "user-123")
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
w := httptest.NewRecorder()
fc.On("GetAvatar", mock.Anything, "user-123").Times(1).
Return(nil, connector.NewConnectorError(502, "Bad Gateway"))
httpAdapter.GetAvatar(w, req)
Expect(w.Result().StatusCode).To(Equal(502))
})
It("General error returns 500", func() {
req := httptest.NewRequest("GET", "/wopi/avatars/user-123", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("userID", "user-123")
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
w := httptest.NewRecorder()
fc.On("GetAvatar", mock.Anything, "user-123").Times(1).
Return(nil, errors.New("unexpected failure"))
httpAdapter.GetAvatar(w, req)
Expect(w.Result().StatusCode).To(Equal(500))
})
It("Success writes Content-Type, Cache-Control and body", func() {
req := httptest.NewRequest("GET", "/wopi/avatars/user-123", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("userID", "user-123")
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
w := httptest.NewRecorder()
avatarData := []byte{0xFF, 0xD8, 0xFF, 0xE0} // fake JPEG bytes
fc.On("GetAvatar", mock.Anything, "user-123").Times(1).Return(
&connector.ConnectorResponse{
Status: 200,
Headers: map[string]string{
"Content-Type": "image/jpeg",
"Cache-Control": "public, max-age=300",
},
Body: avatarData,
}, nil)
httpAdapter.GetAvatar(w, req)
resp := w.Result()
Expect(resp.StatusCode).To(Equal(200))
Expect(resp.Header.Get("Content-Type")).To(Equal("image/jpeg"))
Expect(resp.Header.Get("Cache-Control")).To(Equal("public, max-age=300"))
body, _ := io.ReadAll(resp.Body)
Expect(body).To(Equal(avatarData))
})
})
Describe("PutRelativeFile", func() {
It("Connector error", func() {
contentBody := "this is the new fake content"

View File

@@ -143,22 +143,28 @@ func WopiContextAuthMiddleware(cfg *config.Config, st microstore.Store, next htt
Logger()
ctx = wopiLogger.WithContext(ctx)
hashedRef := helpers.HashResourceId(claims.WopiContext.FileReference.GetResourceId())
fileID := parseWopiFileID(cfg, r.URL.Path)
if claims.WopiContext.TemplateReference != nil {
hashedTemplateRef := helpers.HashResourceId(claims.WopiContext.TemplateReference.GetResourceId())
// the fileID could be one of the references within the access token if both are set
// because we can use the access token to get the contents of the template file
if fileID != hashedTemplateRef && fileID != hashedRef {
wopiLogger.Error().Msg("file reference in the URL doesn't match the one inside the access token")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
} else {
if fileID != hashedRef {
wopiLogger.Error().Msg("file reference in the URL doesn't match the one inside the access token")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
// Validate that the file ID in the URL matches the WOPI token's file
// reference. This check only applies to /wopi/files/ and /wopi/templates/
// paths. Other WOPI-authenticated endpoints (e.g. /wopi/avatars/) don't
// carry a file ID in the URL — they only need a valid WOPI token.
if strings.Contains(r.URL.Path, "/files/") || strings.Contains(r.URL.Path, "/templates/") {
hashedRef := helpers.HashResourceId(claims.WopiContext.FileReference.GetResourceId())
fileID := parseWopiFileID(cfg, r.URL.Path)
if claims.WopiContext.TemplateReference != nil {
hashedTemplateRef := helpers.HashResourceId(claims.WopiContext.TemplateReference.GetResourceId())
// the fileID could be one of the references within the access token if both are set
// because we can use the access token to get the contents of the template file
if fileID != hashedTemplateRef && fileID != hashedRef {
wopiLogger.Error().Msg("file reference in the URL doesn't match the one inside the access token")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
} else {
if fileID != hashedRef {
wopiLogger.Error().Msg("file reference in the URL doesn't match the one inside the access token")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
}
}

View File

@@ -194,5 +194,20 @@ func prepareRoutes(r *chi.Mux, options Options) {
adapter.GetFile(w, r)
})
})
// Avatar proxy: serves user avatars authenticated by the WOPI token.
// Collabora loads avatars via img.src (plain GET, no auth headers),
// so the token must be in the URL query string.
r.Route("/avatars/{userID}", func(r chi.Router) {
r.Use(
func(h stdhttp.Handler) stdhttp.Handler {
return colabmiddleware.WopiContextAuthMiddleware(options.Config, options.Store, h)
},
colabmiddleware.CollaborationTracingMiddleware,
)
r.Get("/", func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
adapter.GetAvatar(w, r)
})
})
})
}