Ctrl K
ComunidadGuías…

web/apps/apps.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Package apps is the HTTP frontend of the application package. It
// exposes the HTTP api install, update or uninstall applications.
package apps

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"path"
	"runtime"
	"strconv"
	"strings"
	"time"

	"github.com/cozy/cozy-stack/model/account"
	"github.com/cozy/cozy-stack/model/app"
	"github.com/cozy/cozy-stack/model/instance"
	"github.com/cozy/cozy-stack/model/job"
	"github.com/cozy/cozy-stack/model/oauth"
	"github.com/cozy/cozy-stack/model/permission"
	"github.com/cozy/cozy-stack/model/session"
	"github.com/cozy/cozy-stack/pkg/appfs"
	build "github.com/cozy/cozy-stack/pkg/config"
	"github.com/cozy/cozy-stack/pkg/consts"
	"github.com/cozy/cozy-stack/pkg/couchdb"
	"github.com/cozy/cozy-stack/pkg/jsonapi"
	"github.com/cozy/cozy-stack/pkg/limits"
	"github.com/cozy/cozy-stack/pkg/logger"
	"github.com/cozy/cozy-stack/pkg/registry"
	"github.com/cozy/cozy-stack/web/jobs"
	"github.com/cozy/cozy-stack/web/middlewares"
	"github.com/labstack/echo/v4"
)

// JSMimeType is the content-type for javascript
const JSMimeType = "application/javascript"

const typeTextEventStream = "text/event-stream"

type AppLog struct {
	Time  time.Time `json:"timestamp"`
	Level string    `json:"level"`
	Msg   string    `json:"msg"`
}

type apiApp struct {
	app.Manifest
}

func (man *apiApp) MarshalJSON() ([]byte, error) {
	return json.Marshal(man.Manifest)
}

// Links is part of the Manifest interface
func (man *apiApp) Links() *jsonapi.LinksList {
	var route string
	links := jsonapi.LinksList{}
	switch a := man.Manifest.(type) {
	case (*app.WebappManifest):
		route = "/apps/"
		if a.Icon() != "" {
			links.Icon = "/apps/" + a.Slug() + "/icon/" + a.Version()
		}
		if (a.State() == app.Ready || a.State() == app.Installed) &&
			a.Instance != nil {
			links.Related = a.Instance.SubDomain(a.Slug()).String()
		}
	case (*app.KonnManifest):
		route = "/konnectors/"
		if a.Icon() != "" {
			links.Icon = "/konnectors/" + a.Slug() + "/icon/" + a.Version()
		}
		links.Perms = "/permissions/konnectors/" + a.Slug()
	}
	if route != "" {
		links.Self = route + man.Manifest.Slug()
	}
	return &links
}

// Relationships is part of the Manifest interface
func (man *apiApp) Relationships() jsonapi.RelationshipMap {
	return jsonapi.RelationshipMap{}
}

// Included is part of the Manifest interface
func (man *apiApp) Included() []jsonapi.Object {
	return []jsonapi.Object{}
}

// apiApp is a jsonapi.Object
var _ jsonapi.Object = (*apiApp)(nil)

func getHandler(appType consts.AppType) echo.HandlerFunc {
	return func(c echo.Context) error {
		instance := middlewares.GetInstance(c)
		slug := c.Param("slug")
		man, err := app.GetBySlug(instance, slug, appType)
		if err != nil {
			return wrapAppsError(err)
		}
		if err := middlewares.Allow(c, permission.GET, man); err != nil {
			return err
		}

		registries := instance.Registries()
		copier := app.Copier(man.AppType(), instance)
		man = app.DoLazyUpdate(instance, man, copier, registries)

		if appType == consts.WebappType {
			// TODO: Check is this line is really necessary
			man.(*app.WebappManifest).Instance = instance
		}
		return jsonapi.Data(c, http.StatusOK, &apiApp{man}, nil)
	}
}

