github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/apps/serve.go (about)

     1  package apps
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"html/template"
     8  	"io"
     9  	"net/http"
    10  	"net/url"
    11  	"os"
    12  	"path"
    13  	"strings"
    14  
    15  	"github.com/cozy/cozy-stack/model/app"
    16  	"github.com/cozy/cozy-stack/model/feature"
    17  	"github.com/cozy/cozy-stack/model/instance"
    18  	"github.com/cozy/cozy-stack/model/intent"
    19  	"github.com/cozy/cozy-stack/model/permission"
    20  	"github.com/cozy/cozy-stack/model/session"
    21  	csettings "github.com/cozy/cozy-stack/model/settings"
    22  	"github.com/cozy/cozy-stack/model/sharing"
    23  	"github.com/cozy/cozy-stack/pkg/appfs"
    24  	"github.com/cozy/cozy-stack/pkg/assets"
    25  	"github.com/cozy/cozy-stack/pkg/config/config"
    26  	"github.com/cozy/cozy-stack/pkg/consts"
    27  	"github.com/cozy/cozy-stack/pkg/couchdb"
    28  	"github.com/cozy/cozy-stack/pkg/jsonapi"
    29  	"github.com/cozy/cozy-stack/pkg/registry"
    30  	"github.com/cozy/cozy-stack/web/auth"
    31  	"github.com/cozy/cozy-stack/web/middlewares"
    32  	"github.com/cozy/cozy-stack/web/settings"
    33  	"github.com/cozy/cozy-stack/web/statik"
    34  	"github.com/labstack/echo/v4"
    35  )
    36  
    37  // Serve is an handler for serving files from the VFS for a client-side app
    38  func Serve(c echo.Context) error {
    39  	method := c.Request().Method
    40  	if method != "GET" && method != "HEAD" {
    41  		return echo.NewHTTPError(http.StatusMethodNotAllowed, "Method "+method+" not allowed")
    42  	}
    43  
    44  	i := middlewares.GetInstance(c)
    45  	slug := c.Get("slug").(string)
    46  
    47  	if !i.OnboardingFinished {
    48  		return c.Redirect(http.StatusFound, i.PageURL("/", nil))
    49  	}
    50  
    51  	webapp, err := app.GetWebappBySlug(i, slug)
    52  	if err != nil {
    53  		if errors.Is(err, app.ErrNotFound) {
    54  			return handleAppNotFound(c, i, slug)
    55  		}
    56  		return err
    57  	}
    58  
    59  	route, file := webapp.FindRoute(path.Clean(c.Request().URL.Path))
    60  
    61  	if webapp.FromAppsDir {
    62  		// Save permissions in couchdb before loading an index page
    63  		if file == "" && webapp.Permissions() != nil {
    64  			_ = permission.ForceWebapp(i, webapp.Slug(), webapp.Permissions())
    65  		}
    66  
    67  		fs := app.FSForAppDir(slug)
    68  		return ServeAppFile(c, i, fs, webapp)
    69  	}
    70  
    71  	if file == "" || file == route.Index {
    72  		webapp = app.DoLazyUpdate(i, webapp, app.Copier(consts.WebappType, i), i.Registries()).(*app.WebappManifest)
    73  	}
    74  
    75  	switch webapp.State() {
    76  	case app.Installed:
    77  		// This legacy "installed" state is not used anymore with the addition
    78  		// of the registry. Change the webapp state to "ready" and serve the app
    79  		// file.
    80  		webapp.SetState(app.Ready)
    81  		if err := webapp.Update(i, nil); err != nil {
    82  			return err
    83  		}
    84  		fallthrough
    85  	case app.Ready:
    86  		return ServeAppFile(c, i, app.AppsFileServer(i), webapp)
    87  	default:
    88  		return echo.NewHTTPError(http.StatusServiceUnavailable, "Application is not ready")
    89  	}
    90  }
    91  
    92  // handleAppNotFound is used to render the error page when the user wants to
    93  // access an app that is not yet installed
    94  func handleAppNotFound(c echo.Context, i *instance.Instance, slug string) error {
    95  	// Used for the "collect" => "home" renaming
    96  	if slug == "collect" {
    97  		return c.Redirect(http.StatusMovedPermanently, i.DefaultRedirection().String())
    98  	}
    99  	// Used for the deprecated "onboarding" app
   100  	if slug == "onboarding" {
   101  		return c.Redirect(http.StatusMovedPermanently, i.DefaultRedirection().String())
   102  	}
   103  	i.Logger().WithNamespace("apps").Infof("App not found: %s", slug)
   104  	if _, err := registry.GetApplication(slug, i.Registries()); err != nil {
   105  		return app.ErrNotFound
   106  	}
   107  	if _, err := app.GetWebappBySlug(i, consts.StoreSlug); err != nil {
   108  		return app.ErrNotFound
   109  	}
   110  	u := i.SubDomain(consts.StoreSlug)
   111  	u.Fragment = "/discover/" + slug
   112  	return c.Redirect(http.StatusTemporaryRedirect, u.String())
   113  }
   114  
   115  // handleIntent will allow iframes from another app if the current app is
   116  // opened as an intent
   117  func handleIntent(c echo.Context, i *instance.Instance, slug, intentID string) {
   118  	intent := &intent.Intent{}
   119  	if err := couchdb.GetDoc(i, consts.Intents, intentID, intent); err != nil {
   120  		return
   121  	}
   122  	allowed := false
   123  	for _, service := range intent.Services {
   124  		if slug == service.Slug {
   125  			allowed = true
   126  		}
   127  	}
   128  	if !allowed {
   129  		return
   130  	}
   131  	parts := strings.SplitN(intent.Client, "/", 2)
   132  	if len(parts) < 2 || parts[0] != consts.Apps {
   133  		return
   134  	}
   135  	from := i.SubDomain(parts[1]).String()
   136  	if !config.GetConfig().CSPDisabled {
   137  		middlewares.AppendCSPRule(c, "frame-ancestors", from)
   138  	}
   139  }
   140  
   141  // ServeAppFile will serve the requested file using the specified application
   142  // manifest and appfs.FileServer context.
   143  //
   144  // It can be used to serve file application in another context than the VFS,
   145  // for instance for tests or development purposes where we want to serve an
   146  // application that is not installed on the user's instance. However this
   147  // procedure should not be used for standard applications, use the Serve method
   148  // for that.
   149  func ServeAppFile(c echo.Context, i *instance.Instance, fs appfs.FileServer, webapp *app.WebappManifest) error {
   150  	slug := webapp.Slug()
   151  	route, file := webapp.FindRoute(path.Clean(c.Request().URL.Path))
   152  	if route.NotFound() {
   153  		return echo.NewHTTPError(http.StatusNotFound, "Page not found")
   154  	}
   155  	if file == "" {
   156  		file = route.Index
   157  	}
   158  
   159  	sess, isLoggedIn := middlewares.GetSession(c)
   160  	if code := c.QueryParam("session_code"); code != "" {
   161  		// XXX we should always clear the session code to avoid it being
   162  		// reused, even if the user is already logged in and we don't want to
   163  		// create a new session
   164  		if checked := i.CheckAndClearSessionCode(code); checked && !isLoggedIn {
   165  			sessionID, err := auth.SetCookieForNewSession(c, session.NormalRun)
   166  			req := c.Request()
   167  			if err == nil {
   168  				if err = session.StoreNewLoginEntry(i, sessionID, "", req, "session_code", false); err != nil {
   169  					i.Logger().Errorf("Could not store session history %q: %s", sessionID, err)
   170  				}
   171  			}
   172  			redirect := req.URL
   173  			redirect.RawQuery = ""
   174  			return c.Redirect(http.StatusSeeOther, redirect.String())
   175  		}
   176  	}
   177  
   178  	filepath := path.Join("/", route.Folder, file)
   179  	isRobotsTxt := filepath == "/robots.txt"
   180  
   181  	if !route.Public && !isLoggedIn {
   182  		if isRobotsTxt {
   183  			if f, ok := assets.Get("/robots.txt", i.ContextName); ok {
   184  				_, err := io.Copy(c.Response(), f.Reader())
   185  				return err
   186  			}
   187  		}
   188  		if file != route.Index {
   189  			return echo.NewHTTPError(http.StatusUnauthorized, "You must be authenticated")
   190  		}
   191  		reqURL := c.Request().URL
   192  		subdomain := i.SubDomain(slug)
   193  		subdomain.Path = reqURL.Path
   194  		subdomain.RawQuery = reqURL.RawQuery
   195  		subdomain.Fragment = reqURL.Fragment
   196  		params := url.Values{
   197  			"redirect": {subdomain.String()},
   198  		}
   199  		if jwt := c.QueryParam("jwt"); jwt != "" {
   200  			params.Add("jwt", jwt)
   201  		}
   202  		return c.Redirect(http.StatusFound, i.PageURL("/auth/login", params))
   203  	}
   204  
   205  	version := webapp.Version()
   206  	shasum := webapp.Checksum()
   207  
   208  	if file != route.Index {
   209  		// If file is not the index, it is considered an asset of the application
   210  		// (JS, image, ...). For theses assets we check if it contains an unique
   211  		// identifier to help caching. In such case, a long cache (1 year) is set.
   212  		//
   213  		// A unique identifier is matched when the file base contains a "long"
   214  		// hexadecimal subpart between '.', of at least 10 characters: for instance
   215  		// "app.badf00dbadf00d.js".
   216  		if _, id := statik.ExtractAssetID(file); id != "" {
   217  			c.Response().Header().Set("Cache-Control", "max-age=31536000, immutable")
   218  		}
   219  
   220  		err := fs.ServeFileContent(c.Response(), c.Request(), slug, version, shasum, filepath)
   221  		if os.IsNotExist(err) {
   222  			if isRobotsTxt {
   223  				if f, ok := assets.Get("/robots.txt", i.ContextName); ok {
   224  					_, err = io.Copy(c.Response(), f.Reader())
   225  					return err
   226  				}
   227  			}
   228  			return echo.NewHTTPError(http.StatusNotFound, "Asset not found")
   229  		}
   230  		if err != nil {
   231  			return echo.NewHTTPError(http.StatusInternalServerError, err)
   232  		}
   233  
   234  		return nil
   235  	}
   236  
   237  	if !isLoggedIn {
   238  		doc, err := i.SettingsDocument()
   239  		if err == nil {
   240  			if to, ok := doc.M["moved_to"].(string); ok && to != "" {
   241  				subdomainType, _ := doc.M["moved_to_subdomain_type"].(string)
   242  				return renderMovedLink(c, i, to, subdomainType)
   243  			}
   244  		}
   245  	}
   246  
   247  	// For share by link, show the password page if it is password protected.
   248  	code := c.QueryParam("sharecode")
   249  	token, err := middlewares.TransformShortcodeToJWT(i, code)
   250  	if err == nil {
   251  		claims, err := middlewares.ExtractClaims(c, i, token)
   252  		if err == nil && claims.AudienceString() == consts.ShareAudience {
   253  			pdoc, err := permission.GetForShareCode(i, token)
   254  			if err == nil && pdoc.Password != nil && !middlewares.HasCookieForPassword(c, i, pdoc.ID()) {
   255  				return renderPasswordPage(c, i, pdoc.ID())
   256  			}
   257  		}
   258  	}
   259  
   260  	if intentID := c.QueryParam("intent"); intentID != "" {
   261  		handleIntent(c, i, slug, intentID)
   262  	}
   263  
   264  	// For index file, we inject the locale, the stack domain, and a token if the
   265  	// user is connected
   266  	content, err := fs.Open(slug, version, shasum, filepath)
   267  	if err != nil {
   268  		return err
   269  	}
   270  	defer content.Close()
   271  
   272  	buf, err := io.ReadAll(content)
   273  	if err != nil {
   274  		return err
   275  	}
   276  
   277  	// XXX: Force include Warnings template in all app indexes
   278  	tmplText := string(buf)
   279  	if closeTagIdx := strings.Index(tmplText, "</head>"); closeTagIdx >= 0 {
   280  		tmplText = tmplText[:closeTagIdx] + "\n{{.Warnings}}\n" + tmplText[closeTagIdx:]
   281  	} else {
   282  		needsOpenTag := true
   283  		if openTagIdx := strings.Index(tmplText, "<head>"); openTagIdx >= 0 {
   284  			needsOpenTag = false
   285  		}
   286  
   287  		if bodyTagIdx := strings.Index(tmplText, "<body>"); bodyTagIdx >= 0 {
   288  			before := tmplText[:bodyTagIdx]
   289  			after := tmplText[bodyTagIdx:]
   290  
   291  			tmplText = before
   292  
   293  			if needsOpenTag {
   294  				tmplText += "\n<head>"
   295  			}
   296  
   297  			tmplText += "\n{{.Warnings}}\n</head>\n" + after
   298  		}
   299  	}
   300  
   301  	tmpl, err := template.New(file).Parse(tmplText)
   302  	if err != nil {
   303  		i.Logger().WithNamespace("apps").Warnf("%s cannot be parsed as a template: %s", file, err)
   304  		return fs.ServeFileContent(c.Response(), c.Request(), slug, version, shasum, filepath)
   305  	}
   306  
   307  	sessID := ""
   308  	if isLoggedIn {
   309  		sessID = sess.ID()
   310  
   311  		if file == "" || file == route.Index {
   312  			if !route.Public {
   313  				if handled, err := middlewares.CheckOAuthClientsLimitExceeded(c); handled {
   314  					return err
   315  				}
   316  			}
   317  		}
   318  	}
   319  	params := buildServeParams(c, i, webapp, isLoggedIn, sessID)
   320  
   321  	generated := &bytes.Buffer{}
   322  	if err := tmpl.Execute(generated, params); err != nil {
   323  		i.Logger().WithNamespace("apps").Warnf("%s cannot be interpreted as a template: %s", file, err)
   324  		return c.Render(http.StatusInternalServerError, "error.html", echo.Map{
   325  			"Domain":       i.ContextualDomain(),
   326  			"ContextName":  i.ContextName,
   327  			"Locale":       i.Locale,
   328  			"Title":        i.TemplateTitle(),
   329  			"Favicon":      middlewares.Favicon(i),
   330  			"Illustration": "/images/generic-error.svg",
   331  			"Error":        "Error Application not supported Message",
   332  			"SupportEmail": i.SupportEmailAddress(),
   333  		})
   334  	}
   335  
   336  	res := c.Response()
   337  	res.Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8)
   338  	res.Header().Set("Cache-Control", "private, no-store, must-revalidate")
   339  	res.WriteHeader(http.StatusOK)
   340  	_, err = io.Copy(res, generated)
   341  	return err
   342  }
   343  
   344  func buildServeParams(
   345  	c echo.Context,
   346  	inst *instance.Instance,
   347  	webapp *app.WebappManifest,
   348  	isLoggedIn bool,
   349  	sessID string,
   350  ) serveParams {
   351  	token := getServeToken(c, inst, webapp, isLoggedIn, sessID)
   352  	tracking := false
   353  	settings, err := inst.SettingsDocument()
   354  	if err == nil {
   355  		if t, ok := settings.M["tracking"].(string); ok {
   356  			tracking = t == "true"
   357  		}
   358  	}
   359  	var subdomainsType string
   360  	switch config.GetConfig().Subdomains {
   361  	case config.FlatSubdomains:
   362  		subdomainsType = "flat"
   363  	case config.NestedSubdomains:
   364  		subdomainsType = "nested"
   365  	}
   366  
   367  	return serveParams{
   368  		Token:      token,
   369  		SubDomain:  subdomainsType,
   370  		Tracking:   tracking,
   371  		webapp:     webapp,
   372  		instance:   inst,
   373  		isLoggedIn: isLoggedIn,
   374  	}
   375  }
   376  
   377  func getServeToken(
   378  	c echo.Context,
   379  	inst *instance.Instance,
   380  	webapp *app.WebappManifest,
   381  	isLoggedIn bool,
   382  	sessID string,
   383  ) string {
   384  	sharecode := c.QueryParam("sharecode")
   385  	if sharecode == "" {
   386  		if isLoggedIn {
   387  			return inst.BuildAppToken(webapp.Slug(), sessID)
   388  		}
   389  		return ""
   390  	}
   391  
   392  	// XXX The sharecode can be used for share by links, or for Cozy to Cozy
   393  	// sharings. When it is used for a Cozy to Cozy sharing, it can be for a
   394  	// preview token, and we need to upgrade it to an interact token if the
   395  	// member has a known Cozy URL. We do this upgrade when serving the preview
   396  	// page, not when sending the invitation link by mail, because we want the
   397  	// same link to work (and be upgraded) after the user has accepted the
   398  	// sharing.
   399  	token, pdoc, err := permission.GetTokenAndPermissionsFromShortcode(inst, sharecode)
   400  	if err != nil || pdoc.Type != permission.TypeSharePreview {
   401  		return sharecode
   402  	}
   403  	sharingID := strings.Split(pdoc.SourceID, "/")
   404  	sharingDoc, err := sharing.FindSharing(inst, sharingID[1])
   405  	if err != nil || sharingDoc.ReadOnlyRules() {
   406  		return token
   407  	}
   408  	m, err := sharingDoc.FindMemberBySharecode(inst, token)
   409  	if err != nil {
   410  		return token
   411  	}
   412  	if m.Instance != "" && !m.ReadOnly && m.Status != sharing.MemberStatusRevoked {
   413  		memberIndex := 0
   414  		for i := range sharingDoc.Members {
   415  			if sharingDoc.Members[i].Instance == m.Instance {
   416  				memberIndex = i
   417  			}
   418  		}
   419  		interact, err := sharingDoc.GetInteractCode(inst, m, memberIndex)
   420  		if err == nil {
   421  			return interact
   422  		}
   423  	}
   424  	return token
   425  }
   426  
   427  func renderMovedLink(c echo.Context, i *instance.Instance, to, subdomainType string) error {
   428  	name, _ := csettings.PublicName(i)
   429  	link := *c.Request().URL
   430  	if u, err := url.Parse(to); err == nil {
   431  		parts := strings.SplitN(c.Request().Host, ".", 2)
   432  		app := parts[0]
   433  		if config.GetConfig().Subdomains == config.FlatSubdomains {
   434  			parts = strings.SplitN(app, "-", 2)
   435  			app = parts[len(parts)-1]
   436  		}
   437  		if subdomainType == "nested" {
   438  			link.Host = app + "." + u.Host
   439  		} else {
   440  			parts := strings.SplitN(u.Host, ".", 2)
   441  			link.Host = parts[0] + "-" + app + "." + parts[1]
   442  		}
   443  		link.Scheme = u.Scheme
   444  	}
   445  
   446  	return c.Render(http.StatusGone, "move_link.html", echo.Map{
   447  		"Domain":      i.ContextualDomain(),
   448  		"ContextName": i.ContextName,
   449  		"Locale":      i.Locale,
   450  		"Title":       i.Translate("Move Link Title", name),
   451  		"ThemeCSS":    middlewares.ThemeCSS(i),
   452  		"Favicon":     middlewares.Favicon(i),
   453  		"Link":        link.String(),
   454  	})
   455  }
   456  
   457  func renderPasswordPage(c echo.Context, inst *instance.Instance, permID string) error {
   458  	return c.Render(http.StatusUnauthorized, "share_by_link_password.html", echo.Map{
   459  		"Action":      inst.PageURL("/auth/share-by-link/password", nil),
   460  		"Domain":      inst.ContextualDomain(),
   461  		"ContextName": inst.ContextName,
   462  		"Locale":      inst.Locale,
   463  		"Title":       inst.TemplateTitle(),
   464  		"ThemeCSS":    middlewares.ThemeCSS(inst),
   465  		"Favicon":     middlewares.Favicon(inst),
   466  		"PermID":      permID,
   467  	})
   468  }
   469  
   470  // serveParams is a struct used for rendering the index.html of webapps. A
   471  // struct is used, and not a map, to have some methods declared on it. It
   472  // allows to be lazy when constructing the paths of the assets: if an asset is
   473  // not used in the template, the method won't be called and the stack can avoid
   474  // checking if this asset is dynamically overridden in this instance context.
   475  type serveParams struct {
   476  	Token      string
   477  	SubDomain  string
   478  	Tracking   bool
   479  	webapp     *app.WebappManifest
   480  	instance   *instance.Instance
   481  	isLoggedIn bool
   482  }
   483  
   484  func (s serveParams) CozyData() (string, error) {
   485  	data := map[string]interface{}{
   486  		"token":     s.Token,
   487  		"domain":    s.Domain(),
   488  		"subdomain": s.SubDomain,
   489  		"tracking":  s.Tracking,
   490  		"locale":    s.Locale(),
   491  		"app": map[string]interface{}{
   492  			"editor": s.AppEditor(),
   493  			"name":   s.AppName(),
   494  			"prefix": s.AppNamePrefix(),
   495  			"slug":   s.AppSlug(),
   496  			"icon":   s.IconPath(),
   497  		},
   498  		"flags":        s.GetFlags(),
   499  		"capabilities": s.GetCapabilities(),
   500  	}
   501  	bytes, err := json.Marshal(data)
   502  
   503  	if err != nil {
   504  		return "", err
   505  	}
   506  
   507  	return string(bytes), nil
   508  }
   509  
   510  func (s serveParams) Domain() string {
   511  	return s.instance.ContextualDomain()
   512  }
   513  
   514  func (s serveParams) ContextName() string {
   515  	return s.instance.ContextName
   516  }
   517  
   518  func (s serveParams) Locale() string {
   519  	return s.instance.Locale
   520  }
   521  
   522  func (s serveParams) AppSlug() string {
   523  	return s.webapp.Slug()
   524  }
   525  
   526  func (s serveParams) AppName() string {
   527  	return s.webapp.NameLocalized(s.instance.Locale)
   528  }
   529  
   530  func (s serveParams) AppEditor() string {
   531  	return s.webapp.Editor()
   532  }
   533  
   534  func (s serveParams) AppNamePrefix() string {
   535  	return s.webapp.NamePrefix()
   536  }
   537  
   538  func (s serveParams) IconPath() string {
   539  	return s.webapp.Icon()
   540  }
   541  
   542  func (s serveParams) Capabilities() (string, error) {
   543  	bytes, err := json.Marshal(s.GetCapabilities())
   544  	if err != nil {
   545  		return "", err
   546  	}
   547  	return string(bytes), nil
   548  }
   549  
   550  func (s serveParams) GetCapabilities() jsonapi.Object {
   551  	capabilities := settings.NewCapabilities(s.instance)
   552  	capabilities.SetID("")
   553  	return capabilities
   554  }
   555  
   556  func (s serveParams) Flags() (string, error) {
   557  	flags, err := feature.GetFlags(s.instance)
   558  	if err != nil {
   559  		return "{}", err
   560  	}
   561  	bytes, err := json.Marshal(flags)
   562  	if err != nil {
   563  		return "", err
   564  	}
   565  	return string(bytes), nil
   566  }
   567  
   568  func (s serveParams) GetFlags() *feature.Flags {
   569  	flags, err := feature.GetFlags(s.instance)
   570  	if err != nil {
   571  		flags = &feature.Flags{
   572  			M: map[string]interface{}{},
   573  		}
   574  	}
   575  	return flags
   576  }
   577  
   578  func (s serveParams) CozyBar() (template.HTML, error) {
   579  	return cozybarHTML(s.instance, s.isLoggedIn)
   580  }
   581  
   582  func (s serveParams) CozyClientJS() (template.HTML, error) {
   583  	return cozyclientjsHTML(s.instance)
   584  }
   585  
   586  func (s serveParams) CozyFonts() template.HTML {
   587  	return middlewares.CozyFonts(s.instance)
   588  }
   589  
   590  func (s serveParams) ThemeCSS() template.HTML {
   591  	return middlewares.ThemeCSS(s.instance)
   592  }
   593  
   594  func (s serveParams) Favicon() template.HTML {
   595  	return middlewares.Favicon(s.instance)
   596  }
   597  
   598  func (s serveParams) DefaultWallpaper() string {
   599  	return statik.AssetPath(
   600  		s.instance.ContextualDomain(),
   601  		"/images/default-wallpaper.jpg",
   602  		s.instance.ContextName)
   603  }
   604  
   605  func (s serveParams) Warnings() (template.HTML, error) {
   606  	return warningsHTML(s.instance, s.isLoggedIn)
   607  }
   608  
   609  var clientTemplate *template.Template
   610  var barTemplate *template.Template
   611  var warningsTemplate *template.Template
   612  
   613  // BuildTemplates ensure that cozy-client-js and the bar can be injected in templates
   614  func BuildTemplates() {
   615  	clientTemplate = template.Must(template.New("cozy-client-js").Funcs(middlewares.FuncsMap).Parse(`` +
   616  		`<script src="{{asset .Domain "/js/cozy-client.min.js" .ContextName}}"></script>`,
   617  	))
   618  
   619  	barTemplate = template.Must(template.New("cozy-bar").Funcs(middlewares.FuncsMap).Parse(`
   620  <link rel="stylesheet" type="text/css" href="{{asset .Domain "/fonts/fonts.css" .ContextName}}">
   621  <link rel="stylesheet" type="text/css" href="{{asset .Domain "/css/cozy-bar.min.css" .ContextName}}">
   622  <script src="{{asset .Domain "/js/cozy-bar.min.js" .ContextName}}"></script>`,
   623  	))
   624  
   625  	warningsTemplate = template.Must(template.New("warnings").Funcs(middlewares.FuncsMap).Parse(`
   626  {{if .LoggedIn}}
   627  {{range .Warnings}}
   628  <meta name="user-action-required" data-title="{{ .Title }}" data-code="{{ .Code }}" data-detail="{{ .Detail }}" {{with .Links}}{{with .Self}}data-links="{{ . }}"{{end}}{{end}} />
   629  {{end}}
   630  {{end}}`,
   631  	))
   632  }
   633  
   634  func cozyclientjsHTML(i *instance.Instance) (template.HTML, error) {
   635  	buf := new(bytes.Buffer)
   636  	err := clientTemplate.Execute(buf, echo.Map{
   637  		"Domain":      i.ContextualDomain(),
   638  		"ContextName": i.ContextName,
   639  	})
   640  	if err != nil {
   641  		return "", err
   642  	}
   643  	return template.HTML(buf.String()), nil
   644  }
   645  
   646  func cozybarHTML(i *instance.Instance, loggedIn bool) (template.HTML, error) {
   647  	buf := new(bytes.Buffer)
   648  	err := barTemplate.Execute(buf, echo.Map{
   649  		"Domain":      i.ContextualDomain(),
   650  		"Warnings":    middlewares.ListWarnings(i),
   651  		"ContextName": i.ContextName,
   652  		"LoggedIn":    loggedIn,
   653  	})
   654  	if err != nil {
   655  		return "", err
   656  	}
   657  	return template.HTML(buf.String()), nil
   658  }
   659  
   660  func warningsHTML(i *instance.Instance, loggedIn bool) (template.HTML, error) {
   661  	buf := new(bytes.Buffer)
   662  	err := warningsTemplate.Execute(buf, echo.Map{
   663  		"Warnings": middlewares.ListWarnings(i),
   664  		"LoggedIn": loggedIn,
   665  	})
   666  	if err != nil {
   667  		return "", err
   668  	}
   669  	return template.HTML(buf.String()), nil
   670  }