github.com/decred/politeia@v1.4.0/politeiawww/cmd/cmswww/editinvoice.go (about) 1 // Copyright (c) 2017-2019 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "bufio" 9 "encoding/base64" 10 "encoding/hex" 11 "encoding/json" 12 "fmt" 13 "os" 14 "path/filepath" 15 "strconv" 16 "strings" 17 18 "github.com/decred/politeia/politeiad/api/v1/mime" 19 v1 "github.com/decred/politeia/politeiawww/api/cms/v1" 20 www "github.com/decred/politeia/politeiawww/api/www/v1" 21 "github.com/decred/politeia/politeiawww/cmd/shared" 22 "github.com/decred/politeia/util" 23 ) 24 25 // EditInvoiceCmd edits an existing invoice. 26 type EditInvoiceCmd struct { 27 Args struct { 28 Month uint `positional-arg-name:"month" required:"true"` 29 Year uint `positional-arg-name:"year"` 30 Token string `positional-arg-name:"token"` // Censorship token 31 CSV string `positional-arg-name:"csvfile"` // Invoice CSV file 32 Attachments []string `positional-arg-name:"attachmentfiles"` // Invoice attachments 33 } `positional-args:"true" optional:"true"` 34 Name string `long:"name" optional:"true" description:"Full name of the contractor"` 35 Contact string `long:"contact" optional:"true" description:"Email address or contact of the contractor"` 36 Location string `long:"location" optional:"true" description:"Location (e.g. Dallas, TX, USA) of the contractor"` 37 PaymentAddress string `long:"paymentaddress" optional:"true" description:"Payment address for this invoice."` 38 Rate string `long:"rate" optional:"true" description:"Hourly rate for labor."` 39 } 40 41 // Execute executes the edit invoice command. 42 func (cmd *EditInvoiceCmd) Execute(args []string) error { 43 month := cmd.Args.Month 44 year := cmd.Args.Year 45 token := cmd.Args.Token 46 csvFile := cmd.Args.CSV 47 attachmentFiles := cmd.Args.Attachments 48 49 if csvFile == "" { 50 return errInvoiceCSVNotFound 51 } 52 53 // Check for user identity 54 if cfg.Identity == nil { 55 return shared.ErrUserIdentityNotFound 56 } 57 58 // Get server public key 59 vr, err := client.Version() 60 if err != nil { 61 return err 62 } 63 64 if cmd.Name == "" || cmd.Location == "" || cmd.PaymentAddress == "" { 65 reader := bufio.NewReader(os.Stdin) 66 if cmd.Name == "" { 67 fmt.Print("Enter name for the invoice: ") 68 cmd.Name, _ = reader.ReadString('\n') 69 } 70 if cmd.Contact == "" { 71 fmt.Print("Enter email to associate with this invoice: ") 72 cmd.Contact, _ = reader.ReadString('\n') 73 } 74 if cmd.Location == "" { 75 fmt.Print("Enter location to associate with this invoice: ") 76 cmd.Location, _ = reader.ReadString('\n') 77 } 78 if cmd.PaymentAddress == "" { 79 fmt.Print("Enter payment address for this invoice: ") 80 cmd.PaymentAddress, _ = reader.ReadString('\n') 81 } 82 if cmd.Rate == "" { 83 fmt.Print("Enter hourly rate for this invoice: ") 84 cmd.Rate, _ = reader.ReadString('\n') 85 } 86 fmt.Print("\nPlease carefully review your information and ensure it's " + 87 "correct. If not, press Ctrl + C to exit. Or, press Enter to continue " + 88 "your registration.") 89 reader.ReadString('\n') 90 } 91 92 var csv []byte 93 files := make([]www.File, 0, www.PolicyMaxImages+1) 94 // Read markdown file into memory and convert to type File 95 fpath := util.CleanAndExpandPath(csvFile) 96 csv, err = os.ReadFile(fpath) 97 if err != nil { 98 return fmt.Errorf("ReadFile %v: %v", fpath, err) 99 } 100 101 invInput, err := validateParseCSV(csv) 102 if err != nil { 103 return fmt.Errorf("Parsing CSV failed: %v", err) 104 } 105 106 invInput.Month = month 107 invInput.Year = year 108 invInput.ContractorName = strings.TrimSpace(cmd.Name) 109 invInput.ContractorLocation = strings.TrimSpace(cmd.Location) 110 invInput.ContractorContact = strings.TrimSpace(cmd.Contact) 111 invInput.PaymentAddress = strings.TrimSpace(cmd.PaymentAddress) 112 invInput.Version = v1.InvoiceInputVersion 113 114 rate, err := strconv.Atoi(strings.TrimSpace(cmd.Rate)) 115 if err != nil { 116 return fmt.Errorf("invalid rate entered, please try again") 117 } 118 invInput.ContractorRate = uint(rate) 119 120 b, err := json.Marshal(invInput) 121 if err != nil { 122 return fmt.Errorf("Marshal: %v", err) 123 } 124 125 f := www.File{ 126 Name: "invoice.json", 127 MIME: mime.DetectMimeType(b), 128 Digest: hex.EncodeToString(util.Digest(b)), 129 Payload: base64.StdEncoding.EncodeToString(b), 130 } 131 132 files = append(files, f) 133 134 // Read attachment files into memory and convert to type File 135 for _, file := range attachmentFiles { 136 path := util.CleanAndExpandPath(file) 137 attachment, err := os.ReadFile(path) 138 if err != nil { 139 return fmt.Errorf("ReadFile %v: %v", path, err) 140 } 141 142 f := www.File{ 143 Name: filepath.Base(file), 144 MIME: mime.DetectMimeType(attachment), 145 Digest: hex.EncodeToString(util.Digest(attachment)), 146 Payload: base64.StdEncoding.EncodeToString(attachment), 147 } 148 149 files = append(files, f) 150 } 151 152 // Compute merkle root and sign it 153 sig, err := signedMerkleRoot(files, nil, cfg.Identity) 154 if err != nil { 155 return fmt.Errorf("SignMerkleRoot: %v", err) 156 } 157 158 // Setup edit invoice request 159 ei := &v1.EditInvoice{ 160 Token: token, 161 Files: files, 162 PublicKey: hex.EncodeToString(cfg.Identity.Public.Key[:]), 163 Signature: sig, 164 } 165 166 // Print request details 167 err = shared.PrintJSON(ei) 168 if err != nil { 169 return err 170 } 171 172 // Send request 173 eir, err := client.EditInvoice(ei) 174 if err != nil { 175 return err 176 } 177 178 // Verify the censorship record 179 pr := v1.InvoiceRecord{ 180 Files: ei.Files, 181 PublicKey: ei.PublicKey, 182 Signature: ei.Signature, 183 CensorshipRecord: eir.Invoice.CensorshipRecord, 184 } 185 err = verifyInvoice(pr, vr.PubKey) 186 if err != nil { 187 return fmt.Errorf("unable to verify invoice %v: %v", 188 eir.Invoice.CensorshipRecord.Token, err) 189 } 190 191 // Print response details 192 return shared.PrintJSON(eir) 193 } 194 195 // editInvoiceHelpMsg is the output of the help command when 'editinvoice' 196 // is specified. 197 const editInvoiceHelpMsg = `editinvoice [flags] "month" "year" token" "csvfile" "attachmentfiles" 198 199 Edit a invoice. 200 201 Arguments: 202 1. month (uint, required) Invoice Month 203 2. year (uint, required) Invoice Year 204 1. token (string, required) Invoice censorship token 205 2. csvfile (string, required) Edited invoice 206 3. attachmentfiles (string, optional) Attachments 207 208 Flags: 209 --name (string, optional) Fill in contractor name 210 --contact (string, optional) Fill in email address or contact of the contractor 211 --location (string, optional) Fill in contractor location (e.g. Dallas, TX, USA) of the contractor 212 --paymentaddress (string, optional) Fill in payment address for this invoice. 213 --rate (string, optional) Fill in contractor pay rate for labor. 214 215 Request: 216 { 217 "month": (uint) Invoice Month 218 "token": (string) Censorship token 219 "files": [ 220 { 221 "name": (string) Filename 222 "mime": (string) Mime type 223 "digest": (string) File digest 224 "payload": (string) File payload 225 } 226 ], 227 "publickey": (string) Public key used to sign invoice 228 "signature": (string) Signature of the merkle root 229 } 230 231 Response: 232 { 233 "invoice": { 234 "month": (uint16) Month of invoice 235 "year": (uint16) Year of invoice 236 "state": (PropStateT) Current state of invoice 237 "status": (PropStatusT) Current status of invoice 238 "timestamp": (int64) Timestamp of last update of invoice 239 "userid": (string) ID of user who submitted invoice 240 "username": (string) Username of user who submitted invoice 241 "publickey": (string) Public key used to sign invoice 242 "signature": (string) Signature of merkle root 243 "files": [ 244 { 245 "name": (string) Filename 246 "mime": (string) Mime type 247 "digest": (string) File digest 248 "payload": (string) File payload 249 } 250 ], 251 "numcomments": (uint) Number of comments on the invoice 252 "version": (string) Version of invoice 253 "censorshiprecord": { 254 "token": (string) Censorship token 255 "merkle": (string) Merkle root of invoice 256 "signature": (string) Server side signature of []byte(Merkle+Token) 257 } 258 } 259 }`