func downloadHandler(appType consts.AppType) echo.HandlerFunc {
	return func(c echo.Context) error {
		inst := middlewares.GetInstance(c)
		slug := c.Param("slug")
		man, err := app.GetBySlug(inst, slug, appType)
		if err != nil {
			return wrapAppsError(err)
		}
		if err := middlewares.Allow(c, permission.GET, man); err != nil {
			return err
		}

		version := c.Param("version")
		if version == "" {
			version = man.Version()
		}

		source := man.Source()
		if strings.HasPrefix(source, "registry://") {
			registries := inst.Registries()
			v, err := registry.GetVersion(slug, version, registries)
			if err != nil {
				return wrapAppsError(err)
			}
			return c.Redirect(http.StatusSeeOther, v.URL)
		}

		if version != man.Version() {
			err := errors.New("code for this version is not available")
			return jsonapi.PreconditionFailed("version", err)
		}

		var fs appfs.FileServer
		switch appType {
		case consts.WebappType:
			man := man.(*app.WebappManifest)
			if man.FromAppsDir {
				fs = app.FSForAppDir(slug)
			} else {
				fs = app.AppsFileServer(inst)
			}
		case consts.KonnectorType:
			fs = app.KonnectorsFileServer(inst)
		}

		return fs.ServeCodeTarball(c.Response(), c.Request(), slug, version, man.Checksum())
	}
}

// installHandler handles all POST /:slug request and tries to install
// or update the application with the given Source.
func installHandler(installerType consts.AppType) echo.HandlerFunc {
	return func(c echo.Context) error {
		instance := middlewares.GetInstance(c)
		slug := c.Param("slug")
		source := c.QueryParam("Source")
		if source == "" {
			source = "registry://" + slug + "/stable"
		}
		if err := middlewares.AllowInstallApp(c, installerType, source, permission.POST); err != nil {
			return err
		}

		var overridenParameters map[string]interface{}
		if p := c.QueryParam("Parameters"); p != "" {
			if err := json.Unmarshal([]byte(p), &overridenParameters); err != nil {
				return echo.NewHTTPError(http.StatusBadRequest)
			}
		}

		var w http.ResponseWriter
		isEventStream := c.Request().Header.Get(echo.HeaderAccept) == typeTextEventStream
		if isEventStream {
			w = c.Response().Writer
			w.Header().Set(echo.HeaderContentType, typeTextEventStream)
			w.WriteHeader(200)
		}

		inst, err := app.NewInstaller(instance, app.Copier(installerType, instance),
			&app.InstallerOptions{
				Operation:   app.Install,
				Type:        installerType,
				SourceURL:   source,
				Slug:        slug,
				Deactivated: c.QueryParam("Deactivated") == "true",
				Registries:  instance.Registries(),

				OverridenParameters: overridenParameters,
			},
		)
		if err != nil {
			if isEventStream {
				var b []byte
				if b, err = json.Marshal(err.Error()); err == nil {
					writeStream(w, "error", string(b))
				}
			}
			return wrapAppsError(err)
		}

		go inst.Run()
		return pollInstaller(c, instance, isEventStream, w, slug, inst)
	}
}

// logsHandler handles all POST /:slug/logs requests and forwards the log lines
// sent as a JSON array to the server logger.
func logsHandler(appType consts.AppType) echo.HandlerFunc {
	return func(c echo.Context) error {
		inst := middlewares.GetInstance(c)

		slug := c.Param("slug")
		if err := middlewares.AllowMaximal(c); err != nil {
			// If logs are not sent by the flagship app, check that it's sent by
			// a konnector or an app with the logs permission and get its slug
			// from the permission.
			pdoc, err := middlewares.GetPermission(c)
			if err != nil {
				return err
			}

			if err := middlewares.AllowWholeType(c, permission.POST, consts.AppLogs); err != nil {
				return err
			}

			if appType == consts.KonnectorType && pdoc.Type == permission.TypeKonnector {
				slug = strings.TrimPrefix(pdoc.SourceID, consts.Konnectors+"/")
			} else if appType == consts.WebappType && pdoc.Type == permission.TypeWebapp {
				slug = strings.TrimPrefix(pdoc.SourceID, consts.Apps+"/")
			} else {
				return middlewares.ErrForbidden
			}
		}

		clientSide := false
		if appType == consts.KonnectorType {
			man, err := app.GetKonnectorBySlug(inst, slug)
			if err != nil {
				return wrapAppsError(err)
			}
			clientSide = man.ClientSide()
		}

		var logs []AppLog
		if err := json.NewDecoder(c.Request().Body).Decode(&logs); err != nil {
			return jsonapi.BadJSON()
		}

		l := logger.WithDomain(inst.Domain).WithNamespace("jobs").
			WithField("slug", slug).
			WithField("job_id", c.QueryParam("job_id"))

		if clientSide {
			l = l.WithField("worker_id", "client")
		}
		for _, log := range logs {
			level, err := logger.ParseLevel(log.Level)
			if err != nil {
				return jsonapi.InvalidAttribute("level", err)
			}

			l := l.WithTime(log.Time)

			if v := c.QueryParam("version"); v != "" {
				l = l.WithField("version", v)
			}

			l.Log(level, log.Msg)
		}

		return c.NoContent(http.StatusNoContent)
	}
}

