2 Commits

Author SHA1 Message Date
tueem 85e4df52a5 feat(*): initial commit
build / Go-Build (push) Successful in 1m1s
2026-06-24 00:16:21 +02:00
tueem 3cf9e6f266 feat(db): implement basic db 2026-06-19 12:24:55 +02:00
20 changed files with 1189 additions and 1 deletions
+16
View File
@@ -0,0 +1,16 @@
# Build stage
FROM golang:latest AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -v -o outfit-voting-abi26 ./cmd/
# Final stage
FROM debian:bookworm-slim
WORKDIR /root/
EXPOSE 4000
COPY --from=builder /app/outfit-voting-abi26 /usr/local/bin/outfit-voting-abi26
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
+6
View File
@@ -0,0 +1,6 @@
package outfitvotingabi26
import "embed"
//go:embed frontend
var frontendFS embed.FS
+51 -1
View File
@@ -1 +1,51 @@
package cmd package main
import (
"flag"
"github.com/lib/pq"
"tomatentum.net/outfit-voting-abi26/internal/auth"
"tomatentum.net/outfit-voting-abi26/internal/database"
"tomatentum.net/outfit-voting-abi26/internal/http"
)
var config pq.Config
var (
oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURI string
jwtSigningKey string
)
func main() {
handleflag()
database.OpenDB(config)
defer database.CloseDB()
auth.SetSigningKey(jwtSigningKey)
if err := auth.InitProvider(oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURI); err != nil {
panic(err)
}
if err := http.Start(); err != nil {
panic(err)
}
}
func handleflag() {
config.SSLMode = pq.SSLModePrefer
flag.StringVar(&config.Host, "dbhost", "localhost", "Postgres host")
config.Port = uint16(*flag.Uint("dbport", 5432, "Postgres port"))
flag.StringVar(&config.Database, "dbname", "postgres", "Postgres db name")
flag.StringVar(&config.User, "dbuser", "postgres", "Postgres user")
flag.StringVar(&config.Password, "dbpass", "", "Postgres password")
flag.StringVar(&oidcIssuer, "oidcIssuer", "", "The OIDC Issuer")
flag.StringVar(&oidcClientID, "oidcClientID", "", "The OIDC Client ID")
flag.StringVar(&oidcClientSecret, "oidcClientSecret", "", "The OIDC Client Secret")
flag.StringVar(&oidcRedirectURI, "oidcRedirectURI", "", "The OIDC Redirect URI")
flag.StringVar(&jwtSigningKey, "signingKey", "UNSAFE", "The JWT signingkey")
flag.Parse()
}
+45
View File
@@ -0,0 +1,45 @@
#!/bin/sh
CMD="outfit-voting-abi26"
if [ -n "$DB_HOST" ]; then
CMD="$CMD --dbhost $DB_HOST"
fi
if [ -n "$DB_PORT" ]; then
CMD="$CMD --dbport $DB_PORT"
fi
if [ -n "$DB_NAME" ]; then
CMD="$CMD --dbname $DB_NAME"
fi
if [ -n "$DB_USER" ]; then
CMD="$CMD --dbuser $DB_USER"
fi
if [ -n "$DB_PASS" ]; then
CMD="$CMD --dbpass $DB_PASS"
fi
if [ -n "$OIDC_ISSUER" ]; then
CMD="$CMD --oidcIssuer $OIDC_ISSUER"
fi
if [ -n "$OIDC_CLIENTID" ]; then
CMD="$CMD --oidcClientID $OIDC_CLIENTID"
fi
if [ -n "$OIDC_SECRET" ]; then
CMD="$CMD --oidcClientSecret $OIDC_SECRET"
fi
if [ -n "$OIDC_REDIRECT" ]; then
CMD="$CMD --oidcRedirectURI $OIDC_REDIRECT"
fi
if [ -n "$SIGNINGKEY" ]; then
CMD="$CMD --signingKey $SIGNINGKEY"
fi
eval exec $CMD "$@"
View File
+8
View File
@@ -1,3 +1,11 @@
module tomatentum.net/outfit-voting-abi26 module tomatentum.net/outfit-voting-abi26
go 1.25.5 go 1.25.5
require (
github.com/coreos/go-oidc/v3 v3.19.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/lib/pq v1.12.3 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
)
+14
View File
@@ -0,0 +1,14 @@
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coocood/freecache v1.2.7 h1:IDP0x1Yg8sgRmsSWzFyhaB+amYJpKS7v5QIXNHxXvM8=
github.com/coocood/freecache v1.2.7/go.mod h1:+Ga2+A5/0D6MMistGuoeKZaZucAGZ56u+fYKiY+xqNA=
github.com/coreos/go-oidc/v3 v3.19.0 h1:F/xyOi3x1UnG1U27YVnM1N6bHiL1K2upi6U/0qr8r+I=
github.com/coreos/go-oidc/v3 v3.19.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
+54
View File
@@ -0,0 +1,54 @@
package auth
import (
"time"
"github.com/golang-jwt/jwt/v5"
)
var signingkey []byte
type JWTClaims struct {
User string `json:"user"`
jwt.RegisteredClaims
}
func SetSigningKey(key string) {
signingkey = []byte(key)
}
func NewJWT(user string) (string, error) {
claims := JWTClaims{
user,
jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)), // Expires in 24 hours
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "outfit-voting-abi26",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(signingkey)
}
func ValidateJWT(token string) (string, error) {
claims := &JWTClaims{}
parsed, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return signingkey, nil
})
if err != nil {
return "", err
}
claims, ok := parsed.Claims.(*JWTClaims)
if !ok {
return "", jwt.ErrTokenInvalidClaims
}
return claims.User, nil
}
+125
View File
@@ -0,0 +1,125 @@
package auth
import (
"context"
"errors"
"log"
"net/http"
"slices"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
"tomatentum.net/outfit-voting-abi26/internal/util"
)
var (
provider *oidc.Provider
verifier *oidc.IDTokenVerifier
config oauth2.Config
ctx context.Context
authCookie string = "outfit-voting-abi26-auth"
)
func InitProvider(issuer, id, secret, redirect string) error {
ctx = context.Background()
var err error
provider, err = oidc.NewProvider(ctx, issuer)
if err != nil {
return err
}
verifier = provider.Verifier(&oidc.Config{
ClientID: id,
})
config = oauth2.Config{
ClientID: id,
ClientSecret: secret,
Endpoint: provider.Endpoint(),
RedirectURL: redirect,
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
return nil
}
func GetToken(code string) (*oidc.IDToken, error) {
token, err := config.Exchange(ctx, code)
if err != nil {
return &oidc.IDToken{}, err
}
rawToken, ok := token.Extra("id_token").(string)
if !ok {
return &oidc.IDToken{}, errors.New("id_token missing")
}
return verifier.Verify(ctx, rawToken)
}
func Redirect(w http.ResponseWriter, r *http.Request) {
state, nonce := setCookies(w, r)
AddRedirect(state, r.RequestURI)
url := config.AuthCodeURL(state, oidc.Nonce(nonce))
http.Redirect(w, r, url, http.StatusFound)
}
func CheckState(r *http.Request) error {
cookie, err := r.Cookie("state")
if err != nil {
return errors.Join(err, errors.New("State not found"))
}
log.Printf("DEBUG: statecookie: %s, statequery: %s\n", cookie.Value, r.URL.Query().Get("state"))
if r.URL.Query().Get("state") != cookie.Value {
return errors.New("State did not match")
}
return nil
}
func CheckNonce(r *http.Request, idtoken *oidc.IDToken) error {
cookie, err := r.Cookie("nonce")
if err != nil {
return errors.Join(err, errors.New("Nonce not found"))
}
if idtoken.Nonce != cookie.Value {
return errors.New("Nonce did not match")
}
return nil
}
func CheckAudience(idtoken *oidc.IDToken) error {
if !slices.Contains(idtoken.Audience, config.ClientID) {
return errors.New("Audience does not match")
}
return nil
}
func setCookies(w http.ResponseWriter, r *http.Request) (state, nonce string) {
state, err := util.RandString(16)
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
nonce, err = util.RandString(16)
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
setCallbackCookie(w, r, "state", state)
setCallbackCookie(w, r, "nonce", nonce)
return state, nonce
}
func setCallbackCookie(w http.ResponseWriter, r *http.Request, name, value string) {
c := &http.Cookie{
Name: name,
Value: value,
MaxAge: int(time.Hour.Seconds()),
Secure: r.TLS != nil,
HttpOnly: true,
}
http.SetCookie(w, c)
}
+22
View File
@@ -0,0 +1,22 @@
package auth
import (
"sync"
)
var callbackRedirectMap map[string]string = make(map[string]string, 5)
var callbackRedirectMutex sync.Mutex
func AddRedirect(state, url string) {
callbackRedirectMutex.Lock()
callbackRedirectMap[state] = url
callbackRedirectMutex.Unlock()
}
func GetRedirect(state string) string {
callbackRedirectMutex.Lock()
redirect := callbackRedirectMap[state]
delete(callbackRedirectMap, state)
callbackRedirectMutex.Unlock()
return redirect
}
+125
View File
@@ -0,0 +1,125 @@
package auth
import (
"log"
"net/http"
"net/url"
"strings"
"time"
)
func IsAuth(r *http.Request) (bool, error) {
jwt, err := getAuthToken(r)
if err != nil {
return false, err
}
_, err = ValidateJWT(jwt)
if err != nil {
return false, err
}
return true, nil
}
func LogoutEndpoint(w http.ResponseWriter, r *http.Request) {
setAuthCookie(w, r, "")
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func AuthMiddleware(next http.HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
jwt, err := getAuthToken(r)
if err != nil {
log.Println("Error during token retrieval", err)
Redirect(w, r)
return
}
user, err := ValidateJWT(jwt)
if err != nil {
log.Println("Error during authorization", err)
Redirect(w, r)
return
}
log.Printf("%s: Authorized user %s", r.URL.Path, user)
w.Header().Add("user", user)
next.ServeHTTP(w, r)
})
}
func CallbackHandler(mux *http.ServeMux) {
url, err := url.Parse(config.RedirectURL)
if err != nil {
log.Fatalf("Invalid Redirect URL Submitted: %s\n", config.RedirectURL)
}
mux.HandleFunc(url.Path, func(w http.ResponseWriter, r *http.Request) {
if err := CheckState(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
code := r.URL.Query().Get("code")
idtoken, err := GetToken(code)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := CheckAudience(idtoken); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := CheckNonce(r, idtoken); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
jwt, err := NewJWT(idtoken.Subject)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
setAuthCookie(w, r, jwt)
redirect := GetRedirect(r.URL.Query().Get("state"))
log.Printf("DEBUG: REDIRECT %s\n", redirect)
http.Redirect(w, r, redirect, http.StatusFound)
})
}
func setAuthCookie(w http.ResponseWriter, r *http.Request, jwt string) {
c := &http.Cookie{
Name: authCookie,
Value: jwt,
MaxAge: int(time.Hour.Seconds()),
Secure: r.TLS != nil,
Path: "/",
HttpOnly: true,
}
http.SetCookie(w, c)
}
func getAuthToken(r *http.Request) (string, error) {
header := strings.Split(r.Header.Get("Authorization"), " ")
if len(header) > 1 {
token := header[1]
if len(strings.TrimSpace(token)) > 0 {
log.Printf("Got Auth Header %s\n", header[1])
return token, nil
}
}
cookie, err := r.Cookie(authCookie)
if err != nil {
return "", err
}
return cookie.Value, nil
}
+45
View File
@@ -0,0 +1,45 @@
package internal
import (
"sync"
"time"
)
type CacheItem[T any] struct {
item T
imutex sync.Mutex
expiry time.Time
TTLSeconds uint
FetchFunc func() (T, error)
}
func (c *CacheItem[T]) Get() (T, error) {
c.imutex.Lock()
defer c.imutex.Unlock()
if !c.expiry.After(time.Now()) {
c.expiry = time.Now().Add(time.Duration(c.TTLSeconds) * time.Second)
item, err := c.FetchFunc()
if err != nil {
return item, err
}
c.item = item
}
return c.item, nil
}
func (c *CacheItem[T]) Invalidate() {
c.imutex.Lock()
defer c.imutex.Unlock()
c.expiry = time.Now()
}
func NewCache[T any](fetchfunc func() (T, error), ttl uint) CacheItem[T] {
return CacheItem[T]{
TTLSeconds: ttl,
FetchFunc: fetchfunc,
}
}
+45
View File
@@ -0,0 +1,45 @@
package database
import (
"database/sql"
"log"
"time"
"github.com/lib/pq"
)
var db *sql.DB
func OpenDB(conf pq.Config) {
connector, err := pq.NewConnectorConfig(conf)
if err != nil {
panic(err)
}
db = sql.OpenDB(connector)
db.SetMaxOpenConns(500)
db.SetMaxIdleConns(50)
db.SetConnMaxLifetime(time.Hour)
if err := db.Ping(); err != nil {
panic(err)
}
log.Printf("Database opened to %s:%d, %s as %s!\n", conf.Host, conf.Port, conf.Database, conf.User)
if err := initStateDB(); err != nil {
panic(err)
}
if err := initVoteDB(); err != nil {
panic(err)
}
}
func CloseDB() {
if err := db.Close(); err != nil {
panic(err)
}
}
+87
View File
@@ -0,0 +1,87 @@
package database
import (
"strconv"
"tomatentum.net/outfit-voting-abi26/internal"
)
const INITSTATEDB string = "CREATE TABLE IF NOT EXISTS state(key varchar(255) PRIMARY KEY, value TEXT)"
const STATEINSERT string = "INSERT INTO state(key, value) VALUES ($1, $2) ON CONFLICT (key) DO NOTHING;"
const STATESET string = "UPDATE state SET value = $1 WHERE key = $2"
const STATEGET string = "SELECT value FROM state WHERE key = $1"
const KEYVOTELOCKED string = "votinglocked"
const KEYRESULTLOCKED string = "resultlocked"
var voteLocked internal.CacheItem[bool] = internal.NewCache(func() (bool, error) {
res := db.QueryRow(STATEGET, KEYVOTELOCKED)
var valuestring string
if err := res.Scan(&valuestring); err != nil {
return false, err
}
value, err := strconv.ParseBool(valuestring)
if err != nil {
return false, err
}
return value, nil
}, 30)
var resultLocked internal.CacheItem[bool] = internal.NewCache(func() (bool, error) {
res := db.QueryRow(STATEGET, KEYRESULTLOCKED)
var valuestring string
if err := res.Scan(&valuestring); err != nil {
return false, err
}
value, err := strconv.ParseBool(valuestring)
if err != nil {
return false, err
}
return value, nil
}, 30)
func initStateDB() error {
_, err := db.Exec(INITSTATEDB)
if err != nil {
return err
}
_, err = db.Exec(STATEINSERT, KEYVOTELOCKED, "true")
_, err = db.Exec(STATEINSERT, KEYRESULTLOCKED, "true")
if err != nil {
return err
}
return nil
}
func setState(key string, value string) error {
_, err := db.Exec(STATESET, value, key)
return err
}
func GetVoteLocked() (bool, error) {
return voteLocked.Get()
}
func GetResultLocked() (bool, error) {
return resultLocked.Get()
}
func SetVoteLocked(value bool) error {
voteLocked.Invalidate()
return setState(KEYVOTELOCKED, strconv.FormatBool(value))
}
func SetResultLocked(value bool) error {
resultLocked.Invalidate()
return setState(KEYRESULTLOCKED, strconv.FormatBool(value))
}
+201
View File
@@ -0,0 +1,201 @@
package database
import (
"errors"
"fmt"
"strings"
"tomatentum.net/outfit-voting-abi26/internal/util"
)
const VALUESTEMPLATE string = "(%s, %s, %s, %d)"
const INITVOTEDB string = "CREATE TABLE IF NOT EXISTS vote(id varchar(16) PRIMARY KEY, category TEXT NOT NULL, name TEXT NOT NULL, votes int NOT NULL)"
const INSERTVOTEENTRY string = "INSERT INTO vote(id, category, name, votes) VALUES($1, $2, $3, $4)"
const BULKINSERTVOTEENTRY string = "INSERT INTO vote(id, name, votes) VALUES"
const SETVOTE string = "UPDATE vote SET votes = $1 WHERE id = $2"
const INCVOTE string = "UPDATE vote SET votes = votes + 1 WHERE id = $1"
const DELVOTEENTRY string = "DELETE FROM vote WHERE id = $1"
const BULKDELVOTEENTRY string = "DELETE FROM vote WHERE key IN (%s)"
const GETVOTES string = "SELECT * FROM vote ORDER BY votes DESC"
const GETVOTEOPTIONS string = "SELECT id, name FROM vote"
const GETVOTE string = "SELECT * FROM vote WHERE id = $1"
type VoteEntry struct {
VoteOption
Votes int
}
type VoteOption struct {
ID string
Category string
Name string
}
func initVoteDB() error {
_, err := db.Exec(INITVOTEDB)
return err
}
func InsertVoteEntry(name string, category string) (VoteEntry, error) {
id, err := util.RandString(8)
if err != nil {
return VoteEntry{}, err
}
_, err = db.Exec(INSERTVOTEENTRY, id, name, 0)
if err != nil {
return VoteEntry{}, err
}
return VoteEntry{VoteOption{id, category, name}, 0}, nil
}
func BulkInsertVoteEntry(names []string, category string) (*[]VoteEntry, error) {
stmnt := BULKINSERTVOTEENTRY
var entries []VoteEntry = make([]VoteEntry, len(names))
for i, v := range names {
id, err := util.RandString(8)
if err != nil {
return nil, err
}
stmnt = stmnt + fmt.Sprintf(VALUESTEMPLATE, id, category, v, 0)
entries = append(entries, VoteEntry{VoteOption{id, category, v}, 0})
if i < len(names)-1 {
stmnt = stmnt + ","
}
}
stmnt = stmnt + ";"
_, err := db.Exec(stmnt)
if err != nil {
return nil, err
}
return &entries, nil
}
func SetVote(id string, votes int) error {
_, err := db.Exec(SETVOTE, votes, id)
if err != nil {
return err
}
return nil
}
func IncVote(id string) error {
res, err := db.Exec(INCVOTE, id)
if err != nil {
return err
}
n, _ := res.RowsAffected()
if n < 1 {
return errors.New("id does not exist")
}
return nil
}
func DelVoteEntry(id string) error {
res, err := db.Exec(DELVOTEENTRY, id)
if err != nil {
return err
}
n, _ := res.RowsAffected()
if n < 1 {
return errors.New("id does not exist")
}
return nil
}
func BulkDelVoteEntry(ids []string) error {
placeholders := make([]string, len(ids))
for i := 0; i < len(ids); i++ {
placeholders = append(placeholders, fmt.Sprintf("$%d", i))
}
res, err := db.Exec(fmt.Sprintf(BULKDELVOTEENTRY, strings.Join(placeholders, ", ")), ids)
if err != nil {
return err
}
n, _ := res.RowsAffected()
if n < 1 {
return errors.New("id does not exist")
}
return nil
}
func GetVotes() ([]VoteEntry, error) {
res, err := db.Query(GETVOTES)
if err != nil {
return nil, err
}
defer res.Close()
var current VoteEntry
var entries []VoteEntry = []VoteEntry{}
for res.Next() {
if err := res.Err(); err != nil {
return nil, err
}
if err := res.Scan(&current.ID, &current.Category, &current.Name, &current.Votes); err != nil {
return nil, err
}
entries = append(entries, current)
}
return entries, nil
}
func GetVoteOptions() ([]VoteOption, error) {
res, err := db.Query(GETVOTEOPTIONS)
if err != nil {
return nil, err
}
defer res.Close()
var current VoteOption
var entries []VoteOption = []VoteOption{}
for res.Next() {
if err := res.Err(); err != nil {
return nil, err
}
if err := res.Scan(&current.ID, &current.Category, &current.Name); err != nil {
return nil, err
}
entries = append(entries, current)
}
return entries, nil
}
func GetVote(id string) (VoteEntry, error) {
res := db.QueryRow(GETVOTE, id)
var current VoteEntry
if err := res.Scan(&current.ID, &current.Category, &current.Name, &current.Votes); err != nil {
return VoteEntry{}, err
}
return current, nil
}
+46
View File
@@ -0,0 +1,46 @@
package http
import (
"net/http"
"tomatentum.net/outfit-voting-abi26/internal/auth"
"tomatentum.net/outfit-voting-abi26/internal/http/vote"
)
var mux *http.ServeMux = http.NewServeMux()
func Start() error {
mux.Handle("/", auth.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
mux.HandleFunc("/api/auth/logout", auth.LogoutEndpoint)
mux.HandleFunc("GET /api/vote/locked", vote.VoteLockedEndpoint)
mux.HandleFunc("GET /api/result/locked", vote.ResultLockedEndpoint)
mux.Handle("PATCH /api/vote/locked", auth.AuthMiddleware(vote.SetVoteLockedEndpoint))
mux.Handle("PATCH /api/result/locked", auth.AuthMiddleware(vote.SetResultLockedEndpoint))
mux.Handle("GET /api/vote", vote.VoteLockMiddleware(vote.GetVoteOptionsEndpoint))
mux.Handle("GET /api/result", vote.ResultLockMiddleware(vote.GetVoteResultEndpoint))
mux.Handle("GET /api/result/{id}", vote.ResultLockMiddleware(vote.GetVoteResultSingleEndoint))
mux.Handle("PATCH /api/vote/{id}/override", auth.AuthMiddleware(vote.SetVoteEndpoint))
mux.Handle("PATCH /api/vote/{id}/inc", vote.VoteLockMiddleware(vote.IncVoteEndpoint))
mux.Handle("DELETE /api/vote/{id}", auth.AuthMiddleware(vote.DelVoteEntryEndpoint))
mux.Handle("POST /api/vote", auth.AuthMiddleware(vote.AddVoteEntryEndpoint))
auth.CallbackHandler(mux)
return http.ListenAndServe(":4000", corsMiddleware(mux))
}
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
+33
View File
@@ -0,0 +1,33 @@
package vote
import (
"encoding/json"
"net/http"
"tomatentum.net/outfit-voting-abi26/internal/database"
)
func GetVoteResultEndpoint(w http.ResponseWriter, r *http.Request) {
entries, err := database.GetVotes()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(entries)
}
func GetVoteResultSingleEndoint(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
entry, err := database.GetVote(id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(entry)
}
+127
View File
@@ -0,0 +1,127 @@
package vote
import (
"encoding/json"
"net/http"
"tomatentum.net/outfit-voting-abi26/internal/database"
)
type SetVoteRequest struct {
Votes int
}
type AddVoteEntryRequest struct {
Name string
Category string
}
type BulkAddVoteEntryRequest struct {
Category string
names []string
}
func GetVoteOptionsEndpoint(w http.ResponseWriter, r *http.Request) {
entries, err := database.GetVoteOptions()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(entries)
}
func SetVoteEndpoint(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
var request SetVoteRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
r.Body.Close()
if err := database.SetVote(id, request.Votes); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func IncVoteEndpoint(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if err := database.IncVote(id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func DelVoteEntryEndpoint(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if err := database.DelVoteEntry(id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func BulkDelVoteEntryEndpoint(w http.ResponseWriter, r *http.Request) {
var ids []string
if err := json.NewDecoder(r.Body).Decode(&ids); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
r.Body.Close()
if err := database.BulkDelVoteEntry(ids); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func AddVoteEntryEndpoint(w http.ResponseWriter, r *http.Request) {
var request AddVoteEntryRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
r.Body.Close()
entry, err := database.InsertVoteEntry(request.Name, request.Category)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(entry)
}
func BulkVoteEntryEndpoint(w http.ResponseWriter, r *http.Request) {
var request BulkAddVoteEntryRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
r.Body.Close()
entry, err := database.BulkInsertVoteEntry(request.names, request.Category)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(entry)
}
+124
View File
@@ -0,0 +1,124 @@
package vote
import (
"encoding/json"
"log"
"net/http"
"tomatentum.net/outfit-voting-abi26/internal/auth"
"tomatentum.net/outfit-voting-abi26/internal/database"
)
type StateChangeRequest struct {
Value bool
}
func SetVoteLockedEndpoint(w http.ResponseWriter, r *http.Request) {
var request StateChangeRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
r.Body.Close()
if err := database.SetVoteLocked(request.Value); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Printf("Updated Vote Lock state to %t\n", request.Value)
w.WriteHeader(http.StatusOK)
}
func SetResultLockedEndpoint(w http.ResponseWriter, r *http.Request) {
var request StateChangeRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
r.Body.Close()
if err := database.SetResultLocked(request.Value); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Printf("Updated Result Lock state to %t\n", request.Value)
w.WriteHeader(http.StatusOK)
}
func VoteLockedEndpoint(w http.ResponseWriter, r *http.Request) {
locked, err := database.GetVoteLocked()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(locked)
}
func ResultLockedEndpoint(w http.ResponseWriter, r *http.Request) {
locked, err := database.GetResultLocked()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(locked)
}
func VoteLockMiddleware(next http.HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ok, err := auth.IsAuth(r)
if ok {
next.ServeHTTP(w, r)
return
}
log.Println("DEBUG: User not authenticated so vote lock is not bypassed", err)
locked, err := database.GetVoteLocked()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if locked {
w.WriteHeader(http.StatusLocked)
w.Write([]byte("Vote currently locked!"))
return
}
next.ServeHTTP(w, r)
})
}
func ResultLockMiddleware(next http.HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ok, err := auth.IsAuth(r)
if ok {
next.ServeHTTP(w, r)
return
}
log.Println("DEBUG: User not authenticated so result lock is not bypassed", err)
locked, err := database.GetResultLocked()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if locked {
w.WriteHeader(http.StatusLocked)
w.Write([]byte("Results currently locked!"))
return
}
next.ServeHTTP(w, r)
})
}
+15
View File
@@ -0,0 +1,15 @@
package util
import (
"crypto/rand"
"encoding/base64"
"io"
)
func RandString(nByte int) (string, error) {
b := make([]byte, nByte)
if _, err := io.ReadFull(rand.Reader, b); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}