github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/bitwarden/folders.go (about) 1 package bitwarden 2 3 import ( 4 "encoding/json" 5 "net/http" 6 "time" 7 8 "github.com/cozy/cozy-stack/model/bitwarden" 9 "github.com/cozy/cozy-stack/model/bitwarden/settings" 10 "github.com/cozy/cozy-stack/model/permission" 11 "github.com/cozy/cozy-stack/pkg/consts" 12 "github.com/cozy/cozy-stack/pkg/couchdb" 13 "github.com/cozy/cozy-stack/pkg/metadata" 14 "github.com/cozy/cozy-stack/web/middlewares" 15 "github.com/labstack/echo/v4" 16 ) 17 18 // https://github.com/bitwarden/jslib/blob/master/common/src/models/request/folderRequest.ts 19 type folderRequest struct { 20 Name string `json:"name"` 21 } 22 23 func (r *folderRequest) toFolder() *bitwarden.Folder { 24 f := bitwarden.Folder{ 25 Name: r.Name, 26 } 27 md := metadata.New() 28 md.DocTypeVersion = bitwarden.DocTypeVersion 29 f.Metadata = md 30 return &f 31 } 32 33 // https://github.com/bitwarden/jslib/blob/master/common/src/models/response/folderResponse.ts 34 type folderResponse struct { 35 ID string `json:"Id"` 36 Name string `json:"Name"` 37 Date time.Time `json:"RevisionDate"` 38 Object string `json:"Object"` 39 } 40 41 func newFolderResponse(f *bitwarden.Folder) *folderResponse { 42 r := folderResponse{ 43 ID: f.CouchID, 44 Name: f.Name, 45 Object: "folder", 46 } 47 if f.Metadata != nil { 48 r.Date = f.Metadata.UpdatedAt.UTC() 49 } 50 return &r 51 } 52 53 type foldersList struct { 54 Data []*folderResponse `json:"Data"` 55 Object string `json:"Object"` 56 } 57 58 // ListFolders is the route for listing the Bitwarden folders. 59 // No pagination yet. 60 func ListFolders(c echo.Context) error { 61 inst := middlewares.GetInstance(c) 62 if err := middlewares.AllowWholeType(c, permission.GET, consts.BitwardenFolders); err != nil { 63 return c.JSON(http.StatusUnauthorized, echo.Map{ 64 "error": "invalid token", 65 }) 66 } 67 68 var folders []*bitwarden.Folder 69 req := &couchdb.AllDocsRequest{} 70 if err := couchdb.GetAllDocs(inst, consts.BitwardenFolders, req, &folders); err != nil { 71 return c.JSON(http.StatusInternalServerError, echo.Map{ 72 "error": err.Error(), 73 }) 74 } 75 76 res := &foldersList{Object: "list"} 77 for _, f := range folders { 78 res.Data = append(res.Data, newFolderResponse(f)) 79 } 80 return c.JSON(http.StatusOK, res) 81 } 82 83 // CreateFolder is the route to add a folder via the Bitwarden API. 84 func CreateFolder(c echo.Context) error { 85 inst := middlewares.GetInstance(c) 86 if err := middlewares.AllowWholeType(c, permission.POST, consts.BitwardenFolders); err != nil { 87 return c.JSON(http.StatusUnauthorized, echo.Map{ 88 "error": "invalid token", 89 }) 90 } 91 92 var req folderRequest 93 if err := json.NewDecoder(c.Request().Body).Decode(&req); err != nil { 94 return c.JSON(http.StatusBadRequest, echo.Map{ 95 "error": "invalid JSON", 96 }) 97 } 98 if req.Name == "" { 99 return c.JSON(http.StatusBadRequest, echo.Map{ 100 "error": "missing name", 101 }) 102 } 103 104 folder := req.toFolder() 105 if err := couchdb.CreateDoc(inst, folder); err != nil { 106 return c.JSON(http.StatusInternalServerError, echo.Map{ 107 "error": err.Error(), 108 }) 109 } 110 111 _ = settings.UpdateRevisionDate(inst, nil) 112 res := newFolderResponse(folder) 113 return c.JSON(http.StatusOK, res) 114 } 115 116 // GetFolder returns information about a single folder. 117 func GetFolder(c echo.Context) error { 118 inst := middlewares.GetInstance(c) 119 if err := middlewares.AllowWholeType(c, permission.GET, consts.BitwardenFolders); err != nil { 120 return c.JSON(http.StatusUnauthorized, echo.Map{ 121 "error": "invalid token", 122 }) 123 } 124 125 id := c.Param("id") 126 if id == "" { 127 return c.JSON(http.StatusNotFound, echo.Map{ 128 "error": "missing id", 129 }) 130 } 131 132 folder := &bitwarden.Folder{} 133 if err := couchdb.GetDoc(inst, consts.BitwardenFolders, id, folder); err != nil { 134 if couchdb.IsNotFoundError(err) { 135 return c.JSON(http.StatusNotFound, echo.Map{ 136 "error": "not found", 137 }) 138 } 139 return c.JSON(http.StatusInternalServerError, echo.Map{ 140 "error": err.Error(), 141 }) 142 } 143 144 res := newFolderResponse(folder) 145 return c.JSON(http.StatusOK, res) 146 } 147 148 // RenameFolder is the route for changing the (encrypted) name of a folder. 149 func RenameFolder(c echo.Context) error { 150 inst := middlewares.GetInstance(c) 151 if err := middlewares.AllowWholeType(c, permission.PUT, consts.BitwardenFolders); err != nil { 152 return c.JSON(http.StatusUnauthorized, echo.Map{ 153 "error": "invalid token", 154 }) 155 } 156 157 id := c.Param("id") 158 if id == "" { 159 return c.JSON(http.StatusNotFound, echo.Map{ 160 "error": "missing id", 161 }) 162 } 163 164 folder := &bitwarden.Folder{} 165 if err := couchdb.GetDoc(inst, consts.BitwardenFolders, id, folder); err != nil { 166 if couchdb.IsNotFoundError(err) { 167 return c.JSON(http.StatusNotFound, echo.Map{ 168 "error": "not found", 169 }) 170 } 171 return c.JSON(http.StatusInternalServerError, echo.Map{ 172 "error": err.Error(), 173 }) 174 } 175 176 var req folderRequest 177 if err := json.NewDecoder(c.Request().Body).Decode(&req); err != nil { 178 return c.JSON(http.StatusBadRequest, echo.Map{ 179 "error": "invalid JSON", 180 }) 181 } 182 if req.Name == "" { 183 return c.JSON(http.StatusBadRequest, echo.Map{ 184 "error": "missing name", 185 }) 186 } 187 188 folder.Name = req.Name 189 if folder.Metadata == nil { 190 md := metadata.New() 191 md.DocTypeVersion = bitwarden.DocTypeVersion 192 folder.Metadata = md 193 } 194 folder.Metadata.ChangeUpdatedAt() 195 if err := couchdb.UpdateDoc(inst, folder); err != nil { 196 return c.JSON(http.StatusInternalServerError, echo.Map{ 197 "error": err.Error(), 198 }) 199 } 200 201 _ = settings.UpdateRevisionDate(inst, nil) 202 res := newFolderResponse(folder) 203 return c.JSON(http.StatusOK, res) 204 } 205 206 // DeleteFolder is the handler for the route to delete a folder. 207 func DeleteFolder(c echo.Context) error { 208 inst := middlewares.GetInstance(c) 209 if err := middlewares.AllowWholeType(c, permission.DELETE, consts.BitwardenFolders); err != nil { 210 return c.JSON(http.StatusUnauthorized, echo.Map{ 211 "error": "invalid token", 212 }) 213 } 214 215 id := c.Param("id") 216 if id == "" { 217 return c.JSON(http.StatusNotFound, echo.Map{ 218 "error": "missing id", 219 }) 220 } 221 222 folder := &bitwarden.Folder{} 223 if err := couchdb.GetDoc(inst, consts.BitwardenFolders, id, folder); err != nil { 224 if couchdb.IsNotFoundError(err) { 225 return c.JSON(http.StatusNotFound, echo.Map{ 226 "error": "not found", 227 }) 228 } 229 return c.JSON(http.StatusInternalServerError, echo.Map{ 230 "error": err.Error(), 231 }) 232 } 233 234 // Move the ciphers that are in this folder to outside of it 235 ciphers, err := bitwarden.FindCiphersInFolder(inst, id) 236 if err != nil { 237 return c.JSON(http.StatusInternalServerError, echo.Map{ 238 "error": err.Error(), 239 }) 240 } 241 docs := make([]interface{}, len(ciphers)) 242 olds := make([]interface{}, len(ciphers)) 243 for i, doc := range ciphers { 244 olds[i] = doc.Clone() 245 doc.FolderID = "" 246 docs[i] = ciphers[i] 247 } 248 if err := couchdb.BulkUpdateDocs(inst, consts.BitwardenCiphers, docs, olds); err != nil { 249 return c.JSON(http.StatusInternalServerError, echo.Map{ 250 "error": err.Error(), 251 }) 252 } 253 254 if folder.Metadata == nil { 255 md := metadata.New() 256 md.DocTypeVersion = bitwarden.DocTypeVersion 257 folder.Metadata = md 258 } 259 folder.Metadata.ChangeUpdatedAt() 260 if err := couchdb.DeleteDoc(inst, folder); err != nil { 261 return c.JSON(http.StatusInternalServerError, echo.Map{ 262 "error": err.Error(), 263 }) 264 } 265 266 _ = settings.UpdateRevisionDate(inst, nil) 267 return c.NoContent(http.StatusOK) 268 }