First commit
This commit is contained in:
commit
e7559f0bf1
7
.env.example
Normal file
7
.env.example
Normal 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
99
.gitignore
vendored
Normal 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
12
LICENSE
Normal 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
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)
|
||||
}
|
||||
}
|
66
docker-compose.yml
Normal file
66
docker-compose.yml
Normal 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
11
frontend/Dockerfile
Normal 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
5
frontend/babel.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
}
|
19
frontend/jsconfig.json
Normal file
19
frontend/jsconfig.json
Normal 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
51
frontend/package.json
Normal 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
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
18
frontend/public/index.html
Normal file
18
frontend/public/index.html
Normal 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
14
frontend/src/App.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<HelloWorld/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
HelloWorld
|
||||
}
|
||||
}
|
||||
</script>
|
BIN
frontend/src/assets/logo.png
Normal file
BIN
frontend/src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
58
frontend/src/components/HelloWorld.vue
Normal file
58
frontend/src/components/HelloWorld.vue
Normal 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
17
frontend/src/main.js
Normal 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
4
frontend/vue.config.js
Normal file
@ -0,0 +1,4 @@
|
||||
const { defineConfig } = require('@vue/cli-service')
|
||||
module.exports = defineConfig({
|
||||
transpileDependencies: true
|
||||
})
|
6171
frontend/yarn.lock
Normal file
6171
frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
39
nginx.conf
Normal file
39
nginx.conf
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user