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

     1  // Package settings regroups some API methods to facilitate the usage of the
     2  // io.cozy settings documents.
     3  package settings
     4  
     5  import (
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"net/http"
    10  	"net/url"
    11  	"strings"
    12  
    13  	"github.com/cozy/cozy-stack/model/feature"
    14  	"github.com/cozy/cozy-stack/model/instance"
    15  	"github.com/cozy/cozy-stack/model/permission"
    16  	"github.com/cozy/cozy-stack/model/session"
    17  	csettings "github.com/cozy/cozy-stack/model/settings"
    18  	"github.com/cozy/cozy-stack/model/token"
    19  	"github.com/cozy/cozy-stack/pkg/consts"
    20  	"github.com/cozy/cozy-stack/pkg/couchdb"
    21  	"github.com/cozy/cozy-stack/pkg/jsonapi"
    22  	"github.com/cozy/cozy-stack/web/middlewares"
    23  	"github.com/labstack/echo/v4"
    24  	"github.com/mssola/user_agent"
    25  )
    26  
    27  type apiSession struct {
    28  	s *session.Session
    29  }
    30  
    31  func (s *apiSession) ID() string                             { return s.s.ID() }
    32  func (s *apiSession) Rev() string                            { return s.s.Rev() }
    33  func (s *apiSession) DocType() string                        { return consts.Sessions }
    34  func (s *apiSession) Clone() couchdb.Doc                     { return s }
    35  func (s *apiSession) SetID(_ string)                         {}
    36  func (s *apiSession) SetRev(_ string)                        {}
    37  func (s *apiSession) Relationships() jsonapi.RelationshipMap { return nil }
    38  func (s *apiSession) Included() []jsonapi.Object             { return nil }
    39  func (s *apiSession) Links() *jsonapi.LinksList              { return nil }
    40  func (s *apiSession) MarshalJSON() ([]byte, error)           { return json.Marshal(s.s) }
    41  
    42  // HTTPHandler handle all the `/settings` routes.
    43  type HTTPHandler struct {
    44  	svc csettings.Service
    45  }
    46  
    47  // NewHTTPHandler instantiates a new [HTTPHandler].
    48  func NewHTTPHandler(svc csettings.Service) *HTTPHandler {
    49  	return &HTTPHandler{svc}
    50  }
    51  
    52  func (h *HTTPHandler) getSessions(c echo.Context) error {
    53  	inst := middlewares.GetInstance(c)
    54  
    55  	if err := middlewares.AllowWholeType(c, permission.GET, consts.Sessions); err != nil {
    56  		return err
    57  	}
    58  
    59  	sessions, err := session.GetAll(inst)
    60  	if err != nil {
    61  		return err
    62  	}
    63  
    64  	objs := make([]jsonapi.Object, len(sessions))
    65  	for i, s := range sessions {
    66  		objs[i] = &apiSession{s}
    67  	}
    68  
    69  	return jsonapi.DataList(c, http.StatusOK, objs, nil)
    70  }
    71  
    72  func (h *HTTPHandler) listWarnings(c echo.Context) error {
    73  	inst := middlewares.GetInstance(c)
    74  
    75  	// Any request with a token can ask for the context (no permissions are required)
    76  	if _, err := middlewares.GetPermission(c); err != nil && !isMovedError(err) {
    77  		return err
    78  	}
    79  
    80  	w := middlewares.ListWarnings(inst)
    81  
    82  	if len(w) == 0 {
    83  		// Sends a 404 when there is no warnings
    84  		resp := c.Response()
    85  		resp.Header().Set(echo.HeaderContentType, jsonapi.ContentType)
    86  		resp.WriteHeader(http.StatusNotFound)
    87  		_, err := resp.Write([]byte("{\"errors\": []}"))
    88  		return err
    89  	}
    90  
    91  	return jsonapi.DataErrorList(c, w...)
    92  }
    93  
    94  // postEmail handle POST /settings/email
    95  func (h *HTTPHandler) postEmail(c echo.Context) error {
    96  	type body struct {
    97  		Passphrase string `json:"passphrase"`
    98  		Email      string `json:"email"`
    99  	}
   100  
   101  	if err := middlewares.AllowWholeType(c, permission.POST, consts.Settings); err != nil {
   102  		return err
   103  	}
   104  
   105  	var args body
   106  	err := c.Bind(&args)
   107  	if err != nil {
   108  		return jsonapi.BadJSON()
   109  	}
   110  
   111  	inst := middlewares.GetInstance(c)
   112  
   113  	err = h.svc.StartEmailUpdate(inst, &csettings.UpdateEmailCmd{
   114  		Passphrase: []byte(args.Passphrase),
   115  		Email:      args.Email,
   116  	})
   117  
   118  	switch {
   119  	case err == nil:
   120  		c.NoContent(http.StatusNoContent)
   121  		return nil
   122  	case errors.Is(err, instance.ErrInvalidPassphrase):
   123  		return jsonapi.BadRequest(instance.ErrInvalidPassphrase)
   124  	default:
   125  		return jsonapi.InternalServerError(err)
   126  	}
   127  }
   128  
   129  // postEmailResend handle POST /settings/email/resend
   130  func (h *HTTPHandler) postEmailResend(c echo.Context) error {
   131  	if err := middlewares.AllowWholeType(c, permission.POST, consts.Settings); err != nil {
   132  		return err
   133  	}
   134  
   135  	inst := middlewares.GetInstance(c)
   136  
   137  	err := h.svc.ResendEmailUpdate(inst)
   138  
   139  	switch {
   140  	case err == nil:
   141  		c.NoContent(http.StatusNoContent)
   142  		return nil
   143  	case errors.Is(err, instance.ErrInvalidPassphrase):
   144  		return jsonapi.BadRequest(instance.ErrInvalidPassphrase)
   145  	default:
   146  		return jsonapi.InternalServerError(err)
   147  	}
   148  }
   149  
   150  // deleteEmail handle DELETE /settings/email
   151  func (h *HTTPHandler) deleteEmail(c echo.Context) error {
   152  	if err := middlewares.AllowWholeType(c, permission.POST, consts.Settings); err != nil {
   153  		return err
   154  	}
   155  
   156  	inst := middlewares.GetInstance(c)
   157  
   158  	err := h.svc.CancelEmailUpdate(inst)
   159  	switch {
   160  	case err == nil:
   161  		c.NoContent(http.StatusNoContent)
   162  		return nil
   163  	case errors.Is(err, csettings.ErrNoPendingEmail):
   164  		return jsonapi.BadRequest(csettings.ErrNoPendingEmail)
   165  	default:
   166  		return jsonapi.InternalServerError(err)
   167  	}
   168  }
   169  
   170  func (h *HTTPHandler) getEmailConfirmation(c echo.Context) error {
   171  	inst := middlewares.GetInstance(c)
   172  	if !middlewares.IsLoggedIn(c) {
   173  		u := inst.PageURL("/auth/login", url.Values{
   174  			"redirect": {inst.FromURL(c.Request().URL)},
   175  		})
   176  		return c.Redirect(http.StatusSeeOther, u)
   177  	}
   178  
   179  	tok := c.QueryParam("token")
   180  	settingsURL := inst.SubDomain("settings").String()
   181  
   182  	err := h.svc.ConfirmEmailUpdate(inst, tok)
   183  	switch {
   184  	case err == nil:
   185  		// Redirect to the setting page
   186  		return c.Redirect(http.StatusTemporaryRedirect, settingsURL)
   187  	case errors.Is(err, csettings.ErrNoPendingEmail), errors.Is(err, token.ErrInvalidToken):
   188  		return c.Render(http.StatusBadRequest, "error.html", echo.Map{
   189  			"Domain":       inst.ContextualDomain(),
   190  			"ContextName":  inst.ContextName,
   191  			"Locale":       inst.Locale,
   192  			"Title":        inst.TemplateTitle(),
   193  			"Favicon":      middlewares.Favicon(inst),
   194  			"Illustration": "/images/generic-error.svg",
   195  			"ErrorTitle":   "Error InvalidToken Title",
   196  			"Error":        "Error InvalidToken Message",
   197  			"Link":         "Error InvalidToken Link",
   198  			"LinkURL":      settingsURL,
   199  			"SupportEmail": inst.SupportEmailAddress(),
   200  		})
   201  	default:
   202  		return echo.NewHTTPError(http.StatusInternalServerError, err)
   203  	}
   204  }
   205  
   206  func (h *HTTPHandler) installFlagshipApp(c echo.Context) error {
   207  	inst := middlewares.GetInstance(c)
   208  	rawUserAgent := c.Request().UserAgent()
   209  	ua := user_agent.New(rawUserAgent)
   210  	platform := strings.ToLower(ua.Platform())
   211  	os := strings.ToLower(ua.OS())
   212  	flags, err := feature.GetFlags(inst)
   213  	if err != nil {
   214  		return err
   215  	}
   216  
   217  	storeLink := "https://cozy.io/" + inst.Locale + "/download"
   218  	if strings.Contains(platform, "iphone") || strings.Contains(platform, "ipad") {
   219  		id, ok := flags.M["flagship.appstore_id"].(string)
   220  		if !ok {
   221  			id = "id1600636174"
   222  		}
   223  		storeLink = fmt.Sprintf("https://apps.apple.com/%s/app/%s", inst.Locale, id)
   224  	} else if strings.Contains(platform, "android") || strings.Contains(os, "android") {
   225  		id, ok := flags.M["flagship.playstore_id"].(string)
   226  		if !ok {
   227  			id = "io.cozy.flagship.mobile"
   228  		}
   229  		storeLink = fmt.Sprintf("https://play.google.com/store/apps/details?id=%s&hl=%s", id, inst.Locale)
   230  	}
   231  
   232  	return c.Render(http.StatusOK, "install_flagship_app.html", echo.Map{
   233  		"Domain":      inst.ContextualDomain(),
   234  		"ContextName": inst.ContextName,
   235  		"Locale":      inst.Locale,
   236  		"Title":       inst.TemplateTitle(),
   237  		"Favicon":     middlewares.Favicon(inst),
   238  		"StoreLink":   storeLink,
   239  		"SkipLink":    inst.OnboardedRedirection().String(),
   240  	})
   241  }
   242  
   243  func isMovedError(err error) bool {
   244  	j, ok := err.(*jsonapi.Error)
   245  	return ok && j.Code == "moved"
   246  }
   247  
   248  // Register all the `/settings` routes to the given router.
   249  func (h *HTTPHandler) Register(router *echo.Group) {
   250  	router.GET("/disk-usage", h.diskUsage)
   251  	router.GET("/clients-usage", h.clientsUsage)
   252  
   253  	router.POST("/email", h.postEmail)
   254  	router.POST("/email/resend", h.postEmailResend)
   255  	router.DELETE("/email", h.deleteEmail)
   256  	router.GET("/email/confirm", h.getEmailConfirmation)
   257  
   258  	router.GET("/passphrase", h.getPassphraseParameters)
   259  	router.POST("/passphrase", h.registerPassphrase)
   260  	router.POST("/passphrase/flagship", h.registerPassphraseFlagship)
   261  	router.PUT("/passphrase", h.updatePassphrase)
   262  	router.POST("/passphrase/check", h.checkPassphrase)
   263  	router.GET("/hint", h.getHint)
   264  	router.PUT("/hint", h.updateHint)
   265  	router.POST("/vault", h.createVault)
   266  
   267  	router.GET("/capabilities", h.getCapabilities)
   268  	router.GET("/external-ties", h.getExternalTies)
   269  	router.GET("/instance", h.getInstance)
   270  	router.PUT("/instance", h.updateInstance)
   271  	router.POST("/instance/deletion", h.askInstanceDeletion)
   272  	router.PUT("/instance/auth_mode", h.updateInstanceAuthMode)
   273  	router.PUT("/instance/sign_tos", h.updateInstanceTOS)
   274  	router.DELETE("/instance/moved_from", h.clearMovedFrom)
   275  
   276  	router.GET("/flags", h.getFlags)
   277  
   278  	router.GET("/sessions", h.getSessions)
   279  
   280  	router.GET("/clients", h.listClients)
   281  	router.DELETE("/clients/:id", h.revokeClient)
   282  	router.GET("/clients/limit-exceeded", h.limitExceeded)
   283  	router.POST("/synchronized", h.synchronized)
   284  
   285  	router.GET("/onboarded", h.onboarded)
   286  	router.GET("/install_flagship_app", h.installFlagshipApp)
   287  	router.GET("/context", h.context)
   288  	router.GET("/warnings", h.listWarnings)
   289  }