party/cmd/party/common/issues.go

197 lines
4.8 KiB
Go

package common
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"errors"
"time"
"party.at/party/internal/data"
"party.at/party/internal/validator"
)
type IssueDetail struct {
data.Issue
CanVote bool `json:"can_vote"`
}
type OptionResult struct {
data.Option
VoteCount int64 `json:"vote_count"`
}
type IssueWithOptions struct {
IssueDetail
Options []OptionResult
}
type PublicKey struct {
N string `json:"n"`
E int `json:"e"`
}
func (app *Application) FetchIssues(title string, filters data.Filters, user *data.User) ([]IssueDetail, data.Metadata, error) {
issues, metadata, err := app.Models.Issues.GetAll(title, filters)
if err != nil {
return nil, data.Metadata{}, err
}
details := make([]IssueDetail, len(issues))
for i, issue := range issues {
canVote := false
if !user.IsAnonymous() {
_, err = app.Models.BlindSigns.Get(user.ID, issue.ID)
switch {
case errors.Is(err, data.ErrRecordNotFound):
canVote = true
case err != nil:
return nil, data.Metadata{}, err
}
}
details[i] = IssueDetail{Issue: *issue, CanVote: canVote}
}
return details, metadata, nil
}
func (app *Application) CreateIssue(title, description string, startTime, endTime time.Time, optionLabels []string) (*data.Issue, []*data.Option, error) {
issue := &data.Issue{
Title: title,
Description: description,
StartTime: startTime,
EndTime: endTime,
}
options := make([]*data.Option, len(optionLabels))
for i, label := range optionLabels {
options[i] = &data.Option{Label: label}
}
v := validator.New()
v.Check(len(optionLabels) >= 2, "options", "must provide at least two options")
v.Check(len(optionLabels) <= 10, "options", "must not provide more than ten options")
for _, option := range options {
data.ValidateOption(v, option)
}
if data.ValidateIssue(v, issue); !v.Valid() {
return nil, nil, data.ErrValidationFailed.(*data.Error).WithDetails(v.Errors)
}
n, e, privatePEM, err := generateIssueKey()
if err != nil {
return nil, nil, err
}
issue.N = n
issue.E = e
issue.PrivatePem = privatePEM
if err = app.Models.Issues.InsertWithOptions(issue, options); err != nil {
return nil, nil, err
}
return issue, options, nil
}
func (app *Application) GetIssue(id int64, user *data.User) (*IssueWithOptions, error) {
issue, err := app.Models.Issues.Get(id)
if err != nil {
return nil, err
}
canVote := false
if !user.IsAnonymous() {
_, err = app.Models.BlindSigns.Get(user.ID, issue.ID)
switch {
case errors.Is(err, data.ErrRecordNotFound):
canVote = true
case err != nil:
return nil, err
}
}
options, err := app.Models.Options.GetAllForIssue(id)
if err != nil {
return nil, err
}
results := make([]OptionResult, len(options))
for i, o := range options {
voteCount, err := app.Models.Votes.CountForOption(o.ID)
if err != nil {
return nil, err
}
results[i] = OptionResult{Option: *o, VoteCount: voteCount}
}
return &IssueWithOptions{
IssueDetail: IssueDetail{Issue: *issue, CanVote: canVote},
Options: results,
}, nil
}
func (app *Application) UpdateIssue(id int64, title, description *string, startTime, endTime *time.Time) (*data.Issue, error) {
issue, err := app.Models.Issues.Get(id)
if err != nil {
return nil, err
}
if title != nil { issue.Title = *title }
if description != nil { issue.Description = *description }
if startTime != nil { issue.StartTime = *startTime }
if endTime != nil { issue.EndTime = *endTime }
v := validator.New()
if data.ValidateIssue(v, issue); !v.Valid() {
return nil, data.ErrValidationFailed.(*data.Error).WithDetails(v.Errors)
}
if err = app.Models.Issues.Update(issue); err != nil {
return nil, err
}
return issue, nil
}
func (app *Application) DeleteIssue(id int64) error {
return app.Models.Issues.Delete(id)
}
func (app *Application) GetIssuePublicKey(id int64) (*PublicKey, error) {
issue, err := app.Models.Issues.Get(id)
if err != nil {
return nil, err
}
return &PublicKey{N: hex.EncodeToString(issue.N), E: issue.E}, nil
}
func (app *Application) BlindSign(issueID int64, blindedVote []byte, user *data.User) ([]byte, error) {
issue, err := app.Models.Issues.Get(issueID)
if err != nil {
return nil, err
}
if issue.StartTime.After(time.Now()) {
return nil, data.ErrHasNotStarted
}
blindSign := &data.BlindSign{UserID: user.ID, IssueID: issue.ID}
if err = app.Models.BlindSigns.Insert(blindSign); err != nil {
return nil, err
}
return app.Models.BlindSigns.BlindSign(issue.ID, blindedVote)
}
func generateIssueKey() ([]byte, int, []byte, error) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, 0, nil, err
}
privDER := x509.MarshalPKCS1PrivateKey(key)
privPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: privDER})
return key.N.Bytes(), key.E, privPEM, nil
}