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:
Jonas Hahn
2025-08-25 16:43:24 +02:00
parent f5e5d5632d
commit a1cba9c4eb
10 changed files with 230 additions and 10 deletions

4
.gitignore vendored
View File

@@ -1,2 +1,4 @@
qrank.db
tmp
tmp
.env
notes.txt

View File

@@ -26,7 +26,7 @@ body {
flex-wrap: wrap
}
.btn {
.btn, button {
display: inline-block;
padding: 10px 14px;
border-radius: 10px;

3
go.mod
View File

@@ -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
View File

@@ -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
View File

@@ -0,0 +1,8 @@
Hi {{ .Name }},
Welcome to QRank! Please confirm your login by clicking this link:
{{ .Token }}
Thanks,
The QRank Team

View File

@@ -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)

View File

@@ -67,5 +67,5 @@ type LoginToken struct {
type Table struct {
gorm.Model
Name string
Slug string
Slug string `gorm:"uniqueIndex;size:128"`
}

View File

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

View File

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

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