197 lines
4.8 KiB
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
|
|
}
|