moved a lot

This commit is contained in:
vfs committed 2026-05-09 14:40:14 +02:00
1 parent 5772a7d855
commit 136a1c34b3
98 files changed
+6950 -2183

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
package crypto
import (
"crypto/sha256"
"encoding/binary"
)
func VoteMessage(issueID, optionID int64, nonce []byte) [32]byte {
buf := make([]byte, 16+len(nonce))
binary.BigEndian.PutUint64(buf[0:8], uint64(issueID))
binary.BigEndian.PutUint64(buf[8:16], uint64(optionID))
copy(buf[16:], nonce)
return sha256.Sum256(buf)
}
@@ -1,30 +1,56 @@
package data
import (
"time"
"database/sql"
"encoding/pem"
"context"
"database/sql"
"errors"
"fmt"
"math/big"
"crypto/rsa"
"crypto/x509"
"time"
"github.com/lib/pq"
)
type BlindSignRequest struct {
type BlindSign struct {
UserID int64 `json:"user_id"`
IssueID int64 `json:"issue_id"`
Created time.Time `json:"created"`
}
type BlindSignRequestModel struct {
type BlindSignModel struct {
DB *sql.DB
}
func (m BlindSignRequestModel) Insert(blind_sign *BlindSignRequest) error {
func (m BlindSignModel) Get(userID int64, issueID int64) (*BlindSign, error) {
query :=
`SELECT user_id, issue_id, created
FROM blind_signs
WHERE user_id = $1 AND issue_id = $2`
args := []interface{}{
userID,
issueID,
}
var blindSign BlindSign
err := m.DB.QueryRow(query, args...).Scan(&blindSign.UserID, &blindSign.IssueID, &blindSign.Created)
if err != nil {
switch {
case errors.Is(err, sql.ErrNoRows):
return nil, ErrRecordNotFound
default:
return nil, err
}
}
return &blindSign, nil
}
func (m BlindSignModel) Insert(blind_sign *BlindSign) error {
query := `
INSERT INTO blind_sign_requests (user_id, issue_id)
INSERT INTO blind_signs (user_id, issue_id)
VALUES ($1, $2)
RETURNING created`
@@ -33,12 +59,16 @@ RETURNING created`
blind_sign.IssueID,
}
return m.DB.QueryRow(query, args...).Scan(
&blind_sign.Created,
)
err := m.DB.QueryRow(query, args...).Scan(&blind_sign.Created)
if pgErr, ok := err.(*pq.Error); ok {
if pgErr.Code == "23505" {
return ErrDuplicateBlindSign
}
}
return err
}
func (m BlindSignRequestModel) BlindSign(issueID int64, blindedVoteBytes []byte) ([]byte, error) {
func (m BlindSignModel) BlindSign(issueID int64, blindedVoteBytes []byte) ([]byte, error) {
if issueID < 1 {
return nil, ErrRecordNotFound
}
@@ -77,12 +107,3 @@ func (m BlindSignRequestModel) BlindSign(issueID int64, blindedVoteBytes []byte)
return sig.Bytes(), nil
}
func parsePrivateKey(pemBytes []byte) (*rsa.PrivateKey, error) {
block, _ := pem.Decode(pemBytes)
if block == nil {
return nil, errors.New("failed to decode PEM block")
}
return x509.ParsePKCS1PrivateKey(block.Bytes)
}
+16
View File
@@ -0,0 +1,16 @@
package data
import(
"crypto/rsa"
"encoding/pem"
"errors"
"crypto/x509"
)
func parsePrivateKey(pemBytes []byte) (*rsa.PrivateKey, error) {
block, _ := pem.Decode(pemBytes)
if block == nil {
return nil, errors.New("failed to decode PEM block")
}
return x509.ParsePKCS1PrivateKey(block.Bytes)
}
+57 -28
View File
@@ -2,11 +2,12 @@ package data
import (
"time"
"party.at/party/internal/validator"
"database/sql"
"errors"
"context"
"fmt"
"party.at/party/internal/validator"
)
type Issue struct {
@@ -38,8 +39,8 @@ type IssueModel struct {
}
func (m IssueModel) Insert(issue *Issue) error {
query := `
INSERT INTO issues (title, description, start_time, end_time, rsa_n, rsa_e, rsa_private_pem)
query :=
`INSERT INTO issues (title, description, start_time, end_time, rsa_n, rsa_e, rsa_private_pem)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, created, version`
@@ -60,14 +61,64 @@ RETURNING id, created, version`
)
}
// Add a placeholder method for fetching a specific record from the issues table.
func (m IssueModel) InsertWithOptions(issue *Issue, options []*Option) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
tx, err := m.DB.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
query :=
`INSERT INTO issues (title, description, start_time, end_time, rsa_n, rsa_e, rsa_private_pem)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, created, version`
args := []any{
issue.Title,
issue.Description,
issue.StartTime,
issue.EndTime,
issue.N,
issue.E,
issue.PrivatePem,
}
err = tx.QueryRowContext(ctx, query, args...).Scan(&issue.ID, &issue.Created, &issue.Version)
if err != nil {
return err
}
for _, option := range options {
option.IssueID = issue.ID
err = tx.QueryRowContext(ctx,
`INSERT INTO options (issue_id, label)
VALUES ($1, $2)
RETURNING id, created, version`,
option.IssueID,
option.Label,
).Scan(&option.ID, &option.Created, &option.Version)
if err != nil {
return err
}
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func (m IssueModel) Get(id int64) (*Issue, error) {
if id < 1 {
return nil, ErrRecordNotFound
}
query := `
SELECT id, title, description, start_time, end_time, rsa_n, rsa_e, rsa_private_pem, created, version
query :=
`SELECT id, title, description, start_time, end_time, rsa_n, rsa_e, rsa_private_pem, created, version
FROM issues
WHERE id = $1`
@@ -138,35 +189,25 @@ func (m IssueModel) Update(issue *Issue) error {
return nil
}
// Add a placeholder method for deleting a specific record from the issues table.
func (m IssueModel) Delete(id int64) error {
if id < 1 {
return ErrRecordNotFound
}
// Construct the SQL query to delete the record.
query := `
DELETE FROM issues
WHERE id = $1`
// Execute the SQL query using the Exec() method, passing in the id variable as
// the value for the placeholder parameter. The Exec() method returns a sql.Result
// object.
result, err := m.DB.Exec(query, id)
if err != nil {
return err
}
// Call the RowsAffected() method on the sql.Result object to get the number of rows
// affected by the query.
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
// If no rows were affected, we know that the issues table didn't contain a record
// with the provided ID at the moment we tried to delete it. In that case we
// return an ErrRecordNotFound error.
if rowsAffected == 0 {
return ErrRecordNotFound
}
@@ -175,7 +216,6 @@ func (m IssueModel) Delete(id int64) error {
}
func (m IssueModel) GetAll(title string, filters Filters) ([]*Issue, Metadata, error) {
// Construct the SQL query to retrieve all issue records.
query := fmt.Sprintf(`
SELECT
COUNT(*) OVER(), id, title, description, start_time, end_time, rsa_n, rsa_e, rsa_private_pem, created, version
@@ -193,7 +233,6 @@ func (m IssueModel) GetAll(title string, filters Filters) ([]*Issue, Metadata, e
filters.sortDirection(),
)
// Create a context with a 3-second timeout.
ctx, cancel := context.WithTimeout(context.Background(), 3 * time.Second)
defer cancel()
@@ -204,20 +243,14 @@ func (m IssueModel) GetAll(title string, filters Filters) ([]*Issue, Metadata, e
return nil, Metadata{}, err
}
// Importantly, defer a call to rows.Close() to ensure that the resultset is closed
// before GetAll() returns.
defer rows.Close()
totalRecords := 0
issues := []*Issue{}
// Use rows.Next to iterate through the rows in the resultset.
for rows.Next() {
// Initialize an empty Issue struct to hold the data for an individual issue.
var issue Issue
// Scan the values from the row into the Issue struct. Again, note that we're
// using the pq.Array() adapter on the genres field here.
err := rows.Scan(
&totalRecords,
&issue.ID,
@@ -236,18 +269,14 @@ func (m IssueModel) GetAll(title string, filters Filters) ([]*Issue, Metadata, e
return nil, Metadata{}, err
}
// Add the Issue struct to the slice.
issues = append(issues, &issue)
}
// When the rows.Next() loop has finished, call rows.Err() to retrieve any error
// that was encountered during the iteration.
if err = rows.Err(); err != nil {
return nil, Metadata{}, err
}
metadata := calculateMetadata(totalRecords, filters.Page, filters.PageSize)
// If everything went OK, then return the slice of issues.
return issues, metadata, nil
}
+8 -3
View File
@@ -8,7 +8,6 @@ import (
var (
ErrRecordNotFound = errors.New("record not found")
ErrEditConflict = errors.New("edit conflict")
ErrInvalidBlindedVote = errors.New("invalid blinded vote")
)
type Models struct {
@@ -17,7 +16,10 @@ type Models struct {
Issues IssueModel
Tokens TokenModel
Permissions PermissionModel
BlindSignRequests BlindSignRequestModel
Roles RoleModel
BlindSigns BlindSignModel
Votes VoteModel
Options OptionModel
}
func NewModels(db *sql.DB) Models {
@@ -27,6 +29,9 @@ func NewModels(db *sql.DB) Models {
Issues: IssueModel{DB: db},
Tokens: TokenModel{DB: db},
Permissions: PermissionModel{DB: db},
BlindSignRequests: BlindSignRequestModel{DB: db},
Roles: RoleModel{DB: db},
BlindSigns: BlindSignModel{DB: db},
Votes: VoteModel{DB: db},
Options: OptionModel{DB: db},
}
}
+100
View File
@@ -0,0 +1,100 @@
package data
import (
"errors"
"time"
"database/sql"
"party.at/party/internal/validator"
)
type Option struct {
ID int64 `json:"id"`
IssueID int64 `json:"issue_id"`
Label string `json:"label"`
Created time.Time `json:"created"`
Version int `json:"version"`
}
func ValidateOption(v *validator.Validator, option *Option) {
v.Check(option.Label != "", "options", "option labels must not be empty")
}
type OptionModel struct {
DB *sql.DB
}
func (m OptionModel) Insert(option *Option) error {
query := `
INSERT INTO options (issue_id, label)
VALUES ($1, $2)
RETURNING id, created, version`
args := []interface{}{
option.IssueID,
option.Label,
}
return m.DB.QueryRow(query, args...).Scan(
&option.ID,
&option.Created,
&option.Version,
)
}
func (m OptionModel) GetAllForIssue(issueID int64) ([]*Option, error) {
query := `
SELECT id, issue_id, label, created, version
FROM options
WHERE issue_id = $1
ORDER BY id`
rows, err := m.DB.Query(query, issueID)
if err != nil {
return nil, err
}
defer rows.Close()
var options []*Option
for rows.Next() {
var option Option
err := rows.Scan(&option.ID, &option.IssueID, &option.Label, &option.Created, &option.Version)
if err != nil {
return nil, err
}
options = append(options, &option)
}
return options, rows.Err()
}
func (m OptionModel) Get(id int64) (*Option, error) {
if id < 1 {
return nil, ErrRecordNotFound
}
query := `
SELECT id, issue_id, label, created, version
FROM options
WHERE id = $1`
var option Option
err := m.DB.QueryRow(query, id).Scan(
&option.ID,
&option.IssueID,
&option.Label,
&option.Created,
&option.Version,
)
if err != nil {
switch {
case errors.Is(err, sql.ErrNoRows):
return nil, ErrRecordNotFound
default:
return nil, err
}
}
return &option, nil
}
+8 -25
View File
@@ -4,7 +4,6 @@ import (
"context"
"database/sql"
"time"
"github.com/lib/pq"
)
type Permissions []string
@@ -24,14 +23,15 @@ type PermissionModel struct {
}
func (m PermissionModel) GetAllForUser(userID int64) (Permissions, error) {
query :=`
query := `
SELECT permissions.code
FROM permissions
INNER JOIN users_permissions ON users_permissions.permission_id = permissions.id
INNER JOIN users ON users_permissions.user_id = users.id
WHERE users.id = $1`
INNER JOIN roles_permissions ON roles_permissions.permission_id = permissions.id
INNER JOIN roles ON roles_permissions.role_id = roles.id
INNER JOIN users_roles ON users_roles.role_id = roles.id
WHERE users_roles.user_id = $1`
ctx, cancel := context.WithTimeout(context.Background(), 3 * time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
rows, err := m.DB.QueryContext(ctx, query, userID)
@@ -43,27 +43,10 @@ WHERE users.id = $1`
var permissions Permissions
for rows.Next() {
var permission string
err := rows.Scan(&permission)
if err != nil {
if err := rows.Scan(&permission); err != nil {
return nil, err
}
permissions = append(permissions, permission)
}
if err = rows.Err(); err != nil {
return nil, err
}
return permissions, nil
}
func (m PermissionModel) AddForUser(userID int64, codes ...string) error {
query :=`
INSERT INTO users_permissions
SELECT $1, permissions.id FROM permissions WHERE permissions.code = ANY($2)`
ctx, cancel := context.WithTimeout(context.Background(), 3 * time.Second)
defer cancel()
_, err := m.DB.ExecContext(ctx, query, userID, pq.Array(codes))
return err
return permissions, rows.Err()
}
+64
View File
@@ -0,0 +1,64 @@
package data
import (
"context"
"database/sql"
"time"
)
const (
RoleViewer = "viewer"
RoleContributor = "contributor"
RoleAdmin = "admin"
RoleMemberOfParliament = "member_of_parliament"
RolePartyLeadership = "party_leadership"
)
type Role struct {
ID int64
Code string
}
type RoleModel struct {
DB *sql.DB
}
func (m RoleModel) AssignToUser(userID int64, roleCode string) error {
query := `
INSERT INTO users_roles
SELECT $1, id FROM roles WHERE code = $2
ON CONFLICT DO NOTHING`
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
_, err := m.DB.ExecContext(ctx, query, userID, roleCode)
return err
}
func (m RoleModel) GetAllForUser(userID int64) ([]Role, error) {
query := `
SELECT roles.id, roles.code
FROM roles
INNER JOIN users_roles ON users_roles.role_id = roles.id
WHERE users_roles.user_id = $1`
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
rows, err := m.DB.QueryContext(ctx, query, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var roles []Role
for rows.Next() {
var role Role
if err := rows.Scan(&role.ID, &role.Code); err != nil {
return nil, err
}
roles = append(roles, role)
}
return roles, rows.Err()
}
+10 -1
View File
@@ -33,7 +33,6 @@ func generateToken(userID int64, userIdentityID int64, ttl time.Duration, scope
Scope: scope,
}
// Initialize a zero-valued byte slice with a length of 16 bytes.
randomBytes := make([]byte, 16)
_, err := rand.Read(randomBytes)
@@ -82,6 +81,16 @@ func (m TokenModel) Insert(token *Token) error {
return err
}
func (m TokenModel) Delete(hash []byte) error {
query := `DELETE FROM tokens WHERE hash = $1`
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
_, err := m.DB.ExecContext(ctx, query, hash)
return err
}
func (m TokenModel) DeleteAllForUser(scope string, userID int64) error {
query :=`
DELETE FROM tokens
+1 -20
View File
@@ -117,13 +117,8 @@ SELECT id, provider_id, user_id, provider_user, password, version
FROM user_identities
WHERE id = $1`
// Declare a User struct to hold the data returned by the query.
var userIdentity UserIdentity
// Execute the query using the QueryRow() method, passing in the provided id value
// as a placeholder parameter, and scan the response data into the fields of the
// User struct. Importantly, notice that we need to convert the scan target for the
// genres column using the pq.Array() adapter function again.
err := m.DB.QueryRow(query, id).Scan(
&userIdentity.ID,
&userIdentity.ProviderID,
@@ -141,7 +136,7 @@ WHERE id = $1`
return nil, err
}
}
// Otherwise, return a pointer to the User struct.
return &userIdentity, nil
}
@@ -156,10 +151,6 @@ FROM user_identities identity
JOIN users u on identity.user_id = u.id
WHERE u.id = $1`
// Execute the query using the QueryRow() method, passing in the provided id value
// as a placeholder parameter, and scan the response data into the fields of the
// User struct. Importantly, notice that we need to convert the scan target for the
// genres column using the pq.Array() adapter function again.
rows, err := m.DB.Query(query, user_id)
if err != nil {
switch {
@@ -244,7 +235,6 @@ func (m UserIdentityModel) Update(user *UserIdentity) error {
WHERE id = $3 AND version = $4
RETURNING version`
// Create an args slice containing the values for the placeholder parameters.
args := []interface{}{
user.ProviderUserID,
user.Password.hash,
@@ -275,29 +265,20 @@ func (m UserIdentityModel) Delete(id int64) error {
return ErrRecordNotFound
}
// Construct the SQL query to delete the record.
query := `
DELETE FROM user_identities
WHERE id = $1`
// Execute the SQL query using the Exec() method, passing in the id variable as
// the value for the placeholder parameter. The Exec() method returns a sql.Result
// object.
result, err := m.DB.Exec(query, id)
if err != nil {
return err
}
// Call the RowsAffected() method on the sql.Result object to get the number of rows
// affected by the query.
rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}
// If no rows were affected, we know that the issues table didn't contain a record
// with the provided ID at the moment we tried to delete it. In that case we
// return an ErrRecordNotFound error.
if rowsAffected == 0 {
return ErrRecordNotFound
}
+60 -9
View File
@@ -2,17 +2,20 @@ package data
import (
"context"
"time"
"party.at/party/internal/validator"
"database/sql"
"github.com/lib/pq"
"errors"
"crypto/sha256"
"database/sql"
"errors"
"fmt"
"time"
"github.com/lib/pq"
"party.at/party/internal/validator"
)
var (
ErrDuplicateEmail = errors.New("duplicate email")
ErrDuplicateUser = errors.New("duplicate username")
ErrDuplicateEmail = errors.New("duplicate email")
ErrDuplicateUser = errors.New("duplicate username")
ErrInvalidCredentials = errors.New("invalid credentials")
)
var AnonymousUser = &User{}
@@ -128,8 +131,8 @@ func (m UserModel) Get(id int64) (*User, error) {
}
// Define the SQL query for retrieving the issue data.
query :=`
SELECT id, email, phone_number, country, name, alt_name, date_of_birth, address, created, last_login, activated, version
query :=
`SELECT id, email, phone_number, country, name, alt_name, date_of_birth, address, created, last_login, activated, version
FROM users
WHERE id = $1`
@@ -307,6 +310,54 @@ func (m UserModel) Update(user *User) error {
return nil
}
func (m UserModel) GetAll(filters Filters) ([]*User, Metadata, error) {
query := fmt.Sprintf(`
SELECT COUNT(*) OVER(), id, email, phone_number, country, name, alt_name, date_of_birth, address, created, last_login, activated, version
FROM users
ORDER BY %s %s, id ASC
LIMIT $1 OFFSET $2`,
filters.sortColumn(),
filters.sortDirection(),
)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
rows, err := m.DB.QueryContext(ctx, query, filters.limit(), filters.offset())
if err != nil {
return nil, Metadata{}, err
}
defer rows.Close()
var totalRecords int
var users []*User
for rows.Next() {
var user User
if err := rows.Scan(
&totalRecords,
&user.ID,
&user.Email,
&user.PhoneNumber,
&user.Country,
&user.Name,
&user.AltName,
&user.DateOfBirth,
&user.Address,
&user.Created,
&user.LastLogin,
&user.Activated,
&user.Version,
); err != nil {
return nil, Metadata{}, err
}
users = append(users, &user)
}
if err := rows.Err(); err != nil {
return nil, Metadata{}, err
}
return users, calculateMetadata(totalRecords, filters.Page, filters.PageSize), nil
}
func (m UserModel) Delete(id int64) error {
if id < 1 {
return ErrRecordNotFound
+102
View File
@@ -0,0 +1,102 @@
package data
import (
"errors"
"time"
"database/sql"
"math/big"
"context"
"party.at/party/internal/crypto"
"github.com/lib/pq"
)
var (
ErrDuplicateBlindSign = errors.New("user has already requested a blind signature for this issue")
ErrInvalidBlindedVote = errors.New("blinded_vote is out of valid range [1, n-1]")
ErrInvalidSignature = errors.New("signature verification failed")
ErrDuplicateVote = errors.New("this signature has already been used to cast a vote")
)
type Vote struct {
ID int64 `json:"id"`
OptionID int64 `json:"option_id"`
Nonce []byte `json:"nonce"`
Signature []byte `json:"signature"`
Created time.Time `json:"created"`
}
type VoteModel struct {
DB *sql.DB
}
func (m VoteModel) CountForOption(optionID int64) (int64, error) {
query := `
SELECT COUNT(v.id)
FROM options o
LEFT JOIN votes v ON v.option_id = o.id
WHERE o.id = $1
GROUP BY o.id`
ctx, cancel := context.WithTimeout(context.Background(), 3 * time.Second)
defer cancel()
var count int64
err := m.DB.QueryRowContext(ctx, query, optionID).Scan(&count)
if err != nil {
return 0, err
}
return count, err
}
func (m VoteModel) Insert(vote *Vote, issue *Issue) error {
n := new(big.Int).SetBytes(issue.N)
e := issue.E
// Recompute m = SHA-256(issueID || optionID || nonce)
expected := crypto.VoteMessage(issue.ID, vote.OptionID, vote.Nonce)
// Verify: s^e mod n == m
sig := new(big.Int).SetBytes(vote.Signature)
eInt := big.NewInt(int64(e))
recovered := new(big.Int).Exp(sig, eInt, n)
expectedInt := new(big.Int).SetBytes(expected[:])
if recovered.Cmp(expectedInt) != 0 {
return ErrInvalidSignature
}
query :=
`INSERT INTO votes (option_id, nonce, signature)
VALUES ($1, $2, $3)
RETURNING id, created`
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
args := []interface{}{
vote.OptionID,
vote.Nonce,
vote.Signature,
}
err := m.DB.QueryRowContext(ctx, query, args...).Scan(&vote.ID, &vote.Created)
if pgErr, ok := err.(*pq.Error); ok {
if pgErr.Code == "23505" {
if pgErr.Constraint == "votes_signature_key" {
return ErrDuplicateVote
}
}
}
if err != nil {
return err
}
return nil
}