@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package database
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
@@ -17,11 +18,24 @@ func OpenDB(conf pq.Config) {
|
||||
}
|
||||
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() {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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(¤t.ID, ¤t.Category, ¤t.Name, ¤t.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(¤t.ID, ¤t.Category, ¤t.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(¤t.ID, ¤t.Category, ¤t.Name, ¤t.Votes); err != nil {
|
||||
return VoteEntry{}, err
|
||||
}
|
||||
|
||||
return current, nil
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user