Refactor project structure and update configurations. Now first working version
- Updated `.air.conf` for Nix compatibility and simplified build commands. - Enhanced `.gitignore` to include `tmp` directory. - Improved `README.md` with clearer instructions and added language details. - Refined CSS styles for better UI consistency and added alert styles. - Upgraded `flake.nix` to use Go 1.24 and improved shell environment setup. - Modified authentication logic in `auth.go` for better user handling. - Updated `main.go` to dynamically set the listening port and improved logging. - Added new `routes.go` file for handling game entry and history. - Enhanced user models and added statistics tracking in `models.go`. - Improved template rendering and added user feedback messages in HTML templates. - Removed obsolete build error logs and binaries.
This commit is contained in:
37
src/auth.go
37
src/auth.go
@@ -33,8 +33,8 @@ func requireAuth() gin.HandlerFunc {
|
||||
}
|
||||
|
||||
func setSessionCookie(c *gin.Context, token string) {
|
||||
// Very long-lived cookie (~10 years)
|
||||
maxAge := 10 * 365 * 24 * 60 * 60
|
||||
// Very long-lived cookie for one year
|
||||
maxAge := 365 * 24 * 60 * 60
|
||||
httpOnly := true
|
||||
secure := strings.HasPrefix(baseURL, "https://")
|
||||
sameSite := http.SameSiteLaxMode
|
||||
@@ -43,36 +43,23 @@ func setSessionCookie(c *gin.Context, token string) {
|
||||
c.Header("Set-Cookie", (&http.Cookie{Name: "session", Value: token, Path: "/", Domain: cookieDomain, MaxAge: maxAge, Secure: secure, HttpOnly: httpOnly, SameSite: sameSite}).String())
|
||||
}
|
||||
|
||||
func findOrCreateUserByHandle(handle string) (*User, error) {
|
||||
func findUserByHandle(handle string) (*User, error) {
|
||||
h := strings.TrimSpace(handle)
|
||||
if h == "" {
|
||||
return nil, sql.ErrNoRows
|
||||
}
|
||||
if strings.Contains(h, "@") { // email
|
||||
|
||||
// Return user by email or username
|
||||
if strings.Contains(h, "@") {
|
||||
if u, err := userByEmail(strings.ToLower(h)); err == nil {
|
||||
return u, nil
|
||||
}
|
||||
// create
|
||||
u := &User{Email: strings.ToLower(h), Username: defaultUsername()}
|
||||
if err := ensureUniqueUsernameAndSlug(u); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
// Find and return the user
|
||||
if u, err := userByName(h); err == nil {
|
||||
return u, nil
|
||||
}
|
||||
if err := db.Create(u).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
// username
|
||||
var u User
|
||||
if err := db.Where("username = ?", h).First(&u).Error; err == nil {
|
||||
return &u, nil
|
||||
}
|
||||
// If not found, create placeholder user with random email-like token
|
||||
u = User{Email: mustRandToken(3) + "+placeholder@local", Username: h, Slug: slugify(h)}
|
||||
if err := ensureUniqueUsernameAndSlug(&u); err != nil { /* best-effort */
|
||||
}
|
||||
if err := db.Create(&u).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &u, nil
|
||||
|
||||
return nil, sql.ErrNoRows
|
||||
}
|
||||
|
||||
51
src/main.go
51
src/main.go
@@ -4,8 +4,9 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -16,46 +17,44 @@ var (
|
||||
db *gorm.DB
|
||||
baseURL string
|
||||
cookieDomain string
|
||||
|
||||
// Global logger for this package
|
||||
lg = log.Default()
|
||||
)
|
||||
|
||||
const (
|
||||
PORT = "18765"
|
||||
// Default port if APP_PORT is not set
|
||||
defaultPort = 18765
|
||||
)
|
||||
|
||||
var logger = log.Default()
|
||||
|
||||
// ===================== Main =====================
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
|
||||
// Get the listening port
|
||||
port := os.Getenv("APP_PORT")
|
||||
port = strconv.Itoa(defaultPort)
|
||||
|
||||
// Set the base URL
|
||||
baseURL = os.Getenv("APP_BASE_URL")
|
||||
if baseURL == "" {
|
||||
baseURL = "http://localhost:" + PORT
|
||||
baseURL = "http://localhost:" + port
|
||||
}
|
||||
cookieDomain = os.Getenv("APP_COOKIE_DOMAIN")
|
||||
|
||||
// DB connect
|
||||
// Open connection to SQLite
|
||||
var err error
|
||||
dsn := os.Getenv("DATABASE_URL")
|
||||
if dsn != "" {
|
||||
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatal("postgres connect:", err)
|
||||
}
|
||||
log.Println("using Postgres")
|
||||
} else {
|
||||
db, err = gorm.Open(sqlite.Open("qrank.db"), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatal("sqlite connect:", err)
|
||||
}
|
||||
log.Println("using SQLite qrank.db")
|
||||
db, err = gorm.Open(sqlite.Open("qrank.db"), &gorm.Config{})
|
||||
if err != nil {
|
||||
lg.Fatal("sqlite connect:", err)
|
||||
}
|
||||
lg.Println("using SQLite qrank.db")
|
||||
|
||||
if err := db.AutoMigrate(&User{}, &Session{}, &LoginToken{}, &Table{}, &Game{}, &GamePlayer{}); err != nil {
|
||||
log.Fatal("migrate:", err)
|
||||
lg.Fatal("migrate:", err)
|
||||
}
|
||||
|
||||
// Create engine
|
||||
r := gin.Default()
|
||||
|
||||
// Serve static files from the current directory
|
||||
@@ -82,12 +81,10 @@ func main() {
|
||||
r.GET("/me", requireAuth(), getMe)
|
||||
r.POST("/me", requireAuth(), postMe)
|
||||
|
||||
bind := os.Getenv("APP_BIND")
|
||||
if bind == "" {
|
||||
bind = ":" + PORT
|
||||
}
|
||||
log.Println("listening on", bind, "base:", baseURL)
|
||||
// Start application with port
|
||||
bind := ":" + port
|
||||
lg.Println("listening on", bind, "base:", baseURL)
|
||||
if err := r.Run(bind); err != nil {
|
||||
log.Fatal(err)
|
||||
lg.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// One knows that the user is active when there exists a session for the user
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
CreatedAt time.Time
|
||||
@@ -14,13 +15,13 @@ type User struct {
|
||||
Slug string `gorm:"uniqueIndex;size:80"`
|
||||
}
|
||||
|
||||
// Currently no expiry
|
||||
type Session struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
CreatedAt time.Time
|
||||
// No expiry by default; can add later
|
||||
Token string `gorm:"uniqueIndex;size:128"`
|
||||
UserID uint `gorm:"index"`
|
||||
User User
|
||||
Token string `gorm:"uniqueIndex;size:128"`
|
||||
UserID uint `gorm:"index"`
|
||||
User User
|
||||
}
|
||||
|
||||
type LoginToken struct {
|
||||
@@ -46,6 +47,7 @@ type Game struct {
|
||||
Table *Table
|
||||
ScoreA int
|
||||
ScoreB int
|
||||
WinnerIsA bool
|
||||
}
|
||||
|
||||
type GamePlayer struct {
|
||||
@@ -55,3 +57,10 @@ type GamePlayer struct {
|
||||
Side string `gorm:"size:1;index"` // "A" or "B"
|
||||
User User
|
||||
}
|
||||
|
||||
// Also house the common models that are not in the database here
|
||||
type stats struct {
|
||||
Games,
|
||||
Wins,
|
||||
Losses int
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -10,125 +12,86 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func getIndex(c *gin.Context) {
|
||||
if getSessionUser(c) != nil {
|
||||
c.Redirect(302, "/enter")
|
||||
return
|
||||
}
|
||||
getLogin(c)
|
||||
}
|
||||
|
||||
func getLogin(c *gin.Context) {
|
||||
tm.Render(c, "login", gin.H{})
|
||||
}
|
||||
|
||||
func postLogin(c *gin.Context) {
|
||||
email := strings.ToLower(strings.TrimSpace(c.PostForm("email")))
|
||||
if email == "" {
|
||||
c.Redirect(302, "/login")
|
||||
return
|
||||
}
|
||||
|
||||
// ensure user exists
|
||||
var u User
|
||||
tx := db.Where("email = ?", email).First(&u)
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
u = User{Email: email, Username: defaultUsername()}
|
||||
if err := ensureUniqueUsernameAndSlug(&u); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
if err := db.Create(&u).Error; err != nil {
|
||||
log.Println("create user:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// create token valid 30 days
|
||||
token := mustRandToken(24)
|
||||
lt := LoginToken{Token: token, Email: email, ExpiresAt: now().Add(30 * 24 * time.Hour)}
|
||||
if err := db.Create(<).Error; err != nil {
|
||||
log.Println("create token:", err)
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
tm.Render(c, "sent", gin.H{"Email": email})
|
||||
}
|
||||
|
||||
func getMagic(c *gin.Context) {
|
||||
token := c.Query("token")
|
||||
if token == "" {
|
||||
c.Redirect(302, "/login")
|
||||
return
|
||||
}
|
||||
var lt LoginToken
|
||||
if err := db.Where("token = ? AND expires_at > ?", token, now()).First(<).Error; err != nil {
|
||||
c.String(400, "Invalid or expired token")
|
||||
return
|
||||
}
|
||||
// find or create user by email again (in case)
|
||||
u, err := userByEmail(lt.Email)
|
||||
if err != nil {
|
||||
c.String(500, "user lookup error")
|
||||
return
|
||||
}
|
||||
|
||||
// create session
|
||||
sessTok := mustRandToken(24)
|
||||
if err := db.Create(&Session{Token: sessTok, UserID: u.ID}).Error; err != nil {
|
||||
c.String(500, "session error")
|
||||
return
|
||||
}
|
||||
setSessionCookie(c, sessTok)
|
||||
|
||||
c.Redirect(302, "/enter")
|
||||
}
|
||||
|
||||
func getEnter(c *gin.Context) {
|
||||
var table *Table
|
||||
if slug := c.Param("tslug"); slug != "" {
|
||||
var t Table
|
||||
if err := db.Where("slug = ?", slug).First(&t).Error; err == nil {
|
||||
table = &t
|
||||
}
|
||||
}
|
||||
post := "/enter"
|
||||
if table != nil {
|
||||
post = "/t/" + table.Slug + "/enter"
|
||||
}
|
||||
tm.Render(c, "enter", gin.H{"PostAction": post, "Table": table})
|
||||
// Simple render for now
|
||||
tm.Render(c, "enter", gin.H{})
|
||||
}
|
||||
|
||||
func postEnter(c *gin.Context) {
|
||||
current := getSessionUser(c)
|
||||
if current == nil {
|
||||
c.Redirect(302, "/login")
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
return
|
||||
}
|
||||
|
||||
a1h := c.PostForm("a1")
|
||||
a2h := c.PostForm("a2")
|
||||
b1h := c.PostForm("b1")
|
||||
b2h := c.PostForm("b2")
|
||||
// Parse form
|
||||
a1h := strings.TrimSpace(c.PostForm("a1"))
|
||||
a2h := strings.TrimSpace(c.PostForm("a2"))
|
||||
b1h := strings.TrimSpace(c.PostForm("b1"))
|
||||
b2h := strings.TrimSpace(c.PostForm("b2"))
|
||||
scoreA := atoiSafe(c.PostForm("scoreA"))
|
||||
scoreB := atoiSafe(c.PostForm("scoreB"))
|
||||
|
||||
// Resolve players (default A1 to current if empty)
|
||||
if strings.TrimSpace(a1h) == "" {
|
||||
a1h = current.Username
|
||||
}
|
||||
a1, _ := findOrCreateUserByHandle(a1h)
|
||||
var a2, b1, b2 *User
|
||||
if a2h != "" {
|
||||
a2, _ = findOrCreateUserByHandle(a2h)
|
||||
}
|
||||
if b1h != "" {
|
||||
b1, _ = findOrCreateUserByHandle(b1h)
|
||||
}
|
||||
if b2h != "" {
|
||||
b2, _ = findOrCreateUserByHandle(b2h)
|
||||
// Require a winner
|
||||
if scoreA == scoreB {
|
||||
tm.Render(c, "enter", gin.H{"Error": "Score A score must be different from score B",
|
||||
"a1": a1h, "a2": a2h, "b1": b1h, "b2": b2h, "scoreA": scoreA, "scoreB": scoreB})
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve players
|
||||
resolveUser := func(handle string) (*User, error) {
|
||||
if handle == "" {
|
||||
return nil, nil
|
||||
}
|
||||
u, err := findUserByHandle(handle)
|
||||
if err != nil {
|
||||
tm.Render(c, "enter", gin.H{"Error": fmt.Sprintf("User %q not found", handle),
|
||||
"a1": a1h, "a2": a2h, "b1": b1h, "b2": b2h, "scoreA": scoreA, "scoreB": scoreB})
|
||||
return nil, err
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
var a1, a2, b1, b2 *User
|
||||
|
||||
// Always ensure A1 exists, fallback to current if empty
|
||||
var err error
|
||||
if a1, err = resolveUser(a1h); err != nil {
|
||||
return
|
||||
}
|
||||
if a2, err = resolveUser(a2h); err != nil {
|
||||
return
|
||||
}
|
||||
if b1, err = resolveUser(b1h); err != nil {
|
||||
return
|
||||
}
|
||||
if b2, err = resolveUser(b2h); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check for at least one player on each side
|
||||
if b1 == nil && b2 == nil || a1 == nil && a2 == nil {
|
||||
tm.Render(c, "enter", gin.H{"Error": "At least one player required on each side",
|
||||
"a1": a1h, "a2": a2h, "b1": b1h, "b2": b2h, "scoreA": scoreA, "scoreB": scoreB})
|
||||
return
|
||||
}
|
||||
|
||||
// Check for duplicate users
|
||||
seen := map[uint]bool{}
|
||||
users := []*User{a1, a2, b1, b2}
|
||||
for i, u := range users {
|
||||
if u != nil {
|
||||
if seen[u.ID] {
|
||||
tm.Render(c, "enter", gin.H{"Error": fmt.Sprintf("User %q specified multiple times", users[i].Username),
|
||||
"a1": a1h, "a2": a2h, "b1": b1h, "b2": b2h, "scoreA": scoreA, "scoreB": scoreB})
|
||||
return
|
||||
}
|
||||
seen[u.ID] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Look up table if provided
|
||||
var tableID *uint
|
||||
if slug := c.Param("tslug"); slug != "" {
|
||||
var t Table
|
||||
@@ -137,42 +100,49 @@ func postEnter(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
g := Game{ScoreA: scoreA, ScoreB: scoreB, TableID: tableID}
|
||||
// Create game
|
||||
g := Game{
|
||||
ScoreA: scoreA,
|
||||
ScoreB: scoreB,
|
||||
TableID: tableID,
|
||||
WinnerIsA: scoreA > scoreB,
|
||||
}
|
||||
if err := db.Create(&g).Error; err != nil {
|
||||
c.String(500, "game create error")
|
||||
c.String(http.StatusInternalServerError, "game create error")
|
||||
return
|
||||
}
|
||||
|
||||
// Collect players by side
|
||||
players := []struct {
|
||||
U *User
|
||||
S string
|
||||
}{{a1, "A"}}
|
||||
if a2 != nil {
|
||||
players = append(players, struct {
|
||||
U *User
|
||||
S string
|
||||
}{a2, "A"})
|
||||
}
|
||||
if b1 != nil {
|
||||
players = append(players, struct {
|
||||
U *User
|
||||
S string
|
||||
}{b1, "B"})
|
||||
}
|
||||
if b2 != nil {
|
||||
players = append(players, struct {
|
||||
U *User
|
||||
S string
|
||||
}{b2, "B"})
|
||||
u *User
|
||||
side string
|
||||
}{
|
||||
{a1, "A"},
|
||||
{a2, "A"},
|
||||
{b1, "B"},
|
||||
{b2, "B"},
|
||||
}
|
||||
|
||||
for _, p := range players {
|
||||
if p.U != nil {
|
||||
db.Create(&GamePlayer{GameID: g.ID, UserID: p.U.ID, Side: p.S})
|
||||
if p.u != nil {
|
||||
if err := db.Create(&GamePlayer{GameID: g.ID, UserID: p.u.ID, Side: p.side}).Error; err != nil {
|
||||
c.String(http.StatusInternalServerError, "failed to assign player")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.Redirect(302, "/history")
|
||||
c.Redirect(http.StatusFound, "/history")
|
||||
}
|
||||
|
||||
func getIndex(c *gin.Context) {
|
||||
if u := getSessionUser(c); u != nil {
|
||||
c.Redirect(302, "/enter")
|
||||
return
|
||||
}
|
||||
getLogin(c)
|
||||
}
|
||||
|
||||
func getHistory(c *gin.Context) {
|
||||
// Load recent games with players
|
||||
var games []Game
|
||||
@@ -274,53 +244,21 @@ func getUserView(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Compute stats
|
||||
type Stats struct{ Games, Wins, Losses int }
|
||||
st := Stats{}
|
||||
var gps []GamePlayer
|
||||
db.Where("user_id = ?", u.ID).Find(&gps)
|
||||
gameMap := map[uint]string{}
|
||||
for _, gp := range gps {
|
||||
gameMap[gp.GameID] = gp.Side
|
||||
}
|
||||
|
||||
if len(gameMap) > 0 {
|
||||
var games []Game
|
||||
ids := make([]uint, 0, len(gameMap))
|
||||
for gid := range gameMap {
|
||||
ids = append(ids, gid)
|
||||
}
|
||||
db.Find(&games, ids)
|
||||
for _, g := range games {
|
||||
st.Games++
|
||||
if g.ScoreA == g.ScoreB {
|
||||
continue
|
||||
}
|
||||
if g.ScoreA > g.ScoreB && gameMap[g.ID] == "A" {
|
||||
st.Wins++
|
||||
} else if g.ScoreB > g.ScoreA && gameMap[g.ID] == "B" {
|
||||
st.Wins++
|
||||
} else {
|
||||
st.Losses++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
own := false
|
||||
if cu := getSessionUser(c); cu != nil && cu.ID == u.ID {
|
||||
own = true
|
||||
}
|
||||
|
||||
tm.Render(c, "user", gin.H{"Viewed": u, "Stats": st, "Own": own})
|
||||
tm.Render(c, "user", gin.H{"Viewed": u, "Stats": getStatsFromUser(u), "Own": own})
|
||||
}
|
||||
|
||||
func getMe(c *gin.Context) {
|
||||
cu := getSessionUser(c)
|
||||
if cu == nil {
|
||||
u := getSessionUser(c)
|
||||
if u == nil {
|
||||
c.Redirect(302, "/login")
|
||||
return
|
||||
}
|
||||
tm.Render(c, "user", gin.H{"Viewed": cu, "Stats": struct{ Games, Wins, Losses int }{0, 0, 0}, "Own": true})
|
||||
tm.Render(c, "user", gin.H{"Viewed": u, "Stats": getStatsFromUser(u), "Own": true})
|
||||
}
|
||||
|
||||
func postMe(c *gin.Context) {
|
||||
@@ -330,10 +268,12 @@ func postMe(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
newU := strings.TrimSpace(c.PostForm("username"))
|
||||
if newU == "" {
|
||||
if newU == "" || newU == cu.Username {
|
||||
c.Redirect(302, "/me")
|
||||
return
|
||||
}
|
||||
|
||||
// Update username and slug
|
||||
cu.Username = newU
|
||||
cu.Slug = slugify(newU)
|
||||
if err := ensureUniqueUsernameAndSlug(cu); err != nil {
|
||||
@@ -342,5 +282,71 @@ func postMe(c *gin.Context) {
|
||||
if err := db.Save(cu).Error; err != nil {
|
||||
log.Println("save user:", err)
|
||||
}
|
||||
c.Redirect(302, "/u/"+cu.Slug)
|
||||
c.Redirect(302, "/me")
|
||||
}
|
||||
|
||||
func getLogin(c *gin.Context) {
|
||||
tm.Render(c, "login", gin.H{})
|
||||
}
|
||||
|
||||
func postLogin(c *gin.Context) {
|
||||
email := strings.ToLower(strings.TrimSpace(c.PostForm("email")))
|
||||
if email == "" {
|
||||
c.Redirect(302, "/login")
|
||||
return
|
||||
}
|
||||
|
||||
// ensure user exists
|
||||
var u User
|
||||
tx := db.Where("email = ?", email).First(&u)
|
||||
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
||||
u = User{Email: email, Username: defaultUsername()}
|
||||
if err := ensureUniqueUsernameAndSlug(&u); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
if err := db.Create(&u).Error; err != nil {
|
||||
log.Println("create user:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// create token valid 30 days
|
||||
token := mustRandToken(24)
|
||||
lt := LoginToken{Token: token, Email: email, ExpiresAt: now().Add(30 * 24 * time.Hour)}
|
||||
if err := db.Create(<).Error; err != nil {
|
||||
log.Println("create token:", err)
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
tm.Render(c, "sent", gin.H{"Email": email})
|
||||
}
|
||||
|
||||
func getMagic(c *gin.Context) {
|
||||
token := c.Query("token")
|
||||
if token == "" {
|
||||
c.Redirect(302, "/login")
|
||||
return
|
||||
}
|
||||
var lt LoginToken
|
||||
if err := db.Where("token = ? AND expires_at > ?", token, now()).First(<).Error; err != nil {
|
||||
c.String(400, "Invalid or expired token")
|
||||
return
|
||||
}
|
||||
// find or create user by email again (in case)
|
||||
u, err := userByEmail(lt.Email)
|
||||
if err != nil {
|
||||
c.String(500, "user lookup error")
|
||||
return
|
||||
}
|
||||
|
||||
// create session
|
||||
sessTok := mustRandToken(24)
|
||||
if err := db.Create(&Session{Token: sessTok, UserID: u.ID}).Error; err != nil {
|
||||
c.String(500, "session error")
|
||||
return
|
||||
}
|
||||
setSessionCookie(c, sessTok)
|
||||
|
||||
c.Redirect(302, "/enter")
|
||||
}
|
||||
@@ -30,14 +30,20 @@ func NewTemplateManager(dir string, base string, ext string) *TemplateManager {
|
||||
tm := &TemplateManager{
|
||||
templates: make(map[string]*template.Template),
|
||||
funcs: template.FuncMap{
|
||||
// Attach functions to the templates here
|
||||
"fmtTime": func(t time.Time) string {
|
||||
return t.Local().Format("2006-01-02 15:04") // Go’s reference time
|
||||
},
|
||||
"cmpTwo": func(a, b int) bool {
|
||||
return a == b
|
||||
},
|
||||
},
|
||||
base: base,
|
||||
dir: dir,
|
||||
ext: ext,
|
||||
}
|
||||
|
||||
// Populate template manager
|
||||
tm.LoadTemplates()
|
||||
|
||||
return tm
|
||||
@@ -58,7 +64,7 @@ func (tm *TemplateManager) LoadTemplates() {
|
||||
}
|
||||
|
||||
name := stripAfterDot(filepath.Base(file))
|
||||
logger.Println(name)
|
||||
lg.Println(name)
|
||||
|
||||
// Parse base + view template together
|
||||
tpl, err := template.New(name).
|
||||
|
||||
56
src/utils.go
56
src/utils.go
@@ -16,12 +16,15 @@ func mustRandToken(n int) string {
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func now() time.Time { return time.Now().UTC() }
|
||||
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 {
|
||||
// io-game style: adjective-animal-xxxx
|
||||
adjectives := []string{"swift", "brave", "mighty", "cheeky", "sneaky", "zippy", "bouncy", "crispy", "fuzzy", "spicy", "snappy", "jazzy", "spry", "bold", "witty"}
|
||||
animals := []string{"otter", "panda", "falcon", "lynx", "badger", "tiger", "koala", "yak", "gecko", "eagle", "mamba", "fox", "yak", "whale", "rhino"}
|
||||
// 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))
|
||||
@@ -72,8 +75,18 @@ func ensureUniqueUsernameAndSlug(u *User) error {
|
||||
}
|
||||
|
||||
func userByEmail(email string) (*User, error) {
|
||||
// Email must be lowercased and trimmed
|
||||
var u User
|
||||
tx := db.Where("email = ?", strings.ToLower(email)).First(&u)
|
||||
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
|
||||
}
|
||||
@@ -121,3 +134,36 @@ func stripAfterDot(s string) string {
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func getStatsFromUser(u *User) stats {
|
||||
var gps []GamePlayer
|
||||
var games, wins, losses int
|
||||
db.Where("user_id = ?", u.ID).Find(&gps)
|
||||
gameMap := map[uint]string{}
|
||||
for _, gp := range gps {
|
||||
gameMap[gp.GameID] = gp.Side
|
||||
}
|
||||
|
||||
if len(gameMap) > 0 {
|
||||
var gamesL []Game
|
||||
ids := make([]uint, 0, len(gameMap))
|
||||
for gid := range gameMap {
|
||||
ids = append(ids, gid)
|
||||
}
|
||||
db.Find(&gamesL, ids)
|
||||
for _, g := range gamesL {
|
||||
games++
|
||||
if g.ScoreA == g.ScoreB {
|
||||
continue
|
||||
}
|
||||
if g.ScoreA > g.ScoreB && gameMap[g.ID] == "A" {
|
||||
wins++
|
||||
} else if g.ScoreB > g.ScoreA && gameMap[g.ID] == "B" {
|
||||
wins++
|
||||
} else {
|
||||
losses++
|
||||
}
|
||||
}
|
||||
}
|
||||
return stats{Games: games, Wins: wins, Losses: losses}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user