// updateHandler handles all POST /:slug request and tries to install
// or update the application with the given Source.
func updateHandler(installerType consts.AppType) echo.HandlerFunc {
	return func(c echo.Context) error {
		instance := middlewares.GetInstance(c)
		slug := c.Param("slug")
		source := c.QueryParam("Source")
		if err := middlewares.AllowInstallApp(c, installerType, source, permission.POST); err != nil {
			return err
		}

		var overridenParameters map[string]interface{}
		if p := c.QueryParam("Parameters"); p != "" {
			if err := json.Unmarshal([]byte(p), &overridenParameters); err != nil {
				return echo.NewHTTPError(http.StatusBadRequest)
			}
		}

		var w http.ResponseWriter
		isEventStream := c.Request().Header.Get(echo.HeaderAccept) == typeTextEventStream
		if isEventStream {
			w = c.Response().Writer
			w.Header().Set(echo.HeaderContentType, typeTextEventStream)
			w.WriteHeader(200)
		}

		permissionsAcked, _ := strconv.ParseBool(c.QueryParam("PermissionsAcked"))
		inst, err := app.NewInstaller(instance, app.Copier(installerType, instance),
			&app.InstallerOptions{
				Operation:  app.Update,
				Type:       installerType,
				SourceURL:  source,
				Slug:       slug,
				Registries: instance.Registries(),

				PermissionsAcked:    permissionsAcked,
				OverridenParameters: overridenParameters,
			},
		)
		if err != nil {
			if isEventStream {
				var b []byte
				if b, err = json.Marshal(err.Error()); err == nil {
					writeStream(w, "error", string(b))
				}
				return nil
			}
			return wrapAppsError(err)
		}

		go inst.Run()
		return pollInstaller(c, instance, isEventStream, w, slug, inst)
	}
}

// deleteHandler handles all DELETE /:slug used to delete an application with
// the specified slug.
func deleteHandler(installerType consts.AppType) echo.HandlerFunc {
	return func(c echo.Context) error {
		instance := middlewares.GetInstance(c)
		slug := c.Param("slug")
		source := "registry://" + slug
		if err := middlewares.AllowInstallApp(c, installerType, source, permission.DELETE); err != nil {
			return err
		}

		// Check if there is a mobile client attached to this app
		if installerType == consts.WebappType {
			oauthClient, err := oauth.FindClientBySoftwareID(instance, "registry://"+slug)
			if err == nil && oauthClient != nil {
				return wrapAppsError(app.ErrLinkedAppExists)
			}
		}

		// Delete accounts locally and remotely for banking konnectors
		if installerType == consts.KonnectorType {
			toDelete, err := findAccountsToDelete(instance, slug)
			if err != nil {
				return wrapAppsError(err)
			}
			if len(toDelete) > 0 {
				man, err := app.GetKonnectorBySlug(instance, slug)
				if err != nil {
					return wrapAppsError(err)
				}
				deleteKonnectorWithAccounts(instance, man, toDelete)
				return jsonapi.Data(c, http.StatusAccepted, &apiApp{man}, nil)
			}
		}

		inst, err := app.NewInstaller(instance, app.Copier(installerType, instance),
			&app.InstallerOptions{
				Operation:  app.Delete,
				Type:       installerType,
				Slug:       slug,
				Registries: instance.Registries(),
			},
		)
		if err != nil {
			return wrapAppsError(err)
		}
		man, err := inst.RunSync()
		if err != nil {
			return wrapAppsError(err)
		}
		return jsonapi.Data(c, http.StatusOK, &apiApp{man}, nil)
	}
}

