First commit

This commit is contained in:
Chris Cromer 2022-06-29 21:26:05 -04:00
commit e7559f0bf1
Signed by: cromer
GPG Key ID: FA91071797BEEEC2
48 changed files with 8132 additions and 0 deletions

7
.env.example Normal file
View File

@ -0,0 +1,7 @@
MYSQL_DATABASE=alai
MYSQL_ROOT_PASSWORD=rootpass
MYSQL_USER=user
MYSQL_PASSWORD=pass
MYSQL_HOST=%
MYSQL_HOST_IP=database
MYSQL_PORT=3306

99
.gitignore vendored Normal file
View File

@ -0,0 +1,99 @@
# Game files
game/*
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
.env
backend/.env
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
# Mac
.DS_Store
.AppleDouble
.LSOverride
Icon
._*
# Windows
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
[Dd]esktop.ini
# Visual studio code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
.history/
*.vsix
*.code-workspace
# Sublime
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
*.sublime-workspace
sftp-config.json
sftp-config-alt*.json
Package Control.last-run
Package Control.ca-list
Package Control.ca-bundle
Package Control.system-ca-bundle
Package Control.cache/
Package Control.ca-certs/
Package Control.merged-ca-bundle
Package Control.user-ca-bundle
oscrypto-ca-bundle.crt
bh_unicode_properties.cache
GitHub.sublime-settings
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
backend/backend

12
LICENSE Normal file
View File

@ -0,0 +1,12 @@
Copyright 2022 Christopher Cromer
Copyright 2022 Martín Araneda Acuña
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

9
backend/.env.example Normal file
View 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
View 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
View 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
View 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)
}

View 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
View 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
View 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
View 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)
}
}

View 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
View 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
View 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
}

View 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
View 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
View 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"`
}

View 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"`
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}

View 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
View 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
View 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
View 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
View 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
View 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()
}

View 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)
}
}

66
docker-compose.yml Normal file
View File

@ -0,0 +1,66 @@
version: '3.7'
x-common-variables: &common-variables
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
services:
database:
image: mariadb:10.8.3
restart: on-failure
cap_add:
- SYS_NICE
volumes:
- database:/var/lib/mysql
networks:
- network
ports:
- "${MYSQL_PORT}:3306"
environment:
<<: *common-variables
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_ROOT_HOST: ${MYSQL_HOST}
nginx:
image: nginx:1.23.0
depends_on:
- backend
restart: on-failure
volumes:
- "./nginx.conf:/etc/nginx/conf.d/default.conf"
- "./game:/usr/share/nginx/html"
networks:
- network
ports:
- "4050:80"
frontend:
restart: on-failure
stdin_open: true
environment:
- CHOKIDAR_USEPOLLING=true
build:
dockerfile: Dockerfile
context: ./frontend
networks:
- network
backend:
restart: on-failure
build:
dockerfile: Dockerfile
context: "./backend"
depends_on:
- database
networks:
- network
environment:
<<: *common-variables
MYSQL_HOST_IP: ${MYSQL_HOST_IP}
volumes:
database:
networks:
network:

11
frontend/Dockerfile Normal file
View File

@ -0,0 +1,11 @@
FROM node:16-bullseye AS builder
WORKDIR /app
COPY . .
RUN yarn install
RUN yarn build
FROM node:16-bullseye
RUN yarn global add serve
WORKDIR /app
COPY --from=builder /app/dist .
CMD ["serve", "-p", "3000", "-s", "."]

5
frontend/babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

19
frontend/jsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

51
frontend/package.json Normal file
View File

@ -0,0 +1,51 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.27.2",
"core-js": "^3.8.3",
"primeicons": "^5.0.0",
"primevue": "^3.12.6",
"sass": "^1.52.1",
"sass-loader": "^13.0.0",
"vue": "^3.2.13",
"vue-router": "^4.0.15",
"vuex": "^4.0.2",
"webpack": "^5.72.1"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
<style> body { padding: 0 !important; margin: 0 !important; } </style>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

14
frontend/src/App.vue Normal file
View File

@ -0,0 +1,14 @@
<template>
<HelloWorld/>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
}
}
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,58 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

17
frontend/src/main.js Normal file
View File

@ -0,0 +1,17 @@
import { createApp } from 'vue';
import App from './App.vue';
import PrimeVue from 'primevue/config';
import 'primevue/resources/themes/mdc-light-indigo/theme.css';
import 'primevue/resources/primevue.min.css';
import 'primeicons/primeicons.css';
import Menubar from 'primevue/menubar';
import InputText from 'primevue/inputtext';
const app = createApp(App);
app.use(PrimeVue);
app.component('MenuBar', Menubar);
app.component('InputText', InputText);
app.mount('#app');

4
frontend/vue.config.js Normal file
View File

@ -0,0 +1,4 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true
})

6171
frontend/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

39
nginx.conf Normal file
View File

@ -0,0 +1,39 @@
upstream frontend {
server frontend:3000;
}
upstream backend {
server backend:3001;
keepalive 20;
}
server {
listen 80;
autoindex on;
root /usr/share/nginx/html;
index index.html index.php;
location / {
proxy_pass http://frontend;
}
location /sockjs-node {
proxy_pass http://frontend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
location /game {
absolute_redirect off;
rewrite ^([^.]*[^/])$ $1/ redirect;
alias /usr/share/nginx/html;
}
location /api/v1 {
rewrite /api/v1(?:/(.*))? /$1 break;
proxy_pass http://backend;
client_max_body_size 100M;
}
}