Author SHA1 Message Date
Flashfyre 2547ca173a Fix voucher compensation logic 2024-04-10 16:45:01 -04:00
Flashfyre 992864b785 Add voucher compensation logic 2024-04-09 22:22:00 -04:00
maru 0f8fc0085e Change certain struct field names back to what they were 2024-04-09 01:28:56 -04:00
maru 85055d2921 Fix variable re-use bug 2024-04-08 21:34:42 -04:00
maru d97e07b522 Fix condition to read save data from request body 2024-04-08 20:50:47 -04:00
maru 977a05049e Add missing return 2024-04-08 20:47:23 -04:00
maru 5778675171 Major refactor of API 2024-04-08 20:44:36 -04:00
maru 0edfeab3ca Various styling changes 2024-04-08 18:15:09 -04:00
9 changed files with 431 additions and 422 deletions

No files matched your search

+2 -2
View File
@@ -30,7 +30,7 @@ func getUsernameFromRequest(r *http.Request) (string, error) {
return username, nil return username, nil
} }
func getUuidFromRequest(r *http.Request) ([]byte, error) { func getUUIDFromRequest(r *http.Request) ([]byte, error) {
if r.Header.Get("Authorization") == "" { if r.Header.Get("Authorization") == "" {
return nil, fmt.Errorf("missing token") return nil, fmt.Errorf("missing token")
} }
@@ -44,7 +44,7 @@ func getUuidFromRequest(r *http.Request) ([]byte, error) {
return nil, fmt.Errorf("invalid token length: got %d, expected 32", len(token)) return nil, fmt.Errorf("invalid token length: got %d, expected 32", len(token))
} }
uuid, err := db.FetchUuidFromToken(token) uuid, err := db.FetchUUIDFromToken(token)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to validate token: %s", err) return nil, fmt.Errorf("failed to validate token: %s", err)
} }
+50 -109
View File
@@ -5,9 +5,7 @@ import (
"crypto/rand" "crypto/rand"
"database/sql" "database/sql"
"encoding/base64" "encoding/base64"
"encoding/json"
"fmt" "fmt"
"net/http"
"os" "os"
"regexp" "regexp"
"strconv" "strconv"
@@ -18,10 +16,12 @@ import (
) )
const ( const (
argonTime = 1 UUIDSize = 16
argonMemory = 256 * 1024 ArgonTime = 1
argonThreads = 4 ArgonMemory = 256 * 1024
argonKeyLength = 32 ArgonThreads = 4
ArgonKeySize = 32
ArgonSaltSize = 16
) )
var isValidUsername = regexp.MustCompile(`^\w{1,16}$`).MatchString var isValidUsername = regexp.MustCompile(`^\w{1,16}$`).MatchString
@@ -32,21 +32,9 @@ type AccountInfoResponse struct {
} }
// /account/info - get account info // /account/info - get account info
func (s *Server) handleAccountInfo(w http.ResponseWriter, r *http.Request) { func handleAccountInfo(username string, uuid []byte) (AccountInfoResponse, error) {
username, err := getUsernameFromRequest(r) var latestSave time.Time
if err != nil { latestSaveID := -1
httpError(w, r, err.Error(), http.StatusBadRequest)
return
}
uuid, err := getUuidFromRequest(r) // lazy
if err != nil {
httpError(w, r, err.Error(), http.StatusBadRequest)
return
}
var latestSaveTime time.Time
latestSaveId := -1
for id := range sessionSlotCount { for id := range sessionSlotCount {
fileName := "session" fileName := "session"
if id != 0 { if id != 0 {
@@ -58,148 +46,101 @@ func (s *Server) handleAccountInfo(w http.ResponseWriter, r *http.Request) {
continue continue
} }
if stat.ModTime().After(latestSaveTime) { if stat.ModTime().After(latestSave) {
latestSaveTime = stat.ModTime() latestSave = stat.ModTime()
latestSaveId = id latestSaveID = id
} }
} }
response, err := json.Marshal(AccountInfoResponse{Username: username, LastSessionSlot: latestSaveId}) return AccountInfoResponse{Username: username, LastSessionSlot: latestSaveID}, nil
if err != nil {
httpError(w, r, fmt.Sprintf("failed to marshal response json: %s", err), http.StatusInternalServerError)
return
}
w.Write(response)
} }
type AccountRegisterRequest GenericAuthRequest type AccountRegisterRequest GenericAuthRequest
// /account/register - register account // /account/register - register account
func (s *Server) handleAccountRegister(w http.ResponseWriter, r *http.Request) { func handleAccountRegister(username, password string) error {
var request AccountRegisterRequest if !isValidUsername(username) {
err := json.NewDecoder(r.Body).Decode(&request) return fmt.Errorf("invalid username")
}
if len(password) < 6 {
return fmt.Errorf("invalid password")
}
uuid := make([]byte, UUIDSize)
_, err := rand.Read(uuid)
if err != nil { if err != nil {
httpError(w, r, fmt.Sprintf("failed to decode request body: %s", err), http.StatusBadRequest) return fmt.Errorf("failed to generate uuid: %s", err)
return
} }
if !isValidUsername(request.Username) { salt := make([]byte, ArgonSaltSize)
httpError(w, r, "invalid username", http.StatusBadRequest)
return
}
if len(request.Password) < 6 {
httpError(w, r, "invalid password", http.StatusBadRequest)
return
}
uuid := make([]byte, 16)
_, err = rand.Read(uuid)
if err != nil {
httpError(w, r, fmt.Sprintf("failed to generate uuid: %s", err), http.StatusInternalServerError)
return
}
salt := make([]byte, 16)
_, err = rand.Read(salt) _, err = rand.Read(salt)
if err != nil { if err != nil {
httpError(w, r, fmt.Sprintf("failed to generate salt: %s", err), http.StatusInternalServerError) return fmt.Errorf(fmt.Sprintf("failed to generate salt: %s", err))
return
} }
err = db.AddAccountRecord(uuid, request.Username, argon2.IDKey([]byte(request.Password), salt, argonTime, argonMemory, argonThreads, argonKeyLength), salt) err = db.AddAccountRecord(uuid, username, argon2.IDKey([]byte(password), salt, ArgonTime, ArgonMemory, ArgonThreads, ArgonKeySize), salt)
if err != nil { if err != nil {
httpError(w, r, fmt.Sprintf("failed to add account record: %s", err), http.StatusInternalServerError) return fmt.Errorf("failed to add account record: %s", err)
return
} }
w.WriteHeader(http.StatusOK) return nil
} }
type AccountLoginRequest GenericAuthRequest type AccountLoginRequest GenericAuthRequest
type AccountLoginResponse GenericAuthResponse type AccountLoginResponse GenericAuthResponse
// /account/login - log into account // /account/login - log into account
func (s *Server) handleAccountLogin(w http.ResponseWriter, r *http.Request) { func handleAccountLogin(username, password string) (AccountLoginResponse, error) {
var request AccountLoginRequest if !isValidUsername(username) {
err := json.NewDecoder(r.Body).Decode(&request) return AccountLoginResponse{}, fmt.Errorf("invalid username")
if err != nil {
httpError(w, r, fmt.Sprintf("failed to decode request body: %s", err), http.StatusBadRequest)
return
} }
if !isValidUsername(request.Username) { if len(password) < 6 {
httpError(w, r, "invalid username", http.StatusBadRequest) return AccountLoginResponse{}, fmt.Errorf("invalid password")
return
} }
if len(request.Password) < 6 { key, salt, err := db.FetchAccountKeySaltFromUsername(username)
httpError(w, r, "invalid password", http.StatusBadRequest)
return
}
key, salt, err := db.FetchAccountKeySaltFromUsername(request.Username)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
httpError(w, r, "account doesn't exist", http.StatusBadRequest) return AccountLoginResponse{}, fmt.Errorf("account doesn't exist")
return
} }
httpError(w, r, err.Error(), http.StatusInternalServerError) return AccountLoginResponse{}, err
return
} }
if !bytes.Equal(key, argon2.IDKey([]byte(request.Password), salt, argonTime, argonMemory, argonThreads, argonKeyLength)) { if !bytes.Equal(key, argon2.IDKey([]byte(password), salt, ArgonTime, ArgonMemory, ArgonThreads, ArgonKeySize)) {
httpError(w, r, "password doesn't match", http.StatusBadRequest) return AccountLoginResponse{}, fmt.Errorf("password doesn't match")
return
} }
token := make([]byte, 32) token := make([]byte, 32)
_, err = rand.Read(token) _, err = rand.Read(token)
if err != nil { if err != nil {
httpError(w, r, fmt.Sprintf("failed to generate token: %s", err), http.StatusInternalServerError) return AccountLoginResponse{}, fmt.Errorf("failed to generate token: %s", err)
return
} }
err = db.AddAccountSession(request.Username, token) err = db.AddAccountSession(username, token)
if err != nil { if err != nil {
httpError(w, r, "failed to add account session", http.StatusInternalServerError) return AccountLoginResponse{}, fmt.Errorf("failed to add account session")
return
} }
response, err := json.Marshal(AccountLoginResponse{Token: base64.StdEncoding.EncodeToString(token)}) return AccountLoginResponse{Token: base64.StdEncoding.EncodeToString(token)}, nil
if err != nil {
httpError(w, r, fmt.Sprintf("failed to marshal response json: %s", err), http.StatusInternalServerError)
return
}
w.Write(response)
} }
// /account/logout - log out of account // /account/logout - log out of account
func (s *Server) handleAccountLogout(w http.ResponseWriter, r *http.Request) { func handleAccountLogout(token []byte) error {
token, err := base64.StdEncoding.DecodeString(r.Header.Get("Authorization"))
if err != nil {
httpError(w, r, fmt.Sprintf("failed to decode token: %s", err), http.StatusBadRequest)
return
}
if len(token) != 32 { if len(token) != 32 {
httpError(w, r, "invalid token", http.StatusBadRequest) return fmt.Errorf("invalid token")
return
} }
err = db.RemoveSessionFromToken(token) err := db.RemoveSessionFromToken(token)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
httpError(w, r, "token not found", http.StatusBadRequest) return fmt.Errorf("token not found")
return
} }
httpError(w, r, "failed to remove account session", http.StatusInternalServerError) return fmt.Errorf("failed to remove account session")
return
} }
w.WriteHeader(http.StatusOK) return nil
} }
+20 -65
View File
@@ -5,15 +5,13 @@ import (
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"encoding/binary" "encoding/binary"
"encoding/json"
"fmt" "fmt"
"log" "log"
"net/http"
"os" "os"
"strconv"
"time" "time"
"github.com/Flashfyre/pokerogue-server/db" "github.com/Flashfyre/pokerogue-server/db"
"github.com/Flashfyre/pokerogue-server/defs"
"github.com/go-co-op/gocron" "github.com/go-co-op/gocron"
) )
@@ -26,26 +24,33 @@ var (
) )
func ScheduleDailyRunRefresh() { func ScheduleDailyRunRefresh() {
dailyRunScheduler.Every(1).Day().At("00:00").Do(InitDailyRun) dailyRunScheduler.Every(1).Day().At("00:00").Do(func() error {
err := InitDailyRun()
if err != nil {
log.Fatal(err)
}
return nil
}())
dailyRunScheduler.StartAsync() dailyRunScheduler.StartAsync()
} }
func InitDailyRun() { func InitDailyRun() error {
secret, err := os.ReadFile("secret.key") secret, err := os.ReadFile("secret.key")
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
log.Fatalf("failed to read daily seed secret: %s", err) return fmt.Errorf("failed to read daily seed secret: %s", err)
} }
newSecret := make([]byte, 32) newSecret := make([]byte, 32)
_, err := rand.Read(newSecret) _, err := rand.Read(newSecret)
if err != nil { if err != nil {
log.Fatalf("failed to generate daily seed secret: %s", err) return fmt.Errorf("failed to generate daily seed secret: %s", err)
} }
err = os.WriteFile("secret.key", newSecret, 0400) err = os.WriteFile("secret.key", newSecret, 0400)
if err != nil { if err != nil {
log.Fatalf("failed to write daily seed secret: %s", err) return fmt.Errorf("failed to write daily seed secret: %s", err)
} }
secret = newSecret secret = newSecret
@@ -61,6 +66,8 @@ func InitDailyRun() {
} }
log.Printf("Daily Run Seed: %s", dailyRunSeed) log.Printf("Daily Run Seed: %s", dailyRunSeed)
return nil
} }
func deriveDailyRunSeed(seedTime time.Time) []byte { func deriveDailyRunSeed(seedTime time.Time) []byte {
@@ -72,79 +79,27 @@ func deriveDailyRunSeed(seedTime time.Time) []byte {
return hashedSeed[:] return hashedSeed[:]
} }
// /daily/seed - fetch daily run seed
func (s *Server) handleSeed(w http.ResponseWriter) {
w.Write([]byte(dailyRunSeed))
}
// /daily/rankings - fetch daily rankings // /daily/rankings - fetch daily rankings
func (s *Server) handleRankings(w http.ResponseWriter, r *http.Request) { func handleRankings(uuid []byte, category, page int) ([]defs.DailyRanking, error) {
uuid, err := getUuidFromRequest(r) err := db.UpdateAccountLastActivity(uuid)
if err != nil {
httpError(w, r, err.Error(), http.StatusBadRequest)
return
}
err = db.UpdateAccountLastActivity(uuid)
if err != nil { if err != nil {
log.Print("failed to update account last activity") log.Print("failed to update account last activity")
} }
var category int
if r.URL.Query().Has("category") {
category, err = strconv.Atoi(r.URL.Query().Get("category"))
if err != nil {
httpError(w, r, fmt.Sprintf("failed to convert category: %s", err), http.StatusBadRequest)
return
}
}
page := 1
if r.URL.Query().Has("page") {
page, err = strconv.Atoi(r.URL.Query().Get("page"))
if err != nil {
httpError(w, r, fmt.Sprintf("failed to convert page: %s", err), http.StatusBadRequest)
return
}
}
rankings, err := db.FetchRankings(category, page) rankings, err := db.FetchRankings(category, page)
if err != nil { if err != nil {
log.Print("failed to retrieve rankings") log.Print("failed to retrieve rankings")
} }
response, err := json.Marshal(rankings) return rankings, nil
if err != nil {
httpError(w, r, fmt.Sprintf("failed to marshal response json: %s", err), http.StatusInternalServerError)
return
}
w.Write(response)
} }
// /daily/rankingpagecount - fetch daily ranking page count // /daily/rankingpagecount - fetch daily ranking page count
func (s *Server) handleRankingPageCount(w http.ResponseWriter, r *http.Request) { func handleRankingPageCount(category int) (int, error) {
var err error
var category int
if r.URL.Query().Has("category") {
category, err = strconv.Atoi(r.URL.Query().Get("category"))
if err != nil {
httpError(w, r, fmt.Sprintf("failed to convert category: %s", err), http.StatusBadRequest)
return
}
}
pageCount, err := db.FetchRankingPageCount(category) pageCount, err := db.FetchRankingPageCount(category)
if err != nil { if err != nil {
log.Print("failed to retrieve ranking page count") log.Print("failed to retrieve ranking page count")
} }
response, err := json.Marshal(pageCount) return pageCount, nil
if err != nil {
httpError(w, r, fmt.Sprintf("failed to marshal response json: %s", err), http.StatusInternalServerError)
return
}
w.Write(response)
} }
+3 -3
View File
@@ -5,7 +5,7 @@ import (
"net/http" "net/http"
) )
func httpError(w http.ResponseWriter, r *http.Request, error string, code int) { func httpError(w http.ResponseWriter, r *http.Request, err error, code int) {
log.Printf("%s: %s\n", r.URL.Path, error) log.Printf("%s: %s\n", r.URL.Path, err)
http.Error(w, error, code) http.Error(w, err.Error(), code)
} }
+2 -41
View File
@@ -1,14 +1,10 @@
package api package api
import ( import (
"encoding/json"
"fmt"
"log" "log"
"net/http"
"time" "time"
"github.com/Flashfyre/pokerogue-server/db" "github.com/Flashfyre/pokerogue-server/db"
"github.com/Flashfyre/pokerogue-server/defs"
"github.com/go-co-op/gocron" "github.com/go-co-op/gocron"
) )
@@ -30,49 +26,14 @@ func updateStats() {
if err != nil { if err != nil {
log.Print(err) log.Print(err)
} }
battleCount, err = db.FetchBattleCount() battleCount, err = db.FetchBattleCount()
if err != nil { if err != nil {
log.Print(err) log.Print(err)
} }
classicSessionCount, err = db.FetchClassicSessionCount() classicSessionCount, err = db.FetchClassicSessionCount()
if err != nil { if err != nil {
log.Print(err) log.Print(err)
} }
} }
// /game/playercount - get player count
func (s *Server) handlePlayerCountGet(w http.ResponseWriter, r *http.Request) {
response, err := json.Marshal(playerCount)
if err != nil {
httpError(w, r, fmt.Sprintf("failed to marshal response json: %s", err), http.StatusInternalServerError)
return
}
w.Write(response)
}
// /game/titlestats - get title stats
func (s *Server) handleTitleStatsGet(w http.ResponseWriter, r *http.Request) {
titleStats := &defs.TitleStats{
PlayerCount: playerCount,
BattleCount: battleCount,
}
response, err := json.Marshal(titleStats)
if err != nil {
httpError(w, r, fmt.Sprintf("failed to marshal response json: %s", err), http.StatusInternalServerError)
return
}
w.Write(response)
}
// /game/classicsessioncount - get classic session count
func (s *Server) handleClassicSessionCountGet(w http.ResponseWriter, r *http.Request) {
response, err := json.Marshal(classicSessionCount)
if err != nil {
httpError(w, r, fmt.Sprintf("failed to marshal response json: %s", err), http.StatusInternalServerError)
return
}
w.Write(response)
}
+232 -18
View File
@@ -1,14 +1,26 @@
package api package api
import ( import (
"encoding/base64"
"encoding/gob" "encoding/gob"
"encoding/json"
"fmt"
"net/http" "net/http"
"strconv"
"github.com/Flashfyre/pokerogue-server/defs"
) )
type Server struct { type Server struct {
Debug bool Debug bool
} }
/*
The caller of endpoint handler functions are responsible for extracting the necessary data from the request.
Handler functions are responsible for checking the validity of this data and returning a result or error.
Handlers should not return serialized JSON, instead return the struct itself.
*/
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
gob.Register([]interface{}{}) gob.Register([]interface{}{})
gob.Register(map[string]interface{}{}) gob.Register(map[string]interface{}{})
@@ -25,37 +37,239 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
switch r.URL.Path { switch r.URL.Path {
// /account
case "/account/info": case "/account/info":
s.handleAccountInfo(w, r) username, err := getUsernameFromRequest(r)
if err != nil {
httpError(w, r, err, http.StatusBadRequest)
return
}
uuid, err := getUUIDFromRequest(r) // lazy
if err != nil {
httpError(w, r, err, http.StatusBadRequest)
return
}
info, err := handleAccountInfo(username, uuid)
if err != nil {
httpError(w, r, err, http.StatusInternalServerError)
return
}
response, err := json.Marshal(info)
if err != nil {
httpError(w, r, fmt.Errorf("failed to marshal response json: %s", err), http.StatusInternalServerError)
return
}
w.Write(response)
case "/account/register": case "/account/register":
s.handleAccountRegister(w, r) var request AccountRegisterRequest
err := json.NewDecoder(r.Body).Decode(&request)
if err != nil {
httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest)
return
}
err = handleAccountRegister(request.Username, request.Password)
if err != nil {
httpError(w, r, err, http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
case "/account/login": case "/account/login":
s.handleAccountLogin(w, r) var request AccountLoginRequest
err := json.NewDecoder(r.Body).Decode(&request)
if err != nil {
httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest)
return
}
token, err := handleAccountLogin(request.Username, request.Password)
if err != nil {
httpError(w, r, err, http.StatusInternalServerError)
return
}
response, err := json.Marshal(token)
if err != nil {
httpError(w, r, fmt.Errorf("failed to marshal response json: %s", err), http.StatusInternalServerError)
return
}
w.Write(response)
case "/account/logout": case "/account/logout":
s.handleAccountLogout(w, r) token, err := base64.StdEncoding.DecodeString(r.Header.Get("Authorization"))
if err != nil {
httpError(w, r, fmt.Errorf("failed to decode token: %s", err), http.StatusBadRequest)
return
}
err = handleAccountLogout(token)
if err != nil {
httpError(w, r, err, http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
// /game
case "/game/playercount": case "/game/playercount":
s.handlePlayerCountGet(w, r) w.Write([]byte(strconv.Itoa(playerCount)))
case "/game/titlestats": case "/game/titlestats":
s.handleTitleStatsGet(w, r) response, err := json.Marshal(&defs.TitleStats{
PlayerCount: playerCount,
BattleCount: battleCount,
})
if err != nil {
httpError(w, r, fmt.Errorf("failed to marshal response json: %s", err), http.StatusInternalServerError)
return
}
w.Write(response)
case "/game/classicsessioncount": case "/game/classicsessioncount":
s.handleClassicSessionCountGet(w, r) w.Write([]byte(strconv.Itoa(classicSessionCount)))
case "/savedata/get": // /savedata
s.handleSavedataGet(w, r) case "/savedata/get", "/savedata/update", "/savedata/delete", "/savedata/clear":
case "/savedata/update": uuid, err := getUUIDFromRequest(r)
s.handleSavedataUpdate(w, r) if err != nil {
case "/savedata/delete": httpError(w, r, err, http.StatusBadRequest)
s.handleSavedataDelete(w, r) return
case "/savedata/clear": }
s.handleSavedataClear(w, r)
datatype := -1
if r.URL.Query().Has("datatype") {
datatype, err = strconv.Atoi(r.URL.Query().Get("datatype"))
if err != nil {
httpError(w, r, err, http.StatusBadRequest)
return
}
}
var slot int
if r.URL.Query().Has("slot") {
slot, err = strconv.Atoi(r.URL.Query().Get("slot"))
if err != nil {
httpError(w, r, err, http.StatusBadRequest)
return
}
}
var save any
// /savedata/get and /savedata/delete specify datatype, but don't expect data in body
if r.URL.Path != "/savedata/get" && r.URL.Path != "/savedata/delete" {
if datatype == 0 {
var system defs.SystemSaveData
err = json.NewDecoder(r.Body).Decode(&system)
if err != nil {
httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest)
return
}
save = system
// /savedata/clear doesn't specify datatype, it is assumed to be 1 (session)
} else if datatype == 1 || r.URL.Path == "/savedata/clear" {
var session defs.SessionSaveData
err = json.NewDecoder(r.Body).Decode(&session)
if err != nil {
httpError(w, r, fmt.Errorf("failed to decode request body: %s", err), http.StatusBadRequest)
return
}
save = session
}
}
switch r.URL.Path {
case "/savedata/get":
save, err = handleSavedataGet(uuid, datatype, slot)
case "/savedata/update":
err = handleSavedataUpdate(uuid, slot, save)
case "/savedata/delete":
err = handleSavedataDelete(uuid, datatype, slot)
case "/savedata/clear":
// doesn't return a save, but it works
save, err = handleSavedataClear(uuid, slot, save.(defs.SessionSaveData))
}
if err != nil {
httpError(w, r, err, http.StatusInternalServerError)
return
}
if save == nil || r.URL.Path == "/savedata/update" {
w.WriteHeader(http.StatusOK)
return
}
response, err := json.Marshal(save)
if err != nil {
httpError(w, r, fmt.Errorf("failed to marshal response json: %s", err), http.StatusInternalServerError)
return
}
w.Write(response)
// /daily
case "/daily/seed": case "/daily/seed":
s.handleSeed(w) w.Write([]byte(dailyRunSeed))
case "/daily/rankings": case "/daily/rankings":
s.handleRankings(w, r) uuid, err := getUUIDFromRequest(r)
if err != nil {
httpError(w, r, err, http.StatusBadRequest)
return
}
var category int
if r.URL.Query().Has("category") {
category, err = strconv.Atoi(r.URL.Query().Get("category"))
if err != nil {
httpError(w, r, fmt.Errorf("failed to convert category: %s", err), http.StatusBadRequest)
return
}
}
page := 1
if r.URL.Query().Has("page") {
page, err = strconv.Atoi(r.URL.Query().Get("page"))
if err != nil {
httpError(w, r, fmt.Errorf("failed to convert page: %s", err), http.StatusBadRequest)
return
}
}
rankings, err := handleRankings(uuid, category, page)
if err != nil {
httpError(w, r, err, http.StatusInternalServerError)
return
}
response, err := json.Marshal(rankings)
if err != nil {
httpError(w, r, fmt.Errorf("failed to marshal response json: %s", err), http.StatusInternalServerError)
return
}
w.Write(response)
case "/daily/rankingpagecount": case "/daily/rankingpagecount":
s.handleRankingPageCount(w, r) var category int
if r.URL.Query().Has("category") {
var err error
category, err = strconv.Atoi(r.URL.Query().Get("category"))
if err != nil {
httpError(w, r, fmt.Errorf("failed to convert category: %s", err), http.StatusBadRequest)
return
}
}
count, err := handleRankingPageCount(category)
if err != nil {
httpError(w, r, err, http.StatusInternalServerError)
}
w.Write([]byte(strconv.Itoa(count)))
} }
} }
+3 -3
View File
@@ -40,12 +40,12 @@ func readSystemSaveData(uuid []byte) (defs.SystemSaveData, error) {
return system, nil return system, nil
} }
func readSessionSaveData(uuid []byte, slotId int) (defs.SessionSaveData, error) { func readSessionSaveData(uuid []byte, slotID int) (defs.SessionSaveData, error) {
var session defs.SessionSaveData var session defs.SessionSaveData
fileName := "session" fileName := "session"
if slotId != 0 { if slotID != 0 {
fileName += strconv.Itoa(slotId) fileName += strconv.Itoa(slotID)
} }
save, err := os.ReadFile(fmt.Sprintf("userdata/%s/%s.pzs", hex.EncodeToString(uuid), fileName)) save, err := os.ReadFile(fmt.Sprintf("userdata/%s/%s.pzs", hex.EncodeToString(uuid), fileName))
+81 -180
View File
@@ -4,10 +4,8 @@ import (
"bytes" "bytes"
"encoding/gob" "encoding/gob"
"encoding/hex" "encoding/hex"
"encoding/json"
"fmt" "fmt"
"log" "log"
"net/http"
"os" "os"
"strconv" "strconv"
@@ -19,228 +17,158 @@ import (
const sessionSlotCount = 3 const sessionSlotCount = 3
// /savedata/get - get save data // /savedata/get - get save data
func (s *Server) handleSavedataGet(w http.ResponseWriter, r *http.Request) { func handleSavedataGet(uuid []byte, datatype, slot int) (any, error) {
uuid, err := getUuidFromRequest(r) switch datatype {
if err != nil { case 0: // System
httpError(w, r, err.Error(), http.StatusBadRequest)
return
}
switch r.URL.Query().Get("datatype") {
case "0": // System
system, err := readSystemSaveData(uuid) system, err := readSystemSaveData(uuid)
if err != nil { if err != nil {
httpError(w, r, err.Error(), http.StatusInternalServerError) return nil, err
return
} }
saveJson, err := json.Marshal(system) compensations, err := db.FetchAndClaimAccountCompensations(uuid)
if err != nil { if err != nil {
httpError(w, r, fmt.Sprintf("failed to marshal save to json: %s", err), http.StatusInternalServerError) return nil, fmt.Errorf("failed to fetch compensations: %s", err)
return
} }
w.Write(saveJson) for k, v := range compensations {
case "1": // Session typeKey := strconv.Itoa(k)
slotId, err := strconv.Atoi(r.URL.Query().Get("slot")) system.VoucherCounts[typeKey] += v
}
return system, nil
case 1: // Session
if slot < 0 || slot >= sessionSlotCount {
return nil, fmt.Errorf("slot id %d out of range", slot)
}
session, err := readSessionSaveData(uuid, slot)
if err != nil { if err != nil {
httpError(w, r, fmt.Sprintf("failed to convert slot id: %s", err), http.StatusBadRequest) return nil, err
return
} }
if slotId < 0 || slotId >= sessionSlotCount { return session, nil
httpError(w, r, fmt.Sprintf("slot id %d out of range", slotId), http.StatusBadRequest)
return
}
session, err := readSessionSaveData(uuid, slotId)
if err != nil {
httpError(w, r, err.Error(), http.StatusInternalServerError)
return
}
saveJson, err := json.Marshal(session)
if err != nil {
httpError(w, r, fmt.Sprintf("failed to marshal save to json: %s", err), http.StatusInternalServerError)
return
}
w.Write(saveJson)
default: default:
httpError(w, r, "invalid data type", http.StatusBadRequest) return nil, fmt.Errorf("invalid data type")
return
} }
} }
// /savedata/update - update save data // /savedata/update - update save data
func (s *Server) handleSavedataUpdate(w http.ResponseWriter, r *http.Request) { func handleSavedataUpdate(uuid []byte, slot int, save any) error {
uuid, err := getUuidFromRequest(r) err := db.UpdateAccountLastActivity(uuid)
if err != nil {
httpError(w, r, err.Error(), http.StatusBadRequest)
return
}
err = db.UpdateAccountLastActivity(uuid)
if err != nil { if err != nil {
log.Print("failed to update account last activity") log.Print("failed to update account last activity")
} }
hexUuid := hex.EncodeToString(uuid) hexUUID := hex.EncodeToString(uuid)
switch r.URL.Query().Get("datatype") { switch save := save.(type) {
case "0": // System case defs.SystemSaveData: // System
var system defs.SystemSaveData if save.TrainerId == 0 && save.SecretId == 0 {
err = json.NewDecoder(r.Body).Decode(&system) return fmt.Errorf("invalid system data")
if err != nil {
httpError(w, r, fmt.Sprintf("failed to decode request body: %s", err), http.StatusBadRequest)
return
} }
if system.TrainerId == 0 && system.SecretId == 0 { err = db.UpdateAccountStats(uuid, save.GameStats)
httpError(w, r, "invalid system data", http.StatusInternalServerError)
return
}
err = db.UpdateAccountStats(uuid, system.GameStats)
if err != nil { if err != nil {
httpError(w, r, fmt.Sprintf("failed to update account stats: %s", err), http.StatusBadRequest) return fmt.Errorf("failed to update account stats: %s", err)
return
} }
var gobBuffer bytes.Buffer var gobBuffer bytes.Buffer
err = gob.NewEncoder(&gobBuffer).Encode(system) err = gob.NewEncoder(&gobBuffer).Encode(save)
if err != nil { if err != nil {
httpError(w, r, fmt.Sprintf("failed to serialize save: %s", err), http.StatusInternalServerError) return fmt.Errorf("failed to serialize save: %s", err)
return
} }
zstdWriter, err := zstd.NewWriter(nil) zstdWriter, err := zstd.NewWriter(nil)
if err != nil { if err != nil {
httpError(w, r, fmt.Sprintf("failed to create zstd writer, %s", err), http.StatusInternalServerError) return fmt.Errorf("failed to create zstd writer, %s", err)
return
} }
compressed := zstdWriter.EncodeAll(gobBuffer.Bytes(), nil) compressed := zstdWriter.EncodeAll(gobBuffer.Bytes(), nil)
err = os.MkdirAll("userdata/"+hexUuid, 0755) err = os.MkdirAll("userdata/"+hexUUID, 0755)
if err != nil && !os.IsExist(err) { if err != nil && !os.IsExist(err) {
httpError(w, r, fmt.Sprintf("failed to create userdata folder: %s", err), http.StatusInternalServerError) return fmt.Errorf("failed to create userdata folder: %s", err)
return
} }
err = os.WriteFile("userdata/"+hexUuid+"/system.pzs", compressed, 0644) err = os.WriteFile("userdata/"+hexUUID+"/system.pzs", compressed, 0644)
if err != nil { if err != nil {
httpError(w, r, fmt.Sprintf("failed to write save file: %s", err), http.StatusInternalServerError) return fmt.Errorf("failed to write save file: %s", err)
return
}
case "1": // Session
slotId, err := strconv.Atoi(r.URL.Query().Get("slot"))
if err != nil {
httpError(w, r, fmt.Sprintf("failed to convert slot id: %s", err), http.StatusBadRequest)
return
} }
if slotId < 0 || slotId >= sessionSlotCount { db.DeleteClaimedAccountCompensations(uuid)
httpError(w, r, fmt.Sprintf("slot id %d out of range", slotId), http.StatusBadRequest) case defs.SessionSaveData: // Session
return if slot < 0 || slot >= sessionSlotCount {
return fmt.Errorf("slot id %d out of range", slot)
} }
fileName := "session" fileName := "session"
if slotId != 0 { if slot != 0 {
fileName += strconv.Itoa(slotId) fileName += strconv.Itoa(slot)
}
var session defs.SessionSaveData
err = json.NewDecoder(r.Body).Decode(&session)
if err != nil {
httpError(w, r, fmt.Sprintf("failed to decode request body: %s", err), http.StatusBadRequest)
return
} }
var gobBuffer bytes.Buffer var gobBuffer bytes.Buffer
err = gob.NewEncoder(&gobBuffer).Encode(session) err = gob.NewEncoder(&gobBuffer).Encode(save)
if err != nil { if err != nil {
httpError(w, r, fmt.Sprintf("failed to serialize save: %s", err), http.StatusInternalServerError) return fmt.Errorf("failed to serialize save: %s", err)
return
} }
zstdWriter, err := zstd.NewWriter(nil) zstdWriter, err := zstd.NewWriter(nil)
if err != nil { if err != nil {
httpError(w, r, fmt.Sprintf("failed to create zstd writer, %s", err), http.StatusInternalServerError) return fmt.Errorf("failed to create zstd writer, %s", err)
return
} }
compressed := zstdWriter.EncodeAll(gobBuffer.Bytes(), nil) compressed := zstdWriter.EncodeAll(gobBuffer.Bytes(), nil)
err = os.MkdirAll("userdata/"+hexUuid, 0755) err = os.MkdirAll("userdata/"+hexUUID, 0755)
if err != nil && !os.IsExist(err) { if err != nil && !os.IsExist(err) {
httpError(w, r, fmt.Sprintf("failed to create userdata folder: %s", err), http.StatusInternalServerError) return fmt.Errorf(fmt.Sprintf("failed to create userdata folder: %s", err))
return
} }
err = os.WriteFile(fmt.Sprintf("userdata/%s/%s.pzs", hexUuid, fileName), compressed, 0644) err = os.WriteFile(fmt.Sprintf("userdata/%s/%s.pzs", hexUUID, fileName), compressed, 0644)
if err != nil { if err != nil {
httpError(w, r, fmt.Sprintf("failed to write save file: %s", err), http.StatusInternalServerError) return fmt.Errorf("failed to write save file: %s", err)
return
} }
default: default:
httpError(w, r, "invalid data type", http.StatusBadRequest) return fmt.Errorf("invalid data type")
return
} }
w.WriteHeader(http.StatusOK) return nil
} }
// /savedata/delete - delete save data // /savedata/delete - delete save data
func (s *Server) handleSavedataDelete(w http.ResponseWriter, r *http.Request) { func handleSavedataDelete(uuid []byte, datatype, slot int) error {
uuid, err := getUuidFromRequest(r) err := db.UpdateAccountLastActivity(uuid)
if err != nil {
httpError(w, r, err.Error(), http.StatusBadRequest)
return
}
err = db.UpdateAccountLastActivity(uuid)
if err != nil { if err != nil {
log.Print("failed to update account last activity") log.Print("failed to update account last activity")
} }
hexUuid := hex.EncodeToString(uuid) hexUUID := hex.EncodeToString(uuid)
switch r.URL.Query().Get("datatype") { switch datatype {
case "0": // System case 0: // System
err := os.Remove("userdata/" + hexUuid + "/system.pzs") err := os.Remove("userdata/" + hexUUID + "/system.pzs")
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
httpError(w, r, fmt.Sprintf("failed to delete save file: %s", err), http.StatusInternalServerError) return fmt.Errorf("failed to delete save file: %s", err)
return
} }
case "1": // Session case 1: // Session
slotId, err := strconv.Atoi(r.URL.Query().Get("slot")) if slot < 0 || slot >= sessionSlotCount {
if err != nil { return fmt.Errorf("slot id %d out of range", slot)
httpError(w, r, fmt.Sprintf("failed to convert slot id: %s", err), http.StatusBadRequest)
return
}
if slotId < 0 || slotId >= sessionSlotCount {
httpError(w, r, fmt.Sprintf("slot id %d out of range", slotId), http.StatusBadRequest)
return
} }
fileName := "session" fileName := "session"
if slotId != 0 { if slot != 0 {
fileName += strconv.Itoa(slotId) fileName += strconv.Itoa(slot)
} }
err = os.Remove(fmt.Sprintf("userdata/%s/%s.pzs", hexUuid, fileName)) err = os.Remove(fmt.Sprintf("userdata/%s/%s.pzs", hexUUID, fileName))
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
httpError(w, r, fmt.Sprintf("failed to delete save file: %s", err), http.StatusInternalServerError) return fmt.Errorf("failed to delete save file: %s", err)
return
} }
default: default:
httpError(w, r, "invalid data type", http.StatusBadRequest) return fmt.Errorf("invalid data type")
return
} }
w.WriteHeader(http.StatusOK) return nil
} }
type SavedataClearResponse struct { type SavedataClearResponse struct {
@@ -248,73 +176,46 @@ type SavedataClearResponse struct {
} }
// /savedata/clear - mark session save data as cleared and delete // /savedata/clear - mark session save data as cleared and delete
func (s *Server) handleSavedataClear(w http.ResponseWriter, r *http.Request) { func handleSavedataClear(uuid []byte, slot int, save defs.SessionSaveData) (SavedataClearResponse, error) {
uuid, err := getUuidFromRequest(r) err := db.UpdateAccountLastActivity(uuid)
if err != nil {
httpError(w, r, err.Error(), http.StatusBadRequest)
return
}
err = db.UpdateAccountLastActivity(uuid)
if err != nil { if err != nil {
log.Print("failed to update account last activity") log.Print("failed to update account last activity")
} }
slotId, err := strconv.Atoi(r.URL.Query().Get("slot")) if slot < 0 || slot >= sessionSlotCount {
if err != nil { return SavedataClearResponse{}, fmt.Errorf("slot id %d out of range", slot)
httpError(w, r, fmt.Sprintf("failed to convert slot id: %s", err), http.StatusBadRequest)
return
} }
if slotId < 0 || slotId >= sessionSlotCount { sessionCompleted := validateSessionCompleted(save)
httpError(w, r, fmt.Sprintf("slot id %d out of range", slotId), http.StatusBadRequest)
return
}
var session defs.SessionSaveData
err = json.NewDecoder(r.Body).Decode(&session)
if err != nil {
httpError(w, r, fmt.Sprintf("failed to decode request body: %s", err), http.StatusBadRequest)
return
}
sessionCompleted := validateSessionCompleted(session)
newCompletion := false newCompletion := false
if session.GameMode == 3 && session.Seed == dailyRunSeed { if save.GameMode == 3 && save.Seed == dailyRunSeed {
waveCompleted := session.WaveIndex waveCompleted := save.WaveIndex
if !sessionCompleted { if !sessionCompleted {
waveCompleted-- waveCompleted--
} }
err = db.AddOrUpdateAccountDailyRun(uuid, session.Score, waveCompleted) err = db.AddOrUpdateAccountDailyRun(uuid, save.Score, waveCompleted)
if err != nil { if err != nil {
log.Printf("failed to add or update daily run record: %s", err) log.Printf("failed to add or update daily run record: %s", err)
} }
} }
if sessionCompleted { if sessionCompleted {
newCompletion, err = db.TryAddSeedCompletion(uuid, session.Seed, int(session.GameMode)) newCompletion, err = db.TryAddSeedCompletion(uuid, save.Seed, int(save.GameMode))
if err != nil { if err != nil {
log.Printf("failed to mark seed as completed: %s", err) log.Printf("failed to mark seed as completed: %s", err)
} }
} }
response, err := json.Marshal(SavedataClearResponse{Success: newCompletion})
if err != nil {
httpError(w, r, fmt.Sprintf("failed to marshal response json: %s", err), http.StatusInternalServerError)
return
}
fileName := "session" fileName := "session"
if slotId != 0 { if slot != 0 {
fileName += strconv.Itoa(slotId) fileName += strconv.Itoa(slot)
} }
err = os.Remove(fmt.Sprintf("userdata/%s/%s.pzs", hex.EncodeToString(uuid), fileName)) err = os.Remove(fmt.Sprintf("userdata/%s/%s.pzs", hex.EncodeToString(uuid), fileName))
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
httpError(w, r, fmt.Sprintf("failed to delete save file: %s", err), http.StatusInternalServerError) return SavedataClearResponse{}, fmt.Errorf("failed to delete save file: %s", err)
return
} }
w.Write(response) return SavedataClearResponse{Success: newCompletion}, nil
} }
+38 -1
View File
@@ -100,6 +100,43 @@ func UpdateAccountStats(uuid []byte, stats defs.GameStats) error {
return nil return nil
} }
func FetchAndClaimAccountCompensations(uuid []byte) (map[int]int, error) {
var compensations = make(map[int]int)
results, err := handle.Query("SELECT voucherType, count FROM accountCompensations WHERE uuid = ?", uuid)
if err != nil {
return nil, err
}
defer results.Close()
for results.Next() {
var voucherType int
var count int
err := results.Scan(&voucherType, &count)
if err != nil {
return compensations, err
}
compensations[voucherType] = count
}
_, err = handle.Exec("UPDATE accountCompensations SET claimed = 1 WHERE uuid = ?", uuid)
if err != nil {
return compensations, err
}
return compensations, nil
}
func DeleteClaimedAccountCompensations(uuid []byte) error {
_, err := handle.Exec("DELETE FROM accountCompensations WHERE uuid = ? AND claimed = 1", uuid)
if err != nil {
return err
}
return nil
}
func FetchUsernameFromToken(token []byte) (string, error) { func FetchUsernameFromToken(token []byte) (string, error) {
var username string var username string
err := handle.QueryRow("SELECT a.username FROM accounts a JOIN sessions s ON s.uuid = a.uuid WHERE s.token = ? AND s.expire > UTC_TIMESTAMP()", token).Scan(&username) err := handle.QueryRow("SELECT a.username FROM accounts a JOIN sessions s ON s.uuid = a.uuid WHERE s.token = ? AND s.expire > UTC_TIMESTAMP()", token).Scan(&username)
@@ -120,7 +157,7 @@ func FetchAccountKeySaltFromUsername(username string) ([]byte, []byte, error) {
return key, salt, nil return key, salt, nil
} }
func FetchUuidFromToken(token []byte) ([]byte, error) { func FetchUUIDFromToken(token []byte) ([]byte, error) {
var uuid []byte var uuid []byte
err := handle.QueryRow("SELECT uuid FROM sessions WHERE token = ? AND expire > UTC_TIMESTAMP()", token).Scan(&uuid) err := handle.QueryRow("SELECT uuid FROM sessions WHERE token = ? AND expire > UTC_TIMESTAMP()", token).Scan(&uuid)
if err != nil { if err != nil {