func findAccountsToDelete(instance *instance.Instance, slug string) ([]account.CleanEntry, error) {
	jobsSystem := job.System()
	triggers, err := jobsSystem.GetAllTriggers(instance)
	if err != nil {
		return nil, err
	}

	var toDelete []account.CleanEntry
	for _, t := range triggers {
		if !t.Infos().IsKonnectorTrigger() {
			continue
		}

		var msg struct {
			Account string `json:"account"`
			Slug    string `json:"konnector"`
		}
		err := t.Infos().Message.Unmarshal(&msg)
		if err == nil && msg.Slug == slug && msg.Account != "" {
			// XXX we can have several triggers for the same account (e.g. cron + webhook)
			hasEntry := false
			for i, entry := range toDelete {
				if entry.Account.ID() == msg.Account {
					toDelete[i].Triggers = append(entry.Triggers, t)
					hasEntry = true
					break
				}
			}
			if !hasEntry {
				acc := &account.Account{}
				if err := couchdb.GetDoc(instance, consts.Accounts, msg.Account, acc); err == nil {
					entry := account.CleanEntry{
						Account:  acc,
						Triggers: []job.Trigger{t},
					}
					toDelete = append(toDelete, entry)
				}
			}
		}
	}
	return toDelete, nil
}

func deleteKonnectorWithAccounts(instance *instance.Instance, man *app.KonnManifest, toDelete []account.CleanEntry) {
	go func() {
		defer func() {
			if r := recover(); r != nil {
				var err error
				switch r := r.(type) {
				case error:
					err = r
				default:
					err = fmt.Errorf("%v", r)
				}
				stack := make([]byte, 4<<10) // 4 KB
				length := runtime.Stack(stack, false)
				log := instance.Logger().WithNamespace("konnectors").WithField("panic", true)
				log.Errorf("PANIC RECOVER %s: %s", err.Error(), stack[:length])
			}
		}()

		slug := man.Slug()
		for i := range toDelete {
			toDelete[i].ManifestOnDelete = man.OnDeleteAccount() != ""
			toDelete[i].Slug = slug
		}

		log := instance.Logger().WithNamespace("konnectors")
		if err := account.CleanAndWait(instance, toDelete); err != nil {
			log.Errorf("Cannot clean accounts: %v", err)
			return
		}
		inst, err := app.NewInstaller(instance, app.Copier(consts.KonnectorType, instance),
			&app.InstallerOptions{
				Operation:  app.Delete,
				Type:       consts.KonnectorType,
				Slug:       slug,
				Registries: instance.Registries(),
			},
		)
		if err != nil {
			log.Errorf("Cannot uninstall the konnector: %v", err)
			return
		}
		_, err = inst.RunSync()
		if err != nil {
			log.Errorf("Cannot uninstall the konnector: %v", err)
		}
	}()
}

func pollInstaller(c echo.Context, instance *instance.Instance, isEventStream bool, w http.ResponseWriter, slug string, inst *app.Installer) error {
	if !isEventStream {
		man, _, err := inst.Poll()
		if err != nil {
			return wrapAppsError(err)
		}
		go func() {
			for {
				_, done, err := inst.Poll()
				if done || err != nil {
					break
				}
			}
		}()
		return jsonapi.Data(c, http.StatusAccepted, &apiApp{man}, nil)
	}

	manc := inst.ManifestChannel()
	ticker := time.NewTicker(10 * time.Second)
	defer ticker.Stop()
	for {
		select {
		case man := <-manc:
			if err := man.Error(); err != nil {
				var b []byte
				if b, err = json.Marshal(err.Error()); err == nil {
					writeStream(w, "error", string(b))
				}
				return nil
			}
			buf := new(bytes.Buffer)
			if err := jsonapi.WriteData(buf, &apiApp{man}, nil); err == nil {
				writeStream(w, "state", strings.TrimSuffix(buf.String(), "\n"))
			}
			if s := man.State(); s == app.Ready || s == app.Installed || s == app.Errored {
				return nil
			}

		case <-ticker.C:
			_, _ = w.Write([]byte(": still working\r\n"))
		}
		if f, ok := w.(http.Flusher); ok {
			f.Flush()
		}
	}
}

