github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/settings/instance.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 "net/http" 8 "strings" 9 10 "github.com/cozy/cozy-stack/model/instance" 11 "github.com/cozy/cozy-stack/model/instance/lifecycle" 12 "github.com/cozy/cozy-stack/model/permission" 13 "github.com/cozy/cozy-stack/pkg/consts" 14 "github.com/cozy/cozy-stack/pkg/couchdb" 15 "github.com/cozy/cozy-stack/pkg/jsonapi" 16 "github.com/cozy/cozy-stack/web/middlewares" 17 "github.com/labstack/echo/v4" 18 ) 19 20 type apiInstance struct { 21 doc *couchdb.JSONDoc 22 } 23 24 func (i *apiInstance) ID() string { return i.doc.ID() } 25 func (i *apiInstance) Rev() string { return i.doc.Rev() } 26 func (i *apiInstance) DocType() string { return consts.Settings } 27 func (i *apiInstance) Clone() couchdb.Doc { return i } 28 func (i *apiInstance) SetID(id string) { i.doc.SetID(id) } 29 func (i *apiInstance) SetRev(rev string) { i.doc.SetRev(rev) } 30 func (i *apiInstance) Relationships() jsonapi.RelationshipMap { return nil } 31 func (i *apiInstance) Included() []jsonapi.Object { return nil } 32 func (i *apiInstance) Links() *jsonapi.LinksList { 33 return &jsonapi.LinksList{Self: "/settings/instance"} 34 } 35 36 func (i *apiInstance) MarshalJSON() ([]byte, error) { 37 return json.Marshal(i.doc) 38 } 39 40 func (h *HTTPHandler) getInstance(c echo.Context) error { 41 inst := middlewares.GetInstance(c) 42 43 doc, err := inst.SettingsDocument() 44 if err != nil { 45 return err 46 } 47 48 doc.M["locale"] = inst.Locale 49 doc.M["onboarding_finished"] = inst.OnboardingFinished 50 if inst.PasswordDefined != nil { 51 doc.M["password_defined"] = *inst.PasswordDefined 52 } 53 doc.M["auto_update"] = !inst.NoAutoUpdate 54 doc.M["auth_mode"] = instance.AuthModeToString(inst.AuthMode) 55 doc.M["tos"] = inst.TOSSigned 56 doc.M["tos_latest"] = inst.TOSLatest 57 doc.M["uuid"] = inst.UUID 58 doc.M["oidc_id"] = inst.OIDCID 59 doc.M["context"] = inst.ContextName 60 if len(inst.Sponsorships) > 0 { 61 doc.M["sponsorships"] = inst.Sponsorships 62 } 63 // XXX we had a bug where the default_redirection was filled by a full URL 64 // instead of slug+path, and we fix it when this endpoint is called. 65 if value, ok := doc.M["default_redirection"].(string); !ok || strings.HasPrefix(value, "http") { 66 doc.M["default_redirection"] = inst.DefaultAppAndPath() 67 } 68 69 // Allow any application with a token 70 if _, err = middlewares.GetPermission(c); err != nil { 71 return err 72 } 73 74 return jsonapi.Data(c, http.StatusOK, &apiInstance{doc}, nil) 75 } 76 77 func (h *HTTPHandler) updateInstance(c echo.Context) error { 78 inst := middlewares.GetInstance(c) 79 80 doc := &couchdb.JSONDoc{} 81 obj, err := jsonapi.Bind(c.Request().Body, doc) 82 if err != nil { 83 return err 84 } 85 86 if doc.M == nil { 87 return jsonapi.BadJSON() 88 } 89 90 doc.Type = consts.Settings 91 doc.SetID(consts.InstanceSettingsID) 92 doc.SetRev(obj.Meta.Rev) 93 94 if err = middlewares.Allow(c, permission.PUT, doc); err != nil { 95 return err 96 } 97 98 pdoc, err := middlewares.GetPermission(c) 99 if err != nil || pdoc.Type != permission.TypeCLI { 100 delete(doc.M, "auth_mode") 101 delete(doc.M, "tos") 102 delete(doc.M, "tos_latest") 103 delete(doc.M, "uuid") 104 delete(doc.M, "context") 105 delete(doc.M, "sponsorships") 106 delete(doc.M, "oidc_id") 107 } 108 109 if err := lifecycle.Patch(inst, &lifecycle.Options{SettingsObj: doc}); err != nil { 110 return err 111 } 112 113 doc.M["locale"] = inst.Locale 114 doc.M["onboarding_finished"] = inst.OnboardingFinished 115 doc.M["auto_update"] = !inst.NoAutoUpdate 116 doc.M["auth_mode"] = instance.AuthModeToString(inst.AuthMode) 117 doc.M["tos"] = inst.TOSSigned 118 doc.M["tos_latest"] = inst.TOSLatest 119 doc.M["uuid"] = inst.UUID 120 doc.M["oidc_id"] = inst.OIDCID 121 doc.M["context"] = inst.ContextName 122 123 return jsonapi.Data(c, http.StatusOK, &apiInstance{doc}, nil) 124 } 125 126 func (h *HTTPHandler) updateInstanceTOS(c echo.Context) error { 127 inst := middlewares.GetInstance(c) 128 129 // Allow any request from OAuth tokens to use this route 130 pdoc, err := middlewares.GetPermission(c) 131 if err != nil { 132 return err 133 } 134 if pdoc.Type != permission.TypeOauth && pdoc.Type != permission.TypeCLI { 135 return echo.NewHTTPError(http.StatusForbidden) 136 } 137 138 if err := lifecycle.ManagerSignTOS(inst, c.Request()); err != nil { 139 return err 140 } 141 142 return c.NoContent(http.StatusNoContent) 143 } 144 145 func (h *HTTPHandler) updateInstanceAuthMode(c echo.Context) error { 146 inst := middlewares.GetInstance(c) 147 148 if err := middlewares.AllowWholeType(c, permission.PUT, consts.Settings); err != nil { 149 return err 150 } 151 152 args := struct { 153 AuthMode string `json:"auth_mode"` 154 TwoFactorActivationCode string `json:"two_factor_activation_code"` 155 }{} 156 if err := c.Bind(&args); err != nil { 157 return err 158 } 159 160 authMode, err := instance.StringToAuthMode(args.AuthMode) 161 if err != nil { 162 return jsonapi.BadRequest(err) 163 } 164 if inst.HasAuthMode(authMode) { 165 return c.NoContent(http.StatusNoContent) 166 } 167 168 switch authMode { 169 case instance.Basic: 170 case instance.TwoFactorMail: 171 if args.TwoFactorActivationCode == "" { 172 if err = lifecycle.SendMailConfirmationCode(inst); err != nil { 173 return err 174 } 175 return c.NoContent(http.StatusNoContent) 176 } 177 if ok := inst.ValidateMailConfirmationCode(args.TwoFactorActivationCode); !ok { 178 return c.NoContent(http.StatusUnprocessableEntity) 179 } 180 } 181 182 err = lifecycle.Patch(inst, &lifecycle.Options{AuthMode: args.AuthMode}) 183 if err != nil { 184 return err 185 } 186 187 return c.NoContent(http.StatusNoContent) 188 } 189 190 func (h *HTTPHandler) askInstanceDeletion(c echo.Context) error { 191 if err := middlewares.RequireSettingsApp(c); err != nil { 192 return err 193 } 194 195 inst := middlewares.GetInstance(c) 196 if err := lifecycle.AskDeletion(inst); err != nil { 197 return err 198 } 199 return c.NoContent(http.StatusNoContent) 200 } 201 202 func (h *HTTPHandler) clearMovedFrom(c echo.Context) error { 203 if !middlewares.IsLoggedIn(c) { 204 return echo.NewHTTPError(http.StatusForbidden) 205 } 206 207 inst := middlewares.GetInstance(c) 208 doc, err := inst.SettingsDocument() 209 if err != nil { 210 return err 211 } 212 213 if doc.M["moved_from"] != nil { 214 delete(doc.M, "moved_from") 215 if err := couchdb.UpdateDoc(inst, doc); err != nil { 216 return err 217 } 218 } 219 220 return c.NoContent(http.StatusNoContent) 221 }