Skip to content

Commit

Permalink
flush out proposal owner approve invoice
Browse files Browse the repository at this point in the history
  • Loading branch information
alexlyp committed Aug 12, 2020
1 parent 181d3ad commit 9fe258c
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 20 deletions.
77 changes: 61 additions & 16 deletions mdstream/mdstream.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,30 @@ import (

const (
// mdstream IDs
IDProposalGeneral = 0
IDRecordStatusChange = 2
IDInvoiceGeneral = 3
IDInvoiceStatusChange = 4
IDInvoicePayment = 5
IDDCCGeneral = 6
IDDCCStatusChange = 7
IDDCCSupportOpposition = 8
IDProposalGeneral = 0
IDRecordStatusChange = 2
IDInvoiceGeneral = 3
IDInvoiceStatusChange = 4
IDInvoicePayment = 5
IDDCCGeneral = 6
IDDCCStatusChange = 7
IDDCCSupportOpposition = 8
IDInvoiceProposalApprove = 9

// Note that 13 is in use by the decred plugin
// Note that 14 is in use by the decred plugin
// Note that 15 is in use by the decred plugin

// mdstream current supported versions
VersionProposalGeneral = 2
VersionRecordStatusChange = 2
VersionInvoiceGeneral = 1
VersionInvoiceStatusChange = 1
VersionInvoicePayment = 1
VersionDCCGeneral = 1
VersionDCCStatusChange = 1
VersionDCCSupposeOpposition = 1
VersionProposalGeneral = 2
VersionRecordStatusChange = 2
VersionInvoiceGeneral = 1
VersionInvoiceStatusChange = 1
VersionInvoicePayment = 1
VersionDCCGeneral = 1
VersionDCCStatusChange = 1
VersionDCCSupposeOpposition = 1
VersionInvoiceProposalApprove = 1

// Filenames of user defined metadata that is stored as politeiad
// files instead of politeiad metadata streams. This is done so
Expand Down Expand Up @@ -530,3 +532,46 @@ func DecodeDCCSupportOpposition(payload []byte) ([]DCCSupportOpposition, error)

return md, nil
}

// InvoiceProposalApprove represents an invoice status change and is stored
// in the metadata IDInvoiceProposalApprove in politeiad.
type InvoiceProposalApprove struct {
Version uint `json:"version"` // Version of the struct
PublicKey string `json:"publickey"` // Identity of the administrator
Signature string `json:"signature"` // Signature of the line item payload included
Token string `json:"token"` // Token of the invoice
Timestamp int64 `json:"timestamp"`
LineItems []byte `json:"lineitems"` // json payload of line items that are being approved
}

// EncodeInvoiceProposalApprove encodes a InvoiceProposalApprove into a
// JSON byte slice.
func EncodeInvoiceProposalApprove(md InvoiceProposalApprove) ([]byte, error) {
b, err := json.Marshal(md)
if err != nil {
return nil, err
}

return b, nil
}

// DecodeInvoiceProposalApprove decodes a JSON byte slice into a slice of
// InvoiceProposalApproves.
func DecodeInvoiceProposalApprove(payload []byte) ([]InvoiceProposalApprove, error) {
var md []InvoiceProposalApprove

d := json.NewDecoder(strings.NewReader(string(payload)))
for {
var m InvoiceProposalApprove
err := d.Decode(&m)
if err == io.EOF {
break
} else if err != nil {
return nil, err
}

md = append(md, m)
}

return md, nil
}
15 changes: 15 additions & 0 deletions politeiawww/api/cms/v1/v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const (
RouteProposalBilling = "/proposals/billing"
RouteProposalBillingSummary = "/proposals/spendingsummary"
RouteProposalBillingDetails = "/proposals/spendingdetails"
RouteProposalInvoiceApprove = "/proposals/approve"

// Invoice status codes
InvoiceStatusInvalid InvoiceStatusT = 0 // Invalid status
Expand Down Expand Up @@ -1044,3 +1045,17 @@ type ProposalBillingDetails struct {
type ProposalBillingDetailsReply struct {
Details ProposalSpending `json:"details"`
}

// ProposalOwnerApprove is used to approve or reject an proposal referenced
// invoice.
type ProposalOwnerApprove struct {
Token string `json:"token"`
Status InvoiceStatusT `json:"status"`
LineItems []LineItemsInput `json:"lineitems"`
Signature string `json:"signature"` // Signature of LineItems Approved
PublicKey string `json:"publickey"` // Public key of admin
}

// ProposalOwnerApprove used to reply to a ProposalOwnerApprove command.
type ProposalOwnerApproveReply struct {
}
35 changes: 35 additions & 0 deletions politeiawww/cmswww.go
Original file line number Diff line number Diff line change
Expand Up @@ -1094,6 +1094,38 @@ func (p *politeiawww) makeProposalsRequest(method string, route string, v interf
return responseBody, nil
}

// handleProposalInvoiceApprove handles request for proposal owners to approve
// an invoices' line items that reference their proposal.
func (p *politeiawww) handleProposalInvoiceApprove(w http.ResponseWriter, r *http.Request) {
log.Tracef("handleProposalInvoiceApprove")

var poa cms.ProposalOwnerApprove
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&poa); err != nil {
RespondWithError(w, r, 0, "handleProposalInvoiceApprove: unmarshal",
www.UserError{
ErrorCode: www.ErrorStatusInvalidInput,
})
return
}

user, err := p.getSessionUser(w, r)
if err != nil {
RespondWithError(w, r, 0,
"handleProposalInvoiceApprove: getSessionUser %v", err)
return
}

poar, err := p.processProposalInvoiceApprove(poa, user)
if err != nil {
RespondWithError(w, r, 0,
"handleProposalInvoiceApprove: processStartVoteDCC %v", err)
return
}

util.RespondWithJSON(w, http.StatusOK, poar)
}

func (p *politeiawww) setCMSWWWRoutes() {
// Templates
//p.addTemplate(templateNewProposalSubmittedName,
Expand Down Expand Up @@ -1184,6 +1216,9 @@ func (p *politeiawww) setCMSWWWRoutes() {
p.addRoute(http.MethodPost, www.PoliteiaWWWAPIRoute,
www.RouteBatchProposals, p.handlePassThroughBatchProposals,
permissionLogin)
p.addRoute(http.MethodPost, cms.APIRoute,
cms.RouteProposalInvoiceApprove, p.handleProposalInvoiceApprove,
permissionLogin)

// Unauthenticated websocket
p.addRoute("", www.PoliteiaWWWAPIRoute,
Expand Down
99 changes: 95 additions & 4 deletions politeiawww/invoices.go
Original file line number Diff line number Diff line change
Expand Up @@ -1968,13 +1968,11 @@ func (p *politeiawww) processProposalBillingSummary(pbs cms.ProposalBillingSumma
if err != nil {
return nil, err
}

var tvr www.TokenInventoryReply
err = json.Unmarshal(data, &tvr)
if err != nil {
return nil, err
}

approvedProposals := tvr.Approved

approvedProposalDetails := make([]www.ProposalRecord, 0, len(approvedProposals))
Expand Down Expand Up @@ -2074,7 +2072,6 @@ func (p *politeiawww) processProposalBillingDetails(pbd cms.ProposalBillingDetai
if err != nil {
return nil, err
}

spendingSummary := cms.ProposalSpending{}
spendingSummary.Token = pbd.Token

Expand Down Expand Up @@ -2104,7 +2101,6 @@ func (p *politeiawww) processProposalBillingDetails(pbd cms.ProposalBillingDetai
if err != nil {
return nil, err
}

var pdr www.ProposalDetailsReply
err = json.Unmarshal(data, &pdr)
if err != nil {
Expand All @@ -2118,3 +2114,98 @@ func (p *politeiawww) processProposalBillingDetails(pbd cms.ProposalBillingDetai
reply.Details = spendingSummary
return reply, nil
}

// processProposalInvoiceApprove appends a proposal owners approval onto the
// invoice records metadata which will allow admins to determine if an invoice
// is fully approved.
func (p *politeiawww) processProposalInvoiceApprove(poa cms.ProposalOwnerApprove, u *user.User) (*cms.ProposalOwnerApproveReply, error) {
invRec, err := p.getInvoice(poa.Token)
if err != nil {
return nil, err
}
cmsUser, err := p.getCMSUserByID(u.ID.String())
if err != nil {
return nil, err
}
proposalFound := false
for _, lineItem := range invRec.Input.LineItems {
// If the proposal token is empty then don't display it for the
// non invoice owner or admin.
if lineItem.ProposalToken == "" {
continue
}
// Check to see that proposal token matches an owned proposal by
// the recommending user, if not don't include the line item in the
// list of line items to display.
if stringInSlice(cmsUser.ProposalsOwned, lineItem.ProposalToken) {
proposalFound = true
}
}
if !proposalFound {
err := www.UserError{
ErrorCode: www.ErrorStatusUserActionNotAllowed,
}
return nil, err
}

b, err := json.Marshal(poa.LineItems)
if err != nil {
return nil, err
}

// Validate signature
msg := fmt.Sprintf("%v", b)
err = validateSignature(poa.PublicKey, poa.Signature, msg)
if err != nil {
return nil, err
}

// Create the change record.
c := mdstream.InvoiceProposalApprove{
Version: mdstream.VersionInvoiceProposalApprove,
PublicKey: u.PublicKey(),
Timestamp: time.Now().Unix(),
Signature: poa.Signature,
Token: poa.Token,
LineItems: b,
}
blob, err := mdstream.EncodeInvoiceProposalApprove(c)
if err != nil {
return nil, err
}
challenge, err := util.Random(pd.ChallengeSize)
if err != nil {
return nil, err
}

pdCommand := pd.UpdateVettedMetadata{
Challenge: hex.EncodeToString(challenge),
Token: poa.Token,
MDAppend: []pd.MetadataStream{
{
ID: mdstream.IDInvoiceProposalApprove,
Payload: string(blob),
},
},
}

responseBody, err := p.makeRequest(http.MethodPost, pd.UpdateVettedMetadataRoute, pdCommand)
if err != nil {
return nil, err
}

var pdReply pd.UpdateVettedMetadataReply
err = json.Unmarshal(responseBody, &pdReply)
if err != nil {
return nil, fmt.Errorf("Could not unmarshal UpdateVettedMetadataReply: %v",
err)
}

// Verify the UpdateVettedMetadata challenge.
err = util.VerifyChallenge(p.cfg.Identity, challenge, pdReply.Response)
if err != nil {
return nil, err
}

return &cms.ProposalOwnerApproveReply{}, nil
}

0 comments on commit 9fe258c

Please sign in to comment.