func writeStream(w http.ResponseWriter, event string, b string) {
	s := fmt.Sprintf("event: %s\r\ndata: %s\r\n\r\n", event, b)
	_, err := w.Write([]byte(s))
	if err != nil {
		return
	}
	if f, ok := w.(http.Flusher); ok {
		f.Flush()
	}
}

// listWebappsHandler handles all GET / requests which can be used to list
// installed applications.
func listWebappsHandler(c echo.Context) error {
	instance := middlewares.GetInstance(c)
	if err := middlewares.AllowWholeType(c, permission.GET, consts.Apps); err != nil {
		return err
	}

	// Adding the startKey if it is given in the request
	startKey := c.QueryParam("start_key")

	// Same for the limit
	var limit int
	if l := c.QueryParam("limit"); l != "" {
		if converted, err := strconv.Atoi(l); err == nil {
			limit = converted
		}
	}

	docs, next, err := app.ListWebappsWithPagination(instance, limit, startKey)
	if err != nil {
		return wrapAppsError(err)
	}
	objs := make([]jsonapi.Object, len(docs))
	for i, d := range docs {
		d.Instance = instance
		objs[i] = &apiApp{d}
	}

	// Generating links list for the next apps
	links := generateLinksList(c, next, limit, next)

	return jsonapi.DataList(c, http.StatusOK, objs, links)
}

// listKonnectorsHandler handles all GET / requests which can be used to list
// installed applications.
func listKonnectorsHandler(c echo.Context) error {
	instance := middlewares.GetInstance(c)
	if err := middlewares.AllowWholeType(c, permission.GET, consts.Konnectors); err != nil {
		return err
	}

	// Adding the startKey if it is given in the request
	var startKey string
	if sk := c.QueryParam("start_key"); sk != "" {
		startKey = sk
	}

	// Same for the limit
	var limit int
	if l := c.QueryParam("limit"); l != "" {
		if converted, err := strconv.Atoi(l); err == nil {
			limit = converted
		}
	}
	docs, next, err := app.ListKonnectorsWithPagination(instance, limit, startKey)
	if err != nil {
		return wrapAppsError(err)
	}
	objs := make([]jsonapi.Object, len(docs))
	for i, d := range docs {
		objs[i] = &apiApp{d}
	}

	// Generating links list for the next konnectors
	links := generateLinksList(c, next, limit, next)

	return jsonapi.DataList(c, http.StatusOK, objs, links)
}

func generateLinksList(c echo.Context, next string, limit int, nextID string) *jsonapi.LinksList {
	links := &jsonapi.LinksList{}
	if next != "" { // Do not generate the next URL if there are no next konnectors
		nextURL := &url.URL{
			Scheme: c.Scheme(),
			Host:   c.Request().Host,
			Path:   c.Path(),
		}
		values := nextURL.Query()
		values.Set("start_key", nextID)
		values.Set("limit", strconv.Itoa(limit))
		nextURL.RawQuery = values.Encode()

		links.Next = nextURL.String()
	}
	return links
}

// iconHandler gives the icon of an application
func iconHandler(appType consts.AppType) echo.HandlerFunc {
	return func(c echo.Context) error {
		instance := middlewares.GetInstance(c)
		slug := c.Param("slug")
		version := c.Param("version")
		a, err := app.GetBySlug(instance, slug, appType)
		if err != nil {
			if errors.Is(err, app.ErrNotFound) {
				return jsonapi.NotFound(err)
			}
			return err
		}

		if !middlewares.IsLoggedIn(c) {
			if err := middlewares.Allow(c, permission.GET, a); err != nil {
				return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
			}
		}

		if version != "" {
			// The maximum cache-control recommanded is one year :
			// https://www.ietf.org/rfc/rfc2616.txt
			c.Response().Header().Set("Cache-Control", "max-age=31536000, immutable")
		}

		var fs appfs.FileServer
		var filepath string
		switch appType {
		case consts.WebappType:
			a := a.(*app.WebappManifest)
			filepath = path.Join("/", a.Icon())
			if a.FromAppsDir {
				fs = app.FSForAppDir(slug)
			} else {
				fs = app.AppsFileServer(instance)
			}
		case consts.KonnectorType:
			filepath = path.Join("/", a.Icon())
			fs = app.KonnectorsFileServer(instance)
		}

		err = fs.ServeFileContent(c.Response(), c.Request(),
			a.Slug(), a.Version(), a.Checksum(), filepath)
		if os.IsNotExist(err) {
			return echo.NewHTTPError(http.StatusNotFound, err)
		}
		return err
	}
}

