github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/settings/clients.go (about) 1 package settings 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "net/http" 8 "net/url" 9 "strconv" 10 "time" 11 12 "github.com/cozy/cozy-stack/model/feature" 13 "github.com/cozy/cozy-stack/model/instance" 14 "github.com/cozy/cozy-stack/model/oauth" 15 "github.com/cozy/cozy-stack/model/permission" 16 "github.com/cozy/cozy-stack/pkg/consts" 17 "github.com/cozy/cozy-stack/pkg/couchdb" 18 "github.com/cozy/cozy-stack/pkg/jsonapi" 19 "github.com/cozy/cozy-stack/web/auth" 20 "github.com/cozy/cozy-stack/web/middlewares" 21 "github.com/labstack/echo/v4" 22 ) 23 24 type apiOauthClient struct{ *oauth.Client } 25 26 func (c *apiOauthClient) MarshalJSON() ([]byte, error) { 27 return json.Marshal(c.Client) 28 } 29 30 // Links is used to generate a JSON-API link for the client - see 31 // jsonapi.Object interface 32 func (c *apiOauthClient) Links() *jsonapi.LinksList { 33 return &jsonapi.LinksList{Self: "/settings/clients/" + c.ID()} 34 } 35 36 // Relationships is used to generate the parent relationship in JSON-API format 37 // - see jsonapi.Object interface 38 func (c *apiOauthClient) Relationships() jsonapi.RelationshipMap { 39 return jsonapi.RelationshipMap{} 40 } 41 42 // Included is part of the jsonapi.Object interface 43 func (c *apiOauthClient) Included() []jsonapi.Object { 44 return []jsonapi.Object{} 45 } 46 47 func (h *HTTPHandler) listClients(c echo.Context) error { 48 instance := middlewares.GetInstance(c) 49 50 if err := middlewares.AllowWholeType(c, permission.GET, consts.OAuthClients); err != nil { 51 return err 52 } 53 54 bookmark := c.QueryParam("page[cursor]") 55 limit, err := strconv.ParseInt(c.QueryParam("page[limit]"), 10, 64) 56 if err != nil || limit < 0 || limit > consts.MaxItemsPerPageForMango { 57 limit = 100 58 } 59 clients, bookmark, err := oauth.GetAll(instance, int(limit), bookmark) 60 if err != nil { 61 return err 62 } 63 64 objs := make([]jsonapi.Object, len(clients)) 65 for i, d := range clients { 66 objs[i] = jsonapi.Object(&apiOauthClient{d}) 67 } 68 69 links := &jsonapi.LinksList{} 70 if bookmark != "" && len(objs) == int(limit) { 71 v := url.Values{} 72 v.Set("page[cursor]", bookmark) 73 if limit != 100 { 74 v.Set("page[limit]", fmt.Sprintf("%d", limit)) 75 } 76 links.Next = "/settings/clients?" + v.Encode() 77 } 78 return jsonapi.DataList(c, http.StatusOK, objs, links) 79 } 80 81 func (h *HTTPHandler) revokeClient(c echo.Context) error { 82 instance := middlewares.GetInstance(c) 83 84 if err := middlewares.AllowWholeType(c, permission.DELETE, consts.OAuthClients); err != nil { 85 return err 86 } 87 88 clientID := c.Param("id") 89 defer auth.LockOAuthClient(instance, clientID)() 90 91 client, err := oauth.FindClient(instance, clientID) 92 if err != nil { 93 return err 94 } 95 96 if err := client.Delete(instance); err != nil { 97 return errors.New(err.Error) 98 } 99 return c.NoContent(http.StatusNoContent) 100 } 101 102 func (h *HTTPHandler) synchronized(c echo.Context) error { 103 instance := middlewares.GetInstance(c) 104 105 tok := middlewares.GetRequestToken(c) 106 if tok == "" { 107 return permission.ErrInvalidToken 108 } 109 110 claims, err := middlewares.ExtractClaims(c, instance, tok) 111 if err != nil { 112 return err 113 } 114 115 defer auth.LockOAuthClient(instance, claims.Subject)() 116 117 client, err := oauth.FindClient(instance, claims.Subject) 118 if err != nil { 119 return permission.ErrInvalidToken 120 } 121 122 client.SynchronizedAt = time.Now() 123 if err := couchdb.UpdateDoc(instance, client); err != nil { 124 return err 125 } 126 return c.NoContent(http.StatusNoContent) 127 } 128 129 func (h *HTTPHandler) limitExceeded(c echo.Context) error { 130 inst := middlewares.GetInstance(c) 131 132 if !middlewares.IsLoggedIn(c) { 133 return echo.NewHTTPError(http.StatusUnauthorized, "Error Must be authenticated") 134 } 135 136 redirect := c.QueryParam("redirect") 137 if redirect == "" { 138 redirect = inst.DefaultRedirection().String() 139 } 140 141 flags, err := feature.GetFlags(inst) 142 if err != nil { 143 return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("Could not get flags: %w", err)) 144 } 145 146 if clientsLimit, ok := flags.M["cozy.oauthclients.max"].(float64); ok && clientsLimit >= 0 { 147 limit := int(clientsLimit) 148 149 clients, _, err := oauth.GetConnectedUserClients(inst, 100, "") 150 if err != nil { 151 return fmt.Errorf("Could not fetch connected OAuth clients: %s", err) 152 } 153 count := len(clients) 154 155 if count > limit { 156 isFlagship, _ := strconv.ParseBool(c.QueryParam("isFlagship")) 157 158 connectedDevicesURL := inst.SubDomain(consts.SettingsSlug) 159 connectedDevicesURL.Fragment = "/connectedDevices" 160 161 var premiumURL string 162 if inst.HasPremiumLinksEnabled() { 163 iapEnabled, _ := flags.M["flagship.iap.enabled"].(bool) 164 isIapAvailable, _ := strconv.ParseBool(c.QueryParam("isIapAvailable")) 165 166 if !isFlagship || (iapEnabled && isIapAvailable) { 167 var err error 168 if premiumURL, err = inst.ManagerURL(instance.ManagerPremiumURL); err != nil { 169 inst.Logger().Errorf("Could not get instance Premium Manager URL: %s", err.Error()) 170 } 171 } 172 } 173 174 sess, _ := middlewares.GetSession(c) 175 settingsToken := inst.BuildAppToken(consts.SettingsSlug, sess.ID()) 176 return c.Render(http.StatusOK, "oauth_clients_limit_exceeded.html", echo.Map{ 177 "Domain": inst.ContextualDomain(), 178 "ContextName": inst.ContextName, 179 "Locale": inst.Locale, 180 "Title": inst.TemplateTitle(), 181 "Favicon": middlewares.Favicon(inst), 182 "ClientsCount": strconv.Itoa(count), 183 "ClientsLimit": strconv.Itoa(limit), 184 "OpenLinksInNewTab": isFlagship, 185 "ManageDevicesURL": connectedDevicesURL.String(), 186 "PremiumURL": premiumURL, 187 "SettingsToken": settingsToken, 188 }) 189 } 190 } 191 192 return c.Redirect(http.StatusFound, redirect) 193 }