diff --git a/.gitignore b/.gitignore index d839785..8e1bc32 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ qrank.db -tmp \ No newline at end of file +tmp +.env +notes.txt \ No newline at end of file diff --git a/assets/styles.css b/assets/styles.css index 0574157..faeac9e 100644 --- a/assets/styles.css +++ b/assets/styles.css @@ -26,7 +26,7 @@ body { flex-wrap: wrap } -.btn { +.btn, button { display: inline-block; padding: 10px 14px; border-radius: 10px; diff --git a/go.mod b/go.mod index 497b1dd..c789ba6 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -41,6 +42,8 @@ require ( golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.24.0 // indirect google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/postgres v1.6.0 // indirect gorm.io/driver/sqlite v1.6.0 // indirect diff --git a/go.sum b/go.sum index 7cc5ec0..33794f6 100644 --- a/go.sum +++ b/go.sum @@ -57,6 +57,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 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.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -128,7 +130,11 @@ google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFW google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/other/email.txt b/other/email.txt new file mode 100644 index 0000000..0c50bdf --- /dev/null +++ b/other/email.txt @@ -0,0 +1,8 @@ +Hi {{ .Name }}, + +Welcome to QRank! Please confirm your login by clicking this link: + +{{ .Token }} + +Thanks, +The QRank Team diff --git a/src/main.go b/src/main.go index c4fc4a7..22b6e76 100644 --- a/src/main.go +++ b/src/main.go @@ -9,6 +9,7 @@ import ( "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" + "github.com/joho/godotenv" "gorm.io/driver/sqlite" "gorm.io/gorm" ) @@ -34,9 +35,17 @@ const ( func main() { log.SetFlags(log.LstdFlags | log.Lshortfile) + var err error + err = godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + // Get the listening port port := os.Getenv("APP_PORT") - port = strconv.Itoa(defaultPort) + if port == "" { + port = strconv.Itoa(defaultPort) + } // Set the base URL baseURL = os.Getenv("APP_BASE_URL") @@ -45,7 +54,6 @@ func main() { } // Open connection to SQLite - var err error db, err = gorm.Open(sqlite.Open("qrank.db"), &gorm.Config{}) if err != nil { lg.Fatal("sqlite connect:", err) @@ -84,8 +92,9 @@ func main() { authorized.POST("/enter", postEnter) // QR-prepped table routes - authorized.GET("/table/:tableSlug", getEnter) - authorized.POST("/table/:tableSlug", postEnter) + authorized.GET("/table/:tableSlug", getTable) + authorized.POST("/table/:tableSlug", postTable) + authorized.POST("/table/:tableSlug/reset", postTableReset) authorized.GET("/history", getHistory) authorized.GET("/leaderboard", getLeaderboard) diff --git a/src/models.go b/src/models.go index bbb31e6..e0376e1 100644 --- a/src/models.go +++ b/src/models.go @@ -67,5 +67,5 @@ type LoginToken struct { type Table struct { gorm.Model Name string - Slug string + Slug string `gorm:"uniqueIndex;size:128"` } diff --git a/src/routes.go b/src/routes.go index c889c19..ffba073 100644 --- a/src/routes.go +++ b/src/routes.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "errors" "fmt" "log" @@ -9,11 +10,30 @@ import ( "strings" "time" + "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "gorm.io/gorm" ) func getEnter(c *gin.Context) { + sess := sessions.Default(c) + tableSlug := sess.Get("TableSlug") + + var slug string + if tableSlug != nil { + slug = tableSlug.(string) + + } + + if slug != "" { + + var table *Table + db.Model(&Table{}).Where("slug = ?", slug).First(&table) + + tm.Render(c, "enter", gin.H{"Table": table}) + return + + } tm.Render(c, "enter", gin.H{}) } @@ -24,6 +44,16 @@ func postEnter(c *gin.Context) { return } + sess := sessions.Default(c) + tableSlug := sess.Get("TableSlug") + + var slug string + if tableSlug != nil { + slug = tableSlug.(string) + + } + sess.Delete("TableSlug") + // Parse form a1h := strings.TrimSpace(c.PostForm("a1")) a2h := strings.TrimSpace(c.PostForm("a2")) @@ -96,9 +126,18 @@ func postEnter(c *gin.Context) { } // Create game - g := Game{ - ScoreA: scoreA, - ScoreB: scoreB, + var g Game + if slug != "" { + g = Game{ + ScoreA: scoreA, + ScoreB: scoreB, + Table: &Table{Slug: slug}, + } + } else { + g = Game{ + ScoreA: scoreA, + ScoreB: scoreB, + } } if err := db.Create(&g).Error; err != nil { c.String(http.StatusInternalServerError, "game create error") @@ -304,6 +343,10 @@ func postLogin(c *gin.Context) { link := strings.TrimRight(baseURL, "/") + "/magic?token=" + token log.Printf("[MAGIC LINK] %s for %s (valid until %s)\n", link, email, lt.ExpiresAt.Format(time.RFC3339)) + if err := sendEmail(email, link); err != nil { + c.Status(500) + } + tm.Render(c, "sent", gin.H{"Email": email}) } @@ -335,3 +378,77 @@ func getMagic(c *gin.Context) { c.Redirect(302, "/enter") } + +type TableInfo struct { + Players map[uint]*User + PlayerCount int +} + +var tables = make(map[string]*TableInfo) + +func getTable(c *gin.Context) { + slug := c.Param("tableSlug") + u := getSessionUser(c) + if tables[slug] == nil { + tables[slug] = &TableInfo{} + } + + table := tables[slug] + if tables[slug].Players == nil { + tables[slug].Players = make(map[uint]*User) + } + + if table.PlayerCount >= 4 { + tm.Render(c, "table_status", gin.H{"Full": "true", "CurrentSlug": slug, "Table": tables[slug]}) + return + } + + table.Players[u.ID] = u + table.PlayerCount = len(table.Players) + + tm.Render(c, "table_status", gin.H{"CurrentSlug": slug, "Table": tables[slug]}) +} + +func postTable(c *gin.Context) { + slug := c.Param("tableSlug") + table := tables[slug] + session := sessions.Default(c) + + if len(table.Players) == 2 || len(table.Players) == 4 { + + // Pretend we got some values from somewhere else + var count int + tags := []string{"a1", "b1", "a2", "b2"} + formData := make(map[string]string) + for _, value := range table.Players { + formData[tags[count]] = value.Username + count++ + } + if b, err := json.Marshal(formData); err == nil { + session.Set("form_data", string(b)) + _ = session.Save() + } + + session.Set("TableSlug", slug) + _ = session.Save() + + // Reset the table + tables[slug] = &TableInfo{} + + db.Save(&Table{Slug: slug}) + + c.Redirect(302, "/enter") + return + + } + SetMessage(c, "Wrong amount of players!") + c.Redirect(302, "/table/"+slug) + +} + +func postTableReset(c *gin.Context) { + slug := c.Param("tableSlug") + tables[slug] = &TableInfo{} + SetMessage(c, "Resettet the table!") + c.Redirect(302, "/table/"+slug) +} diff --git a/src/utils.go b/src/utils.go index b14db76..dd6e46b 100644 --- a/src/utils.go +++ b/src/utils.go @@ -1,11 +1,18 @@ package main import ( + "bytes" "crypto/rand" "encoding/hex" "errors" + "html/template" + "log" + "os" "strings" "time" + + "github.com/gin-gonic/gin" + "gopkg.in/gomail.v2" ) func mustRandToken(n int) string { @@ -134,3 +141,40 @@ func stripAfterDot(s string) string { } return s } + +func nameFromEmail(email string) string { + parts := strings.SplitN(email, "@", 2) + if len(parts) > 0 { + return parts[0] + } + return "" +} + +func sendEmail(email string, token string) error { + // Parse the email body template + tmpl, err := template.ParseFiles("other/email.txt") + if err != nil { + panic(err) + } + + // Execute template with provided data + var body bytes.Buffer + if err := tmpl.Execute(&body, gin.H{"Token": token, "Name": nameFromEmail(email)}); err != nil { + panic(err) + } + + // Compose email + m := gomail.NewMessage() + m.SetHeader("From", os.Getenv("SMTP_MAIL")) + m.SetHeader("To", email) + m.SetHeader("Subject", "Registration/Login to QRank") + m.SetBody("text/plain", body.String()) + + // Dial and send + d := gomail.NewDialer(os.Getenv("SMTP_HOST"), 587, os.Getenv("SMTP_MAIL"), os.Getenv("SMTP_PASS")) + if err := d.DialAndSend(m); err != nil { + log.Fatalln(err) + return err + } + return nil +} diff --git a/templates/table_status.html b/templates/table_status.html new file mode 100644 index 0000000..459b8bc --- /dev/null +++ b/templates/table_status.html @@ -0,0 +1,31 @@ +{{define "content"}} +
The game can be finished when there are 2 or 4 players.
+ +