First commit
This commit is contained in:
9
backend/.env.example
Normal file
9
backend/.env.example
Normal file
@@ -0,0 +1,9 @@
|
||||
MYSQL_DATABASE=alai
|
||||
MYSQL_ROOT_PASSWORD=rootpass
|
||||
MYSQL_USER=user
|
||||
MYSQL_PASSWORD=pass
|
||||
MYSQL_HOST=%
|
||||
MYSQL_HOST_IP=localhost
|
||||
MYSQL_PORT=3306
|
||||
JWT_SECRET=supersecretkey
|
||||
ADMIN_PASSWORD=adminpass
|
11
backend/Dockerfile
Normal file
11
backend/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM golang:1.18.3 AS builder
|
||||
WORKDIR /usr/src/app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download && go mod verify
|
||||
COPY . .
|
||||
RUN go build -buildvcs=false -v -o /usr/local/bin/ ./...
|
||||
|
||||
FROM golang:1.18.3
|
||||
WORKDIR /usr/local/bin
|
||||
COPY --from=builder /usr/local/bin/backend .
|
||||
CMD ["backend", "migrate", "serve"]
|
104
backend/controllers/game.go
Normal file
104
backend/controllers/game.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"backend/database"
|
||||
"backend/models"
|
||||
"backend/utils"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func PostGame(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
||||
gdb := database.Connect()
|
||||
defer database.Close(gdb)
|
||||
|
||||
decoded := base64.NewDecoder(base64.StdEncoding, request.Body)
|
||||
|
||||
uncompressed, err := gzip.NewReader(decoded)
|
||||
if err != nil {
|
||||
utils.JSONErrorOutput(writer, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
defer uncompressed.Close()
|
||||
|
||||
decoder := json.NewDecoder(uncompressed)
|
||||
|
||||
var game models.Game
|
||||
|
||||
err = decoder.Decode(&game)
|
||||
if err != nil {
|
||||
utils.JSONErrorOutput(writer, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = game.Validate()
|
||||
if err != nil {
|
||||
utils.JSONErrorOutput(writer, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result := postGame(game, gdb)
|
||||
if result.Error != nil {
|
||||
utils.JSONErrorOutput(writer, http.StatusBadRequest, result.Error.Error())
|
||||
return
|
||||
} else {
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func postGame(game models.Game, gdb *gorm.DB) *gorm.DB {
|
||||
return gdb.Create(&game)
|
||||
}
|
||||
|
||||
func ListGames(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
||||
gdb := database.Connect()
|
||||
defer database.Close(gdb)
|
||||
|
||||
var games []models.Game
|
||||
|
||||
result := listGames(&games, gdb)
|
||||
if result.Error != nil {
|
||||
utils.JSONErrorOutput(writer, http.StatusBadRequest, result.Error.Error())
|
||||
return
|
||||
} else {
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(writer).Encode(games)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func listGames(games *[]models.Game, gdb *gorm.DB) *gorm.DB {
|
||||
return gdb.Model(&models.Game{}).Order("ID asc").Joins("Player").Joins("Level").Joins("OS").Joins("GodotVersion").Find(&games)
|
||||
}
|
||||
|
||||
func GetGame(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
||||
gdb := database.Connect()
|
||||
defer database.Close(gdb)
|
||||
|
||||
var game models.Game
|
||||
|
||||
result := getGame(&game, params.ByName("id"), gdb)
|
||||
if result.Error != nil {
|
||||
utils.JSONErrorOutput(writer, http.StatusBadRequest, result.Error.Error())
|
||||
return
|
||||
} else if result.RowsAffected == 0 {
|
||||
utils.JSONErrorOutput(writer, http.StatusNotFound, "A game with the ID "+params.ByName("id")+" does not exist!")
|
||||
return
|
||||
} else {
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(writer).Encode(game)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func getGame(games *models.Game, id string, gdb *gorm.DB) *gorm.DB {
|
||||
return gdb.Model(&models.Game{}).Order("ID asc").Joins("Player").Joins("Level").Joins("OS").Find(&games, id)
|
||||
}
|
134
backend/controllers/user.go
Normal file
134
backend/controllers/user.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"backend/database"
|
||||
"backend/models"
|
||||
"backend/utils"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
func AuthenticateUser(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
||||
type Message struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
message := Message{Status: "authorized"}
|
||||
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(writer).Encode(message)
|
||||
}
|
||||
|
||||
func CreateUser(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
||||
gdb := database.Connect()
|
||||
defer database.Close(gdb)
|
||||
|
||||
user := models.User{
|
||||
Name: request.FormValue("name"),
|
||||
Email: request.FormValue("email"),
|
||||
}
|
||||
user.HashPassword(request.FormValue("password"))
|
||||
|
||||
gdb.Create(&user)
|
||||
}
|
||||
|
||||
func UpdateUser(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
||||
gdb := database.Connect()
|
||||
defer database.Close(gdb)
|
||||
|
||||
var user models.User
|
||||
|
||||
userID, _ := strconv.ParseUint(params.ByName("id"), 10, 64)
|
||||
|
||||
gdb.Model(models.User{}).Where(&models.User{ID: userID}).Find(&user)
|
||||
|
||||
if request.FormValue("password") != "" {
|
||||
var oldPassword = request.FormValue("old_password")
|
||||
err := user.CheckPassword(oldPassword)
|
||||
if err != nil {
|
||||
utils.JSONErrorOutput(writer, http.StatusBadRequest, errors.New("incorrect password").Error())
|
||||
return
|
||||
|
||||
} else {
|
||||
user.HashPassword(request.FormValue("password"))
|
||||
}
|
||||
}
|
||||
|
||||
user.Name = request.FormValue("name")
|
||||
user.Email = request.FormValue("email")
|
||||
|
||||
gdb.Updates(&user)
|
||||
}
|
||||
|
||||
func ListUsers(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
||||
gdb := database.Connect()
|
||||
defer database.Close(gdb)
|
||||
|
||||
var users []models.User
|
||||
|
||||
result := gdb.Model(&models.User{}).Order("ID asc").Find(&users)
|
||||
if result.Error != nil {
|
||||
utils.JSONErrorOutput(writer, http.StatusBadRequest, result.Error.Error())
|
||||
return
|
||||
} else {
|
||||
for i := range users {
|
||||
users[i].Password = ""
|
||||
}
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(writer).Encode(users)
|
||||
}
|
||||
}
|
||||
|
||||
func GetUser(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
||||
gdb := database.Connect()
|
||||
defer database.Close(gdb)
|
||||
|
||||
var user models.User
|
||||
|
||||
result := gdb.Model(&models.User{}).Find(&user, params.ByName("id"))
|
||||
if result.Error != nil {
|
||||
utils.JSONErrorOutput(writer, http.StatusBadRequest, result.Error.Error())
|
||||
return
|
||||
} else if result.RowsAffected == 0 {
|
||||
utils.JSONErrorOutput(writer, http.StatusNotFound, "A user with the id "+params.ByName("id")+" doesn't exist!")
|
||||
return
|
||||
} else {
|
||||
user.Password = ""
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(writer).Encode(user)
|
||||
}
|
||||
}
|
||||
|
||||
func Login(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
||||
gdb := database.Connect()
|
||||
defer database.Close(gdb)
|
||||
|
||||
var user models.User
|
||||
|
||||
gdb.Model(models.User{}).Where(&models.User{Username: request.FormValue("username")}).Find(&user)
|
||||
|
||||
err := user.CheckPassword(request.FormValue("password"))
|
||||
if err != nil {
|
||||
utils.JSONErrorOutput(writer, http.StatusBadRequest, errors.New("incorrect password").Error())
|
||||
return
|
||||
}
|
||||
|
||||
type Token struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
tokenString, _ := utils.GenerateJWT("chris@cromer.cl", "cromer")
|
||||
|
||||
token := Token{Token: tokenString}
|
||||
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(writer).Encode(token)
|
||||
}
|
132
backend/database/database.go
Normal file
132
backend/database/database.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"backend/models"
|
||||
"os"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
func Connect() *gorm.DB {
|
||||
dbUser := os.Getenv("MYSQL_USER")
|
||||
dbPass := os.Getenv("MYSQL_PASSWORD")
|
||||
dbHost := os.Getenv("MYSQL_HOST_IP")
|
||||
dbName := os.Getenv("MYSQL_DATABASE")
|
||||
|
||||
gdb, err := gorm.Open(mysql.New(mysql.Config{
|
||||
DSN: dbUser + ":" + dbPass + "@tcp(" + dbHost + ")/" + dbName + "?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
DefaultStringSize: 256,
|
||||
}), &gorm.Config{
|
||||
CreateBatchSize: 1000,
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
SingularTable: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic("failed to connect database")
|
||||
}
|
||||
|
||||
return gdb
|
||||
}
|
||||
|
||||
func AutoMigrate(gdb *gorm.DB) {
|
||||
var populate = make(map[string]bool)
|
||||
if (!gdb.Migrator().HasTable(&models.OS{})) {
|
||||
populate["os"] = true
|
||||
}
|
||||
if (!gdb.Migrator().HasTable(&models.Level{})) {
|
||||
populate["level"] = true
|
||||
}
|
||||
if (!gdb.Migrator().HasTable(&models.User{})) {
|
||||
populate["user"] = true
|
||||
}
|
||||
gdb.AutoMigrate(&models.GodotVersion{})
|
||||
gdb.AutoMigrate(&models.Game{})
|
||||
gdb.AutoMigrate(&models.Level{})
|
||||
gdb.AutoMigrate(&models.OS{})
|
||||
gdb.AutoMigrate(&models.Player{})
|
||||
gdb.AutoMigrate(&models.ObjectState{})
|
||||
gdb.AutoMigrate(&models.ObjectName{})
|
||||
gdb.AutoMigrate(&models.Object{})
|
||||
gdb.AutoMigrate(&models.Frame{})
|
||||
gdb.AutoMigrate(&models.User{})
|
||||
Populate(gdb, populate)
|
||||
}
|
||||
|
||||
func DropAll(gdb *gorm.DB) {
|
||||
err := gdb.Migrator().DropTable(
|
||||
&models.GodotVersion{},
|
||||
&models.Object{},
|
||||
&models.Frame{},
|
||||
&models.Game{},
|
||||
&models.Level{},
|
||||
&models.OS{},
|
||||
&models.Player{},
|
||||
&models.ObjectState{},
|
||||
&models.ObjectName{},
|
||||
&models.User{})
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func Populate(gdb *gorm.DB, populate map[string]bool) {
|
||||
if _, ok := populate["os"]; ok {
|
||||
os_list := []models.OS{
|
||||
{Name: "Android"},
|
||||
{Name: "iOS"},
|
||||
{Name: "HTML5"},
|
||||
{Name: "OSX"},
|
||||
{Name: "Server"},
|
||||
{Name: "Windows"},
|
||||
{Name: "UWP"},
|
||||
{Name: "X11"},
|
||||
}
|
||||
|
||||
gdb.Create(&os_list)
|
||||
}
|
||||
|
||||
if _, ok := populate["level"]; ok {
|
||||
level_list := []models.Level{
|
||||
{Name: "Prototype"},
|
||||
{Name: "PrototypeR"},
|
||||
{Name: "Level1"},
|
||||
{Name: "Level2"},
|
||||
{Name: "Level3"},
|
||||
{Name: "Level4"},
|
||||
{Name: "Level5"},
|
||||
{Name: "Level6"},
|
||||
}
|
||||
|
||||
gdb.Create(&level_list)
|
||||
}
|
||||
|
||||
if _, ok := populate["user"]; ok {
|
||||
user_list := []models.User{
|
||||
{
|
||||
Name: "Chris Cromer",
|
||||
Username: "cromer",
|
||||
Email: "chris@cromer.cl",
|
||||
},
|
||||
{
|
||||
Name: "Martín Araneda",
|
||||
Username: "martin",
|
||||
Email: "martix.araneda@gmail.com",
|
||||
},
|
||||
}
|
||||
user_list[0].HashPassword(os.Getenv("ADMIN_PASSWORD"))
|
||||
user_list[1].HashPassword(os.Getenv("ADMIN_PASSWORD"))
|
||||
|
||||
gdb.Create(&user_list)
|
||||
}
|
||||
}
|
||||
|
||||
func Close(gdb *gorm.DB) {
|
||||
sqlDB, err := gdb.DB()
|
||||
if err != nil {
|
||||
panic("failed to get gorm db")
|
||||
}
|
||||
sqlDB.Close()
|
||||
}
|
24
backend/go.mod
Normal file
24
backend/go.mod
Normal file
@@ -0,0 +1,24 @@
|
||||
module backend
|
||||
|
||||
go 1.18
|
||||
|
||||
require github.com/julienschmidt/httprouter v1.3.0
|
||||
|
||||
require golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e
|
||||
|
||||
require github.com/joho/godotenv v1.4.0
|
||||
|
||||
require github.com/golang-jwt/jwt/v4 v4.4.2
|
||||
|
||||
require (
|
||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
gorm.io/driver/mysql v1.3.4
|
||||
gorm.io/gorm v1.23.6
|
||||
)
|
25
backend/go.sum
Normal file
25
backend/go.sum
Normal file
@@ -0,0 +1,25 @@
|
||||
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
|
||||
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
gorm.io/driver/mysql v1.3.4 h1:/KoBMgsUHC3bExsekDcmNYaBnfH2WNeFuXqqrqMc98Q=
|
||||
gorm.io/driver/mysql v1.3.4/go.mod h1:s4Tq0KmD0yhPGHbZEwg1VPlH0vT/GBHJZorPzhcxBUE=
|
||||
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.23.6 h1:KFLdNgri4ExFFGTRGGFWON2P1ZN28+9SJRN8voOoYe0=
|
||||
gorm.io/gorm v1.23.6/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
72
backend/main.go
Normal file
72
backend/main.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"backend/database"
|
||||
"backend/routes"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
print(err.Error())
|
||||
}
|
||||
|
||||
allArgs := os.Args[1:]
|
||||
|
||||
dropDB := false
|
||||
migrateDB := false
|
||||
serve := false
|
||||
dbOperation := false
|
||||
|
||||
for _, element := range allArgs {
|
||||
if element == "drop" {
|
||||
dropDB = true
|
||||
dbOperation = true
|
||||
}
|
||||
|
||||
if element == "migrate" {
|
||||
migrateDB = true
|
||||
dbOperation = true
|
||||
}
|
||||
|
||||
if element == "serve" {
|
||||
serve = true
|
||||
}
|
||||
}
|
||||
|
||||
var gdb *gorm.DB
|
||||
if dbOperation {
|
||||
fmt.Print("Connecting to database ... ")
|
||||
gdb = database.Connect()
|
||||
fmt.Println("DONE")
|
||||
}
|
||||
|
||||
if dropDB {
|
||||
fmt.Print("Dropping database ... ")
|
||||
database.DropAll(gdb)
|
||||
fmt.Println("DONE")
|
||||
}
|
||||
|
||||
if migrateDB {
|
||||
fmt.Print("AutoMigrating database ... ")
|
||||
database.AutoMigrate(gdb)
|
||||
fmt.Println("DONE")
|
||||
}
|
||||
|
||||
if dbOperation {
|
||||
fmt.Print("Closing database ... ")
|
||||
database.Close(gdb)
|
||||
fmt.Println("DONE")
|
||||
}
|
||||
|
||||
if serve {
|
||||
router := routes.Initialize()
|
||||
fmt.Println("Serving routes ... DONE")
|
||||
routes.Serve(router)
|
||||
}
|
||||
}
|
30
backend/middlewares/auth.go
Normal file
30
backend/middlewares/auth.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"backend/utils"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
func Authenticate(handle httprouter.Handle) httprouter.Handle {
|
||||
return func(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
|
||||
reqToken := request.Header.Get("Authorization")
|
||||
splitToken := strings.Split(reqToken, "Bearer ")
|
||||
if len(splitToken) < 2 {
|
||||
utils.JSONErrorOutput(writer, http.StatusUnauthorized, errors.New("no token received").Error())
|
||||
return
|
||||
}
|
||||
tokenString := splitToken[1]
|
||||
|
||||
err := utils.ValidateToken(tokenString)
|
||||
if err != nil {
|
||||
utils.JSONErrorOutput(writer, http.StatusUnauthorized, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
handle(writer, request, params)
|
||||
}
|
||||
}
|
17
backend/models/frame.go
Normal file
17
backend/models/frame.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Frame struct {
|
||||
gorm.Model
|
||||
ID uint64 `json:"ID" gorm:"primaryKey"`
|
||||
GameID uint64 `json:"game_id" gorm:"not null"`
|
||||
Game Game `json:"game" gorm:"not null"`
|
||||
Coins uint64 `json:"coins" gorm:";not null"`
|
||||
Points uint64 `json:"points" gorm:"not null"`
|
||||
FPS uint8 `json:"fps" gorm:"not null"`
|
||||
ElapsedTime uint64 `json:"elapsed_time" gorm:"not null"`
|
||||
Objects []Object `json:"objects"`
|
||||
}
|
100
backend/models/game.go
Normal file
100
backend/models/game.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Game struct {
|
||||
gorm.Model
|
||||
ID uint64 `json:"ID" gorm:"primaryKey"`
|
||||
PlayerID uint64 `json:"player_id" gorm:"not null"`
|
||||
Player Player `json:"player"`
|
||||
LevelID uint64 `json:"level_id" gorm:"not null"`
|
||||
Level Level `json:"level" gorm:"not null"`
|
||||
OSID uint64 `json:"os_id" gorm:"not null"`
|
||||
OS OS `json:"os" gorm:"not null"`
|
||||
GodotVersionID uint64 `json:"godot_version_id" gorm:"not null"`
|
||||
GodotVersion GodotVersion `json:"godot_version" gorm:"not null"`
|
||||
ProcessorCount uint64 `json:"processor_count" gorm:"not null"`
|
||||
ScreenCount uint8 `json:"screen_count" gorm:"not null"`
|
||||
ScreenDPI uint8 `json:"screen_dpi" gorm:"not null"`
|
||||
ScreenSize string `json:"screen_size" gorm:"not null"`
|
||||
MachineId string `json:"machine_id" gorm:"not null"`
|
||||
Locale string `json:"locale" gorm:"not null"`
|
||||
GameVersion string `json:"game_version" gorm:"not null"`
|
||||
Won bool `json:"won" gorm:"not null"`
|
||||
Timestamp uint64 `json:"timestamp" gorm:"not null"`
|
||||
Frames []Frame `json:"frames"`
|
||||
}
|
||||
|
||||
func (game *Game) Validate() error {
|
||||
if len(strings.TrimSpace(game.MachineId)) == 0 {
|
||||
return errors.New("empty machine id")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cache the results of the queries here
|
||||
// The object states and names should not be deleted so this should always be valid while running
|
||||
var cachedStateNames = make(map[string]uint64)
|
||||
var cachedObjectNames = make(map[string]uint64)
|
||||
|
||||
func (game *Game) BeforeCreate(tx *gorm.DB) error {
|
||||
// Use the same player ID if the RUT is already in the DB
|
||||
tx.Model(Player{}).Where(&Player{RUT: game.Player.RUT}).Find(&game.Player)
|
||||
|
||||
tx.Model(GodotVersion{}).Where(&GodotVersion{String: game.GodotVersion.String}).Find(&game.GodotVersion)
|
||||
|
||||
for frameIndex, frame := range game.Frames {
|
||||
for objectIndex := range frame.Objects {
|
||||
game.Frames[frameIndex].Objects[objectIndex].ObjectState.Name = game.Frames[frameIndex].Objects[objectIndex].State
|
||||
game.Frames[frameIndex].Objects[objectIndex].ObjectName.Name = game.Frames[frameIndex].Objects[objectIndex].Name
|
||||
|
||||
// Use the existing state names in the database if they exist
|
||||
if ID, ok := cachedStateNames[game.Frames[frameIndex].Objects[objectIndex].ObjectState.Name]; ok {
|
||||
// The name is cached, no need to query the database
|
||||
game.Frames[frameIndex].Objects[objectIndex].ObjectStateID = ID
|
||||
game.Frames[frameIndex].Objects[objectIndex].ObjectState.ID = ID
|
||||
} else {
|
||||
var state ObjectState
|
||||
result := tx.Model(ObjectState{}).Where(&ObjectState{Name: game.Frames[frameIndex].Objects[objectIndex].ObjectState.Name}).Find(&state)
|
||||
if result.RowsAffected == 0 {
|
||||
// Not in the database, so let's create it
|
||||
tx.Create(&game.Frames[frameIndex].Objects[objectIndex].ObjectState)
|
||||
game.Frames[frameIndex].Objects[objectIndex].ObjectStateID = game.Frames[frameIndex].Objects[objectIndex].ObjectState.ID
|
||||
cachedStateNames[game.Frames[frameIndex].Objects[objectIndex].ObjectState.Name] = game.Frames[frameIndex].Objects[objectIndex].ObjectState.ID
|
||||
} else {
|
||||
// It is in the database, so use that
|
||||
game.Frames[frameIndex].Objects[objectIndex].ObjectStateID = state.ID
|
||||
game.Frames[frameIndex].Objects[objectIndex].ObjectState.ID = state.ID
|
||||
cachedStateNames[game.Frames[frameIndex].Objects[objectIndex].ObjectState.Name] = state.ID
|
||||
}
|
||||
}
|
||||
|
||||
// Use the existing object names in the database if they exist
|
||||
if ID, ok := cachedObjectNames[game.Frames[frameIndex].Objects[objectIndex].ObjectName.Name]; ok {
|
||||
// The name is cached, no need to query the database
|
||||
game.Frames[frameIndex].Objects[objectIndex].ObjectNameID = ID
|
||||
game.Frames[frameIndex].Objects[objectIndex].ObjectName.ID = ID
|
||||
} else {
|
||||
var objectName ObjectName
|
||||
result := tx.Model(ObjectName{}).Where(&ObjectName{Name: game.Frames[frameIndex].Objects[objectIndex].ObjectName.Name}).Find(&objectName)
|
||||
if result.RowsAffected == 0 {
|
||||
// Not in the database, so let's create it
|
||||
tx.Create(&game.Frames[frameIndex].Objects[objectIndex].ObjectName)
|
||||
game.Frames[frameIndex].Objects[objectIndex].ObjectNameID = game.Frames[frameIndex].Objects[objectIndex].ObjectName.ID
|
||||
cachedObjectNames[game.Frames[frameIndex].Objects[objectIndex].ObjectName.Name] = game.Frames[frameIndex].Objects[objectIndex].ObjectName.ID
|
||||
} else {
|
||||
// It is in the database, so use that
|
||||
game.Frames[frameIndex].Objects[objectIndex].ObjectNameID = objectName.ID
|
||||
game.Frames[frameIndex].Objects[objectIndex].ObjectName.ID = objectName.ID
|
||||
cachedObjectNames[game.Frames[frameIndex].Objects[objectIndex].ObjectName.Name] = objectName.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
17
backend/models/godot_version.go
Normal file
17
backend/models/godot_version.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type GodotVersion struct {
|
||||
gorm.Model
|
||||
ID uint64 `json:"ID" gorm:"primaryKey"`
|
||||
Major uint8 `json:"major" gorm:"not null"`
|
||||
Minor uint8 `json:"minor" gorm:"not null"`
|
||||
Patch uint8 `json:"patch" gorm:"not null"`
|
||||
Hex uint64 `json:"hex" gorm:"not null"`
|
||||
Status string `json:"status" gorm:"not null"`
|
||||
Build string `json:"build" gorm:"not null"`
|
||||
Year uint16 `json:"year" gorm:"not null"`
|
||||
Hash string `json:"hash" gorm:"unique;size:40;not null"`
|
||||
String string `json:"string" gorm:"unique;not null"`
|
||||
}
|
9
backend/models/level.go
Normal file
9
backend/models/level.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Level struct {
|
||||
gorm.Model
|
||||
ID uint64 `json:"ID" gorm:"primaryKey"`
|
||||
Name string `json:"rut" gorm:"unique;not null"`
|
||||
}
|
20
backend/models/object.go
Normal file
20
backend/models/object.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Object struct {
|
||||
gorm.Model
|
||||
ID uint64 `json:"ID" gorm:"primaryKey"`
|
||||
FrameID uint64 `json:"frame_id" gorm:"not null"`
|
||||
Frame Frame `json:"frame" gorm:"not null"`
|
||||
ObjectNameID uint64 `json:"-" gorm:"not null"`
|
||||
ObjectName ObjectName `json:"-" gorm:"not null"`
|
||||
ObjectStateID uint64 `json:"-" gorm:"not null"`
|
||||
ObjectState ObjectState `json:"-" gorm:"not null"`
|
||||
Name string `json:"name" gorm:"-:all"`
|
||||
State string `json:"state" gorm:"-:all"`
|
||||
PositionX float64 `json:"position_x" gorm:"not null"`
|
||||
PositionY float64 `json:"position_y" gorm:"not null"`
|
||||
VelocityX float64 `json:"velocity_x" gorm:"not null"`
|
||||
VelocityY float64 `json:"velocity_y" gorm:"not null"`
|
||||
}
|
9
backend/models/object_name.go
Normal file
9
backend/models/object_name.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type ObjectName struct {
|
||||
gorm.Model
|
||||
ID uint64 `json:"ID" gorm:"primaryKey"`
|
||||
Name string `json:"name" gorm:"unique;not null"`
|
||||
}
|
9
backend/models/object_state.go
Normal file
9
backend/models/object_state.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type ObjectState struct {
|
||||
gorm.Model
|
||||
ID uint64 `json:"ID" gorm:"primaryKey"`
|
||||
Name string `json:"name" gorm:"unique;not null"`
|
||||
}
|
9
backend/models/os.go
Normal file
9
backend/models/os.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type OS struct {
|
||||
gorm.Model
|
||||
ID uint64 `json:"ID" gorm:"primaryKey"`
|
||||
Name string `json:"name" gorm:"unique;size:8;not null"`
|
||||
}
|
11
backend/models/player.go
Normal file
11
backend/models/player.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Player struct {
|
||||
gorm.Model
|
||||
ID uint64 `json:"ID" gorm:"primaryKey"`
|
||||
RUT string `json:"rut" gorm:"unique;size:9;not null"`
|
||||
Name string `json:"name" gorm:"not null"`
|
||||
Email string `json:"email" gorm:"unique;not null"`
|
||||
}
|
32
backend/models/user.go
Normal file
32
backend/models/user.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
gorm.Model
|
||||
ID uint64 `json:"ID" gorm:"primaryKey"`
|
||||
Name string `json:"name" gorm:"not null"`
|
||||
Username string `json:"username" gorm:"unique; not null"`
|
||||
Email string `json:"email" gorm:"unique;not null"`
|
||||
Password string `json:"password" gorm:"not null"`
|
||||
}
|
||||
|
||||
func (user *User) HashPassword(password string) error {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Password = string(bytes)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (user *User) CheckPassword(providedPassword string) error {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(providedPassword))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
14
backend/routes/game.go
Normal file
14
backend/routes/game.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"backend/controllers"
|
||||
"backend/middlewares"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
func GameRoutes(router *httprouter.Router) {
|
||||
router.POST("/game", controllers.PostGame)
|
||||
router.GET("/games", middlewares.Authenticate(controllers.ListGames))
|
||||
router.GET("/game/:id", middlewares.Authenticate(controllers.GetGame))
|
||||
}
|
56
backend/routes/router.go
Normal file
56
backend/routes/router.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
func Initialize() *httprouter.Router {
|
||||
router := httprouter.New()
|
||||
router.GET("/", index)
|
||||
UserRoutes(router)
|
||||
GameRoutes(router)
|
||||
return router
|
||||
}
|
||||
|
||||
func Serve(router *httprouter.Router) {
|
||||
newRouter := handlers.CombinedLoggingHandler(os.Stdout, router)
|
||||
newRouter = handlers.CompressHandler(newRouter)
|
||||
|
||||
idleConnsClosed := make(chan struct{})
|
||||
|
||||
server := &http.Server{Addr: ":3001", Handler: newRouter}
|
||||
|
||||
// Listen for CTRL-C(SIGTERM)
|
||||
sigterm := make(chan os.Signal)
|
||||
signal.Notify(sigterm, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
<-sigterm
|
||||
// When CTRL-C is pressed shutdown the server
|
||||
if err := server.Shutdown(context.Background()); err != nil {
|
||||
log.Printf("HTTP server Shutdown: %v", err)
|
||||
}
|
||||
close(idleConnsClosed)
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
// Run the server
|
||||
if err := server.ListenAndServe(); err != http.ErrServerClosed {
|
||||
log.Fatalf("HTTP server ListenAndServe: %v", err)
|
||||
}
|
||||
|
||||
<-idleConnsClosed
|
||||
}
|
||||
|
||||
func index(writer http.ResponseWriter, _ *http.Request, _ httprouter.Params) {
|
||||
fmt.Fprintf(writer, "This is the Alai API server!")
|
||||
}
|
17
backend/routes/user.go
Normal file
17
backend/routes/user.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"backend/controllers"
|
||||
"backend/middlewares"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
func UserRoutes(router *httprouter.Router) {
|
||||
router.POST("/login", controllers.Login)
|
||||
router.GET("/users", middlewares.Authenticate(controllers.ListUsers))
|
||||
router.POST("/user", middlewares.Authenticate(controllers.CreateUser))
|
||||
router.GET("/user/:id", middlewares.Authenticate(controllers.GetUser))
|
||||
router.PATCH("/user/:id", middlewares.Authenticate(controllers.UpdateUser))
|
||||
router.GET("/auth", middlewares.Authenticate(controllers.AuthenticateUser))
|
||||
}
|
3
backend/test.sh
Executable file
3
backend/test.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
go test -coverprofile=coverage.out ./...
|
||||
go tool cover -html=coverage.out -o test_coverage.html
|
96
backend/utils/datatype.go
Normal file
96
backend/utils/datatype.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Date time.Time // 2006-01-02
|
||||
type DateTime time.Time // 2006-01-02 15:04:05
|
||||
|
||||
func (date *Date) UnmarshalJSON(b []byte) error {
|
||||
s := strings.Trim(string(b), "\"")
|
||||
t, err := time.Parse("2006-01-02", s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*date = Date(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (date Date) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(time.Time(date))
|
||||
}
|
||||
|
||||
func (date Date) Format(s string) string {
|
||||
t := time.Time(date)
|
||||
return t.Format(s)
|
||||
}
|
||||
|
||||
func (date *Date) Scan(value interface{}) (err error) {
|
||||
nullTime := &sql.NullTime{}
|
||||
err = nullTime.Scan(value)
|
||||
*date = Date(nullTime.Time)
|
||||
return
|
||||
}
|
||||
|
||||
func (date Date) Value() (driver.Value, error) {
|
||||
return time.Time(date), nil
|
||||
}
|
||||
|
||||
func (date Date) GormDataType() string {
|
||||
return "date"
|
||||
}
|
||||
|
||||
func (date Date) GobEncode() ([]byte, error) {
|
||||
return time.Time(date).GobEncode()
|
||||
}
|
||||
|
||||
func (date *Date) GobDecode(b []byte) error {
|
||||
return (*time.Time)(date).GobDecode(b)
|
||||
}
|
||||
|
||||
func (dateTime *DateTime) UnmarshalJSON(b []byte) error {
|
||||
s := strings.Trim(string(b), "\"")
|
||||
t, err := time.Parse("2006-01-02 15:04:05", s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*dateTime = DateTime(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dateTime DateTime) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(time.Time(dateTime))
|
||||
}
|
||||
|
||||
func (dateTime DateTime) Format(s string) string {
|
||||
t := time.Time(dateTime)
|
||||
return t.Format(s)
|
||||
}
|
||||
|
||||
func (date *DateTime) Scan(value interface{}) (err error) {
|
||||
nullTime := &sql.NullTime{}
|
||||
err = nullTime.Scan(value)
|
||||
*date = DateTime(nullTime.Time)
|
||||
return
|
||||
}
|
||||
|
||||
func (date DateTime) Value() (driver.Value, error) {
|
||||
return time.Time(date), nil
|
||||
}
|
||||
|
||||
func (date DateTime) GormDataType() string {
|
||||
return "datetime"
|
||||
}
|
||||
|
||||
func (date DateTime) GobEncode() ([]byte, error) {
|
||||
return time.Time(date).GobEncode()
|
||||
}
|
||||
|
||||
func (date *DateTime) GobDecode(b []byte) error {
|
||||
return (*time.Time)(date).GobDecode(b)
|
||||
}
|
191
backend/utils/datatype_test.go
Normal file
191
backend/utils/datatype_test.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDateUnmarshalJSON(t *testing.T) {
|
||||
want := "1985-02-23 00:00:00 +0000 UTC"
|
||||
var date Date
|
||||
err := date.UnmarshalJSON([]byte("\"1985-02-23\""))
|
||||
msg := time.Time(date).String()
|
||||
if msg != want {
|
||||
t.Fatalf(`date.UnmarshalJSON([]byte("\"1985-02-23\"") = %q, %v, want match for %#q, nil`, msg, err, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateUnmarshalJSONMalformed(t *testing.T) {
|
||||
want := error(&time.ParseError{})
|
||||
var date Date
|
||||
err := date.UnmarshalJSON([]byte("\"1985/02/23\""))
|
||||
msg := time.Time(date).String()
|
||||
if reflect.TypeOf(err) != reflect.TypeOf(want) {
|
||||
t.Fatalf(`date.UnmarshalJSON([]byte("\"1985/02/23\"") = %q, %v, want match for %#q, nil`, msg, err, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateMarshalJSON(t *testing.T) {
|
||||
want := "\"1985-02-23T00:00:00Z\""
|
||||
parsed, _ := time.Parse("2006-01-02", "1985-02-23")
|
||||
var date Date = Date(parsed)
|
||||
msg, err := date.MarshalJSON()
|
||||
if string(msg) != want {
|
||||
t.Fatalf(`date.MarshalJSON() = %q, %v, want match for %#q, nil`, msg, err, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateFormat(t *testing.T) {
|
||||
want := "Feb 23, 1985"
|
||||
parsed, _ := time.Parse("2006-01-02", "1985-02-23")
|
||||
var date Date = Date(parsed)
|
||||
msg := date.Format("Jan 2, 2006")
|
||||
if msg != want {
|
||||
t.Fatalf(`date.Format("Jan 2, 2006") = %q, want match for %#q, nil`, msg, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateScan(t *testing.T) {
|
||||
want := "0001-01-01T00:00:00Z"
|
||||
parsed, _ := time.Parse("2006-01-02", "1985-02-23")
|
||||
var date Date = Date(parsed)
|
||||
err := date.Scan(Date{})
|
||||
msg := date.Format("2006-01-02T15:04:05Z07:00")
|
||||
if msg != want {
|
||||
t.Fatalf(`date.Scan(Date{}) = %q, %v, want match for %#q, nil`, msg, err, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateValue(t *testing.T) {
|
||||
var want time.Time
|
||||
parsed, _ := time.Parse("2006-01-02", "1985-02-23")
|
||||
var date Date = Date(parsed)
|
||||
msg, _ := date.Value()
|
||||
if reflect.TypeOf(msg) != reflect.TypeOf(want) {
|
||||
t.Fatalf(`date.Value() = %q, want match for %#q, nil`, msg, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateGormDateType(t *testing.T) {
|
||||
want := "date"
|
||||
parsed, _ := time.Parse("2006-01-02", "1985-02-23")
|
||||
var date Date = Date(parsed)
|
||||
msg := date.GormDataType()
|
||||
if msg != want {
|
||||
t.Fatalf(`date.GormDateType() = %q, want match for %#q, nil`, msg, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateGobEncode(t *testing.T) {
|
||||
want := "\x01\x00\x00\x00\x0e\x94\x0f!\x00\x00\x00\x00\x00\xff\xff"
|
||||
parsed, _ := time.Parse("2006-01-02", "1985-02-23")
|
||||
var date Date = Date(parsed)
|
||||
msg, err := date.GobEncode()
|
||||
if string(msg) != want {
|
||||
t.Fatalf(`date.GobEncode() = %q, %v, want match for %#q, nil`, msg, err, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateGobDecode(t *testing.T) {
|
||||
want := "1985-02-23T00:00:00Z"
|
||||
parsed, _ := time.Parse("2006-01-02", "2006-01-02")
|
||||
var date Date = Date(parsed)
|
||||
err := date.GobDecode([]byte("\x01\x00\x00\x00\x0e\x94\x0f!\x00\x00\x00\x00\x00\xff\xff"))
|
||||
msg := date.Format("2006-01-02T15:04:05Z07:00")
|
||||
if string(msg) != want {
|
||||
t.Fatalf(`date.GobDecode([]byte()) = %q, %v, want match for %#q, nil`, msg, err, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateTimeUnmarshalJSON(t *testing.T) {
|
||||
want := "1985-02-23 12:13:14 +0000 UTC"
|
||||
var dateTime DateTime
|
||||
err := dateTime.UnmarshalJSON([]byte("\"1985-02-23 12:13:14\""))
|
||||
msg := time.Time(dateTime).String()
|
||||
if msg != want {
|
||||
t.Fatalf(`dateTime.UnmarshalJSON([]byte("\"1985-02-23 12:13:14\"") = %q, %v, want match for %#q, nil`, msg, err, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateTimeUnmarshalJSONMalformed(t *testing.T) {
|
||||
want := error(&time.ParseError{})
|
||||
var dateTime DateTime
|
||||
err := dateTime.UnmarshalJSON([]byte("\"1985/02/23 12-13-14\""))
|
||||
msg := time.Time(dateTime).String()
|
||||
if reflect.TypeOf(err) != reflect.TypeOf(want) {
|
||||
t.Fatalf(`dateTime.UnmarshalJSON([]byte("\"1985/02/23 12-13-14\"") = %q, %v, want match for %#q, nil`, msg, err, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateTimeMarshalJSON(t *testing.T) {
|
||||
want := "\"1985-02-23T12:13:14Z\""
|
||||
parsed, _ := time.Parse("2006-01-02 15:04:05", "1985-02-23 12:13:14")
|
||||
var dateTime DateTime = DateTime(parsed)
|
||||
msg, err := dateTime.MarshalJSON()
|
||||
if string(msg) != want {
|
||||
t.Fatalf(`dateTime.MarshalJSON() = %q, %v, want match for %#q, nil`, msg, err, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateTimeFormat(t *testing.T) {
|
||||
want := "Feb 23, 1985 12:13:14"
|
||||
parsed, _ := time.Parse("2006-01-02 15:04:05", "1985-02-23 12:13:14")
|
||||
var dateTime DateTime = DateTime(parsed)
|
||||
msg := dateTime.Format("Jan 2, 2006 15:04:05")
|
||||
if msg != want {
|
||||
t.Fatalf(`dateTime.Format("Jan 2, 2006 15:04:05") = %q, want match for %#q, nil`, msg, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateTimeScan(t *testing.T) {
|
||||
want := "0001-01-01T00:00:00Z"
|
||||
parsed, _ := time.Parse("2006-01-02 15:04:05", "1985-02-23 12:13:14")
|
||||
var dateTime DateTime = DateTime(parsed)
|
||||
err := dateTime.Scan(Date{})
|
||||
msg := dateTime.Format("2006-01-02T15:04:05Z07:00")
|
||||
if msg != want {
|
||||
t.Fatalf(`dateTime.Scan(Date{}) = %q, %v, want match for %#q, nil`, msg, err, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateTimeValue(t *testing.T) {
|
||||
var want time.Time
|
||||
parsed, _ := time.Parse("2006-01-02 15:04:05", "1985-02-23 12:13:14")
|
||||
var dateTime DateTime = DateTime(parsed)
|
||||
msg, _ := dateTime.Value()
|
||||
if reflect.TypeOf(msg) != reflect.TypeOf(want) {
|
||||
t.Fatalf(`dateTime.Value() = %q, want match for %#q, nil`, msg, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateTimeGormDateType(t *testing.T) {
|
||||
want := "datetime"
|
||||
parsed, _ := time.Parse("2006-01-02 15:04:05", "1985-02-23 12:13:14")
|
||||
var dateTime DateTime = DateTime(parsed)
|
||||
msg := dateTime.GormDataType()
|
||||
if msg != want {
|
||||
t.Fatalf(`dateTime.GormDateType() = %q, want match for %#q, nil`, msg, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateTimeGobEncode(t *testing.T) {
|
||||
want := "\x01\x00\x00\x00\x0e\x94\x0f\xcc\xda\x00\x00\x00\x00\xff\xff"
|
||||
parsed, _ := time.Parse("2006-01-02 15:04:05", "1985-02-23 12:13:14")
|
||||
var dateTime DateTime = DateTime(parsed)
|
||||
msg, err := dateTime.GobEncode()
|
||||
if string(msg) != want {
|
||||
t.Fatalf(`dateTime.GobEncode() = %q, %v, want match for %#q, nil`, msg, err, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateTimeGobDecode(t *testing.T) {
|
||||
want := "1985-02-23T12:13:14Z"
|
||||
parsed, _ := time.Parse("2006-01-02 15:04:05", "2006-01-02 12:13:14")
|
||||
var dateTime DateTime = DateTime(parsed)
|
||||
err := dateTime.GobDecode([]byte("\x01\x00\x00\x00\x0e\x94\x0f\xcc\xda\x00\x00\x00\x00\xff\xff"))
|
||||
msg := dateTime.Format("2006-01-02T15:04:05Z07:00")
|
||||
if string(msg) != want {
|
||||
t.Fatalf(`dateTime.GobDecode([]byte()) = %q, %v, want match for %#q, nil`, msg, err, want)
|
||||
}
|
||||
}
|
16
backend/utils/json.go
Normal file
16
backend/utils/json.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ErrorMessage struct {
|
||||
ErrorMessage string `json:"error_message"`
|
||||
}
|
||||
|
||||
func JSONErrorOutput(writer http.ResponseWriter, status int, msg string) {
|
||||
writer.Header().Set("Content-Type", "application/json")
|
||||
writer.WriteHeader(status)
|
||||
json.NewEncoder(writer).Encode(ErrorMessage{ErrorMessage: msg})
|
||||
}
|
52
backend/utils/jwt.go
Normal file
52
backend/utils/jwt.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
type JWTClaim struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func GenerateJWT(email string, username string) (tokenString string, err error) {
|
||||
expirationTime := time.Now().Add(1 * time.Hour)
|
||||
claims := &JWTClaim{
|
||||
Email: email,
|
||||
Username: username,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err = token.SignedString(os.Getenv("JWT_SECRET"))
|
||||
return
|
||||
}
|
||||
|
||||
func ValidateToken(signedToken string) (err error) {
|
||||
token, err := jwt.ParseWithClaims(
|
||||
signedToken,
|
||||
&JWTClaim{},
|
||||
func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(os.Getenv("JWT_SECRET")), nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
claims, ok := token.Claims.(*JWTClaim)
|
||||
if !ok {
|
||||
err = errors.New("couldn't parse claims")
|
||||
return
|
||||
}
|
||||
if claims.ExpiresAt.Unix() < jwt.NewNumericDate(time.Now().Local()).Unix() {
|
||||
err = errors.New("token expired")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
115
backend/utils/rut.go
Normal file
115
backend/utils/rut.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type RutType int8
|
||||
|
||||
const (
|
||||
Run RutType = iota
|
||||
Rut
|
||||
)
|
||||
|
||||
func (r RutType) String() string {
|
||||
switch r {
|
||||
case Run:
|
||||
return "RUN"
|
||||
case Rut:
|
||||
return "RUT"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func CleanRut(rut *string) {
|
||||
*rut = strings.ToUpper(*rut)
|
||||
*rut = strings.TrimSpace(*rut)
|
||||
*rut = strings.Replace(*rut, ".", "", -1)
|
||||
*rut = strings.Replace(*rut, "-", "", -1)
|
||||
}
|
||||
|
||||
func PrettyRut(rut *string) {
|
||||
tempRut := *rut
|
||||
verifier := strings.ToUpper(tempRut[len(tempRut)-1:])
|
||||
tempRut = tempRut[:len(tempRut)-1]
|
||||
tempRut = Reverse(tempRut)
|
||||
tempRut = InsertNth(tempRut, 3, '.')
|
||||
tempRut = Reverse(tempRut)
|
||||
tempRut = tempRut + "-" + verifier
|
||||
*rut = tempRut
|
||||
}
|
||||
|
||||
func IsValidRut(rut string) (bool, error) {
|
||||
// rut should be 8 or 9 characters
|
||||
if len(rut) != 8 && len(rut) != 9 {
|
||||
return false, errors.New("incorrect RUT length")
|
||||
}
|
||||
|
||||
verifier := strings.ToUpper(rut[len(rut)-1:])
|
||||
tempRut := rut[:len(rut)-1]
|
||||
|
||||
_, err := strconv.Atoi(verifier)
|
||||
if err != nil && verifier != "K" {
|
||||
return false, errors.New("invalid RUT identifier")
|
||||
}
|
||||
|
||||
generatedVerifier, err := generateVerifier(tempRut)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if verifier != generatedVerifier {
|
||||
return false, errors.New("incorrect RUT verifier")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func GetRutType(rut string) (RutType, error) {
|
||||
tempRut := rut[:len(rut)-1]
|
||||
numericRut, err := strconv.Atoi(tempRut)
|
||||
if err != nil {
|
||||
return Run, errors.New("invalid RUN/RUT")
|
||||
}
|
||||
|
||||
if numericRut < 100000000 && numericRut > 50000000 {
|
||||
return Rut, nil
|
||||
} else {
|
||||
return Run, nil
|
||||
}
|
||||
}
|
||||
|
||||
func generateVerifier(rut string) (string, error) {
|
||||
if _, err := strconv.Atoi(rut); err != nil {
|
||||
return "", errors.New("invalid RUT")
|
||||
}
|
||||
|
||||
var multiplier = 2
|
||||
var sum = 0
|
||||
var remainder int
|
||||
var division int
|
||||
var rutLength = len(rut)
|
||||
|
||||
for i := rutLength - 1; i >= 0; i-- {
|
||||
sum = sum + toInt(rut[i:i+1])*multiplier
|
||||
multiplier++
|
||||
if multiplier == 8 {
|
||||
multiplier = 2
|
||||
}
|
||||
}
|
||||
|
||||
division = sum / 11
|
||||
division = division * 11.0
|
||||
remainder = sum - int(division)
|
||||
|
||||
if remainder != 0 {
|
||||
remainder = 11 - remainder
|
||||
}
|
||||
|
||||
if remainder == 10 {
|
||||
return "K", nil
|
||||
} else {
|
||||
return strconv.Itoa(remainder), nil
|
||||
}
|
||||
}
|
138
backend/utils/rut_test.go
Normal file
138
backend/utils/rut_test.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRutTypeRun(t *testing.T) {
|
||||
typeTest := Run
|
||||
want := "RUN"
|
||||
msg := typeTest.String()
|
||||
if msg != want {
|
||||
t.Fatalf(`typeTest.String() = %q, want match for %#q, nil`, msg, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRutTypeRut(t *testing.T) {
|
||||
typeTest := Rut
|
||||
want := "RUT"
|
||||
msg := typeTest.String()
|
||||
if msg != want {
|
||||
t.Fatalf(`typeTest.String() = %q, want match for %#q, nil`, msg, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRutTypeUnknown(t *testing.T) {
|
||||
var typeTest RutType = 2
|
||||
want := "unknown"
|
||||
msg := typeTest.String()
|
||||
if msg != want {
|
||||
t.Fatalf(`typeTest.String() = %q, want match for %#q, nil`, msg, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanRut(t *testing.T) {
|
||||
want := "8675309K"
|
||||
msg := "8.675.309-k"
|
||||
CleanRut(&msg)
|
||||
if msg != want {
|
||||
t.Fatalf(`CleanRut(&msg) = %q, want match for %#q, nil`, msg, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrettyRut(t *testing.T) {
|
||||
want := "8.675.309-K"
|
||||
msg := "8675309k"
|
||||
PrettyRut(&msg)
|
||||
if msg != want {
|
||||
t.Fatalf(`PrettyRut(&msg) = %q, want match for %#q, nil`, msg, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateVerifier(t *testing.T) {
|
||||
want := "K"
|
||||
msg, err := generateVerifier("8675309")
|
||||
if msg != want {
|
||||
t.Fatalf(`generateVerifier("8675309") = %q, %v, want match for %#q, nil`, msg, err, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateVerifier2(t *testing.T) {
|
||||
want := "9"
|
||||
msg, err := generateVerifier("86753095")
|
||||
if msg != want {
|
||||
t.Fatalf(`generateVerifier("86753095") = %q, %v, want match for %q, nil`, msg, err, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateVerifierInvalidString(t *testing.T) {
|
||||
want := "invalid RUT"
|
||||
_, err := generateVerifier("8675f309")
|
||||
if err.Error() != want {
|
||||
t.Fatalf(`generateVerifier("8675f309") = %q, want %q, nil`, err.Error(), want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidRut(t *testing.T) {
|
||||
want := true
|
||||
msg, err := IsValidRut("8675309K")
|
||||
if msg != want {
|
||||
t.Fatalf(`IsValidRut("8675309K") = false, %v, want match for true, nil`, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidRutInvalid(t *testing.T) {
|
||||
want := false
|
||||
msg, err := IsValidRut("86753T99")
|
||||
if msg != want {
|
||||
t.Fatalf(`IsValidRut("86753T99") = true, %v, want match for false, nil`, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidRutInvalidIdentifier(t *testing.T) {
|
||||
want := false
|
||||
msg, err := IsValidRut("8675309C")
|
||||
if msg != want {
|
||||
t.Fatalf(`IsValidRut("8675309C") = true, %v, want match for false, nil`, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidRutIncorrectLength(t *testing.T) {
|
||||
want := false
|
||||
msg, err := IsValidRut("123234")
|
||||
if msg != want {
|
||||
t.Fatalf(`IsValidRut("123234") = true, %v, want match for false, nil`, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidRutIncorrectIdentifier(t *testing.T) {
|
||||
want := false
|
||||
msg, err := IsValidRut("86753096")
|
||||
if msg != want {
|
||||
t.Fatalf(`IsValidRut("86753096") = true, %v, want match for false, nil`, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRutTypeRun(t *testing.T) {
|
||||
want := Run
|
||||
msg, err := GetRutType("8675309K")
|
||||
if msg != want {
|
||||
t.Fatalf(`GetRutType("8675309") = %q, %v, want match for %#q, nil`, msg, err, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRutTypeRut(t *testing.T) {
|
||||
want := Rut
|
||||
msg, err := GetRutType("867530959")
|
||||
if msg != want {
|
||||
t.Fatalf(`GetRutType("867530959") = %q, %v, want match for %#q, nil`, msg, err, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRutTypeRutInvalid(t *testing.T) {
|
||||
want := "invalid RUN/RUT"
|
||||
_, err := GetRutType("8675f309")
|
||||
if err.Error() != want {
|
||||
t.Fatalf(`GetRutType("8675f309") = %q, want %q, nil`, err.Error(), want)
|
||||
}
|
||||
}
|
32
backend/utils/utils.go
Normal file
32
backend/utils/utils.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func toInt(toConvert string) int {
|
||||
converted, _ := strconv.Atoi(toConvert)
|
||||
return converted
|
||||
}
|
||||
|
||||
func Reverse(s string) string {
|
||||
r := []rune(s)
|
||||
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
|
||||
r[i], r[j] = r[j], r[i]
|
||||
}
|
||||
return string(r)
|
||||
}
|
||||
|
||||
func InsertNth(s string, n int, symbol rune) string {
|
||||
var buffer bytes.Buffer
|
||||
var n_1 = n - 1
|
||||
var l_1 = len(s) - 1
|
||||
for i, rune := range s {
|
||||
buffer.WriteRune(rune)
|
||||
if i%n == n_1 && i != l_1 {
|
||||
buffer.WriteRune(symbol)
|
||||
}
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
37
backend/utils/utils_test.go
Normal file
37
backend/utils/utils_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReverse(t *testing.T) {
|
||||
want := "8675309"
|
||||
msg := Reverse("9035768")
|
||||
if msg != want {
|
||||
t.Fatalf(`Reverse("9035768") = %q, want match for %#q, nil`, msg, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertNth(t *testing.T) {
|
||||
want := "867.530.9"
|
||||
msg := InsertNth("8675309", 3, '.')
|
||||
if msg != want {
|
||||
t.Fatalf(`InsertNth("8675309") = %q, want match for %#q, nil`, msg, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToInt(t *testing.T) {
|
||||
want := 123
|
||||
msg := toInt("123")
|
||||
if msg != want {
|
||||
t.Fatalf(`toInt("123") = %q, want match for %#q, nil`, msg, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToIntInvalid(t *testing.T) {
|
||||
want := 123
|
||||
msg := toInt("12f3")
|
||||
if msg == want {
|
||||
t.Fatalf(`toInt("12f3") = %q, want match for %#q, nil`, msg, want)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user