package app import ( "bytes" "encoding/json" "maxwarden/entries" "maxwarden/middleware" "maxwarden/query" "maxwarden/security" . "maxwarden/ui" "maxwarden/users" "net/http" "time" . "maragu.dev/gomponents" . "maragu.dev/gomponents/html" ) func ExportBitwardenHandler(w http.ResponseWriter, r *http.Request) { identity := middleware.GetIdentity(r) session := middleware.GetSession(r) renderView := func(errorMessage string) { AppLayout("Export Vault", *identity, session, Card( Heading("Bitwarden export"), P(Text("Download your MaxWarden vault in one of Bitwarden's native import formats.")), P( InlineStyle("$me { margin-top: $3; }"), Text("In Bitwarden, choose the File format that exactly matches the download: Bitwarden (csv) or Bitwarden (json). CSV is recommended if Bitwarden reports a generic JSON import error."), ), P( InlineStyle("$me { margin-top: $3; color: $color(red-700); font-weight: var(--font-weight-semibold); }"), Text("Warning: the downloaded file contains all usernames and passwords in plaintext. Store it securely and delete it when you are finished."), ), If(errorMessage != "", P( InlineStyle("$me { margin-top: $3; color: $color(red-700); }"), Text(errorMessage), ), ), Form( InlineStyle("$me { margin-top: $5; }"), Action("/app/export/bitwarden"), Method(http.MethodPost), FormLabel(Text("Confirm your master key")), FormInput( Type("password"), Name("password"), AutoComplete("current-password"), Required(), ), Br(), Div( InlineStyle("$me { display: flex; flex-wrap: wrap; gap: $3; margin-top: $3; }"), ButtonUIDanger(Type("submit"), Name("format"), Value("csv"), Text("Download Bitwarden CSV (recommended)")), ButtonUIOutline(Type("submit"), Name("format"), Value("json"), Text("Download Bitwarden JSON")), ), ), ), ).Render(w) } switch r.Method { case http.MethodGet: renderView("") return case http.MethodPost: // Continue below. default: w.Header().Set("Allow", http.MethodGet+", "+http.MethodPost) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } r.Body = http.MaxBytesReader(w, r.Body, 1<<20) if err := r.ParseForm(); err != nil { http.Error(w, "Invalid export request", http.StatusBadRequest) return } user, err := users.FetchById(identity.UserID) if err != nil { http.Error(w, "Unable to verify account", http.StatusInternalServerError) return } if !security.ComparePasswords(r.Form.Get("password"), user.Password) { w.WriteHeader(http.StatusUnauthorized) renderView("The master key is incorrect.") return } secretList, err := entries.Filter(entries.EntryFilter{ Filter: query.NewFilterFromSearch(map[string]string{}), UserId: identity.UserID, MasterKey: identity.MasterKey, }) if err != nil { http.Error(w, "Unable to decrypt vault", http.StatusInternalServerError) return } format := r.Form.Get("format") var document []byte var extension string var contentType string switch format { case "", "csv": var csvDocument bytes.Buffer if err := entries.WriteBitwardenCSV(&csvDocument, secretList); err != nil { http.Error(w, "Unable to create export", http.StatusInternalServerError) return } document = csvDocument.Bytes() extension = "csv" contentType = "text/csv; charset=utf-8" case "json": document, err = json.MarshalIndent(entries.NewBitwardenExport(secretList), "", " ") if err != nil { http.Error(w, "Unable to create export", http.StatusInternalServerError) return } extension = "json" contentType = "application/json; charset=utf-8" default: http.Error(w, "Unsupported export format", http.StatusBadRequest) return } filename := "maxwarden-bitwarden-export-" + time.Now().UTC().Format("2006-01-02") + "." + extension w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"`) w.Header().Set("Content-Type", contentType) w.Header().Set("Pragma", "no-cache") w.Header().Set("X-Content-Type-Options", "nosniff") w.Write(document) }