func createTrigger(c echo.Context) error {
	inst := middlewares.GetInstance(c)
	slug := c.Param("slug")
	man, err := app.GetBySlug(inst, slug, consts.KonnectorType)
	if err != nil {
		return wrapAppsError(err)
	}
	var createdByApp string
	if claims := c.Get("claims"); claims != nil {
		cl := claims.(permission.Claims)
		if cl.Subject != "" {
			createdByApp = cl.Subject
		}
	}
	t, err := man.(*app.KonnManifest).BuildTrigger(inst, c.QueryParam("AccountID"), createdByApp)
	if err != nil {
		return wrapAppsError(err)
	}
	if err = middlewares.Allow(c, permission.POST, t); err != nil {
		return err
	}
	sched := job.System()
	if err = sched.AddTrigger(t); err != nil {
		return wrapAppsError(err)
	}

	if c.QueryParam("ExecNow") == "true" {
		req := t.Infos().JobRequest()
		req.Manual = true
		_, _ = sched.PushJob(inst, req)
	}

	return jsonapi.Data(c, http.StatusCreated, jobs.NewAPITrigger(t.Infos(), inst), nil)
}

type apiOpenParams struct {
	slug   string
	cookie string
	params serveParams
}

func (o *apiOpenParams) ID() string         { return o.slug }
func (o *apiOpenParams) Rev() string        { return "" }
func (o *apiOpenParams) DocType() string    { return consts.AppsOpenParameters }
func (o *apiOpenParams) SetID(id string)    {}
func (o *apiOpenParams) SetRev(rev string)  {}
func (o *apiOpenParams) Clone() couchdb.Doc { return o }
func (o *apiOpenParams) MarshalJSON() ([]byte, error) {
	data := map[string]interface{}{}
	data["Cookie"] = o.cookie
	data["Token"] = o.params.Token
	data["Domain"] = o.params.Domain()
	data["SubDomain"] = o.params.SubDomain
	data["Tracking"] = strconv.FormatBool(o.params.Tracking)
	data["Locale"] = o.params.Locale()
	data["AppEditor"] = o.params.AppEditor()
	data["AppName"] = o.params.AppName()
	data["AppNamePrefix"] = o.params.AppNamePrefix()
	data["AppSlug"] = o.params.AppSlug()
	data["IconPath"] = o.params.IconPath()
	data["Flags"], _ = o.params.Flags()
	data["Capabilities"], _ = o.params.Capabilities()
	data["CozyBar"], _ = o.params.CozyBar()
	data["CozyFonts"] = o.params.CozyFonts()
	data["CozyClientJS"], _ = o.params.CozyClientJS()
	data["ThemeCSS"] = o.params.ThemeCSS()
	data["Favicon"] = o.params.Favicon()
	data["DefaultWallpaper"] = o.params.DefaultWallpaper()
	data["Warnings"], _ = o.params.Warnings()
	return json.Marshal(data)
}

func (o *apiOpenParams) Relationships() jsonapi.RelationshipMap { return nil }
func (o *apiOpenParams) Included() []jsonapi.Object             { return nil }
func (o *apiOpenParams) Links() *jsonapi.LinksList {
	return &jsonapi.LinksList{Self: "/apps/" + o.slug + "/open"}
}

