Files
qrank/src/utils.go
Jonas Hahn 01132e1968 Enhance project functionality: add email template, implement email sending, and improve table management routes
Also add qr code generation route

The table management in the database does currently not work
2025-08-25 16:49:23 +02:00

203 lines
4.3 KiB
Go

package main
import (
"bytes"
"crypto/rand"
"encoding/hex"
"errors"
"html/template"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/gin-gonic/gin"
"gopkg.in/gomail.v2"
"github.com/skip2/go-qrcode"
)
func mustRandToken(n int) string {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return hex.EncodeToString(b)
}
func now() time.Time {
return time.Now().UTC()
}
var adjectives = []string{"swift", "brave", "mighty", "cheeky", "sneaky", "zippy", "bouncy", "crispy", "fuzzy", "spicy", "snappy", "jazzy", "spry", "bold", "witty"}
var animals = []string{"otter", "panda", "falcon", "lynx", "badger", "tiger", "koala", "yak", "gecko", "eagle", "mamba", "fox", "yak", "whale", "rhino"}
func defaultUsername() string {
// Inspired from IO games: adjective-animal-xx
a := adjectives[int(time.Now().UnixNano())%len(adjectives)]
an := animals[int(time.Now().UnixNano()/17)%len(animals)]
return a + "-" + an + "-" + strings.ToLower(mustRandToken(2))
}
func slugify(s string) string {
s = strings.ToLower(s)
s = strings.TrimSpace(s)
s = strings.ReplaceAll(s, " ", "-")
// keep alnum and - only
var b strings.Builder
for _, r := range s {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
b.WriteRune(r)
}
}
out := b.String()
if out == "" {
out = "user-" + mustRandToken(2)
}
return out
}
func ensureUniqueUsernameAndSlug(u *User) error {
// try to keep username/slug unique by appending short token if needed
baseU := u.Username
baseS := slugify(u.Username)
for i := 0; i < 5; i++ {
candU := baseU
candS := baseS
if i > 0 {
suffix := "-" + mustRandToken(1)
candU = baseU + suffix
candS = baseS + suffix
}
var cnt int64
db.Model(&User{}).Where("username = ?", candU).Count(&cnt)
if cnt == 0 {
db.Model(&User{}).Where("slug = ?", candS).Count(&cnt)
if cnt == 0 {
u.Username = candU
u.Slug = candS
return nil
}
}
}
return errors.New("cannot create unique username/slug")
}
func userByEmail(email string) (*User, error) {
// Email must be lowercased and trimmed
var u User
tx := db.Where("email = ?", email).First(&u)
if tx.Error != nil {
return nil, tx.Error
}
return &u, nil
}
func userByName(name string) (*User, error) {
var u User
tx := db.Where("username = ?", name).First(&u)
if tx.Error != nil {
return nil, tx.Error
}
return &u, nil
}
func userBySlug(slug string) (*User, error) {
var u User
tx := db.Where("slug = ?", slug).First(&u)
if tx.Error != nil {
return nil, tx.Error
}
return &u, nil
}
func atoiSafe(s string) int {
var n int
for _, r := range s {
if r < '0' || r > '9' {
return 0
}
}
_, _ = fmtSscanf(s, "%d", &n)
return n
}
// minimal sscanf to avoid fmt import just for one call
func fmtSscanf(s, _ string, a *int) (int, error) {
// Only supports "%d"
n := 0
for i := 0; i < len(s); i++ {
c := s[i]
if c < '0' || c > '9' {
break
}
n = n*10 + int(c-'0')
}
*a = n
return 1, nil
}
func stripAfterDot(s string) string {
if idx := strings.Index(s, "."); idx != -1 {
return s[:idx]
}
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
}
func getQr(c *gin.Context) {
slug := c.Param("qrSlug")
var url string
if slug == "" {
url = "http://localhost:18765"
} else {
url = "http://localhost:18765/table/" + slug
}
png, err := qrcode.Encode(url, qrcode.Medium, 256)
if err != nil {
c.String(http.StatusInternalServerError, "could not generate QR")
return
}
c.Data(http.StatusOK, "image/png", png)
}