Enhance project functionality: add email template, implement email sending, and improve table management routes
The table management in the database does currently not work
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
qrank.db
|
||||
tmp
|
||||
tmp
|
||||
.env
|
||||
notes.txt
|
||||
@@ -26,7 +26,7 @@ body {
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.btn {
|
||||
.btn, button {
|
||||
display: inline-block;
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
|
||||
3
go.mod
3
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
|
||||
|
||||
6
go.sum
6
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=
|
||||
|
||||
8
other/email.txt
Normal file
8
other/email.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Hi {{ .Name }},
|
||||
|
||||
Welcome to QRank! Please confirm your login by clicking this link:
|
||||
|
||||
{{ .Token }}
|
||||
|
||||
Thanks,
|
||||
The QRank Team
|
||||
17
src/main.go
17
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)
|
||||
|
||||
@@ -67,5 +67,5 @@ type LoginToken struct {
|
||||
type Table struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
Slug string
|
||||
Slug string `gorm:"uniqueIndex;size:128"`
|
||||
}
|
||||
|
||||
123
src/routes.go
123
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)
|
||||
}
|
||||
|
||||
44
src/utils.go
44
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
|
||||
}
|
||||
|
||||
31
templates/table_status.html
Normal file
31
templates/table_status.html
Normal file
@@ -0,0 +1,31 @@
|
||||
{{define "content"}}
|
||||
<div class="card">
|
||||
<h2>Table "{{.CurrentSlug}}"</h2>
|
||||
|
||||
{{if .Full}}<b>This table is full <br></b>{{end}}
|
||||
|
||||
Currently there are {{.Table.PlayerCount}} Players playing on this table.
|
||||
|
||||
<ol>
|
||||
{{range .Table.Players}} <li>{{.Username}}</li> {{end}}
|
||||
</ol>
|
||||
|
||||
<form method="get" action="/table/{{.CurrentSlug}}">
|
||||
<button type="submit">Refresh</button>
|
||||
</form>
|
||||
|
||||
<p>The game can be finished when there are 2 or 4 players.</p>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<form method="post" action="/table/{{.CurrentSlug}}">
|
||||
<button style="background-color: greenyellow; border-color: gray;" type="submit">FinishGame</button>
|
||||
</form>
|
||||
<form method="post" action="/table/{{.CurrentSlug}}/reset">
|
||||
<button style="background-color: coral; border-color: gray;" type="submit">ResetGame</button>
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user