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 }