// openWebapp handles GET /apps/:slug/open requests and returns the data useful
// for the flagship app to open the given the webapp in a webview.
func openWebapp(c echo.Context) error {
	if err := middlewares.AllowMaximal(c); err != nil {
		return wrapAppsError(err)
	}

	inst := middlewares.GetInstance(c)
	slug := c.Param("slug")
	webapp, err := app.GetWebappBySlug(inst, slug)
	if err != nil {
		return wrapAppsError(err)
	}

	var cookie *http.Cookie
	sess, err := session.FromCookie(c, inst)
	if err == nil {
		cookie, err = c.Cookie(session.CookieName(inst))
		if err != nil {
			return wrapAppsError(err)
		}
		cookie.MaxAge = 0
		cookie.Path = "/"
		cookie.Domain = session.CookieDomain(inst)
		cookie.Secure = !build.IsDevRelease()
		cookie.HttpOnly = true
		cookie.SameSite = http.SameSiteLaxMode
	} else {
		sess, err = session.New(inst, session.NormalRun, "")
		if err != nil {
			return wrapAppsError(err)
		}
		cookie, err = sess.ToCookie()
		if err != nil {
			return wrapAppsError(err)
		}
	}

	isLoggedIn := true
	params := buildServeParams(c, inst, webapp, nil, isLoggedIn, sess.ID())
	obj := &apiOpenParams{
		slug:   slug,
		cookie: cookie.String(),
		params: params,
	}
	return jsonapi.Data(c, http.StatusOK, obj, nil)
}

// WebappsRoutes sets the routing for the web apps service
func WebappsRoutes(router *echo.Group) {
	router.GET("/", listWebappsHandler)
	router.GET("/:slug", getHandler(consts.WebappType))
	router.POST("/:slug", installHandler(consts.WebappType))
	router.PUT("/:slug", updateHandler(consts.WebappType))
	router.DELETE("/:slug", deleteHandler(consts.WebappType))
	router.GET("/:slug/icon", iconHandler(consts.WebappType))
	router.GET("/:slug/icon/:version", iconHandler(consts.WebappType))
	router.GET("/:slug/open", openWebapp)
	router.GET("/:slug/download", downloadHandler(consts.WebappType))
	router.GET("/:slug/download/:version", downloadHandler(consts.WebappType))
	router.POST("/:slug/logs", logsHandler(consts.WebappType))
}

// KonnectorRoutes sets the routing for the konnectors service
func KonnectorRoutes(router *echo.Group) {
	router.GET("/", listKonnectorsHandler)
	router.GET("/:slug", getHandler(consts.KonnectorType))
	router.POST("/:slug", installHandler(consts.KonnectorType))
	router.PUT("/:slug", updateHandler(consts.KonnectorType))
	router.DELETE("/:slug", deleteHandler(consts.KonnectorType))
	router.GET("/:slug/icon", iconHandler(consts.KonnectorType))
	router.GET("/:slug/icon/:version", iconHandler(consts.KonnectorType))
	router.POST("/:slug/trigger", createTrigger)
	router.GET("/:slug/download", downloadHandler(consts.KonnectorType))
	router.GET("/:slug/download/:version", downloadHandler(consts.KonnectorType))
	router.POST("/:slug/logs", logsHandler(consts.KonnectorType))
}

func wrapAppsError(err error) error {
	switch err {
	case app.ErrInvalidSlugName:
		return jsonapi.InvalidParameter("slug", err)
	case app.ErrAlreadyExists:
		return jsonapi.Conflict(err)
	case app.ErrNotFound:
		return jsonapi.NotFound(err)
	case app.ErrNotSupportedSource:
		return jsonapi.InvalidParameter("Source", err)
	case app.ErrManifestNotReachable:
		return jsonapi.NotFound(err)
	case app.ErrSourceNotReachable:
		return jsonapi.BadRequest(err)
	case app.ErrBadManifest:
		return jsonapi.BadRequest(err)
	case app.ErrMissingSource:
		return jsonapi.BadRequest(err)
	case app.ErrLinkedAppExists:
		return jsonapi.BadRequest(err)
	case limits.ErrRateLimitReached,
		limits.ErrRateLimitExceeded:
		return jsonapi.BadRequest(err)
	}
	if _, ok := err.(*url.Error); ok {
		return jsonapi.InvalidParameter("Source", err)
	}
	return err
}