LuggageTracker/main.go
2025-07-10 15:47:37 -04:00

273 lines
7.2 KiB
Go

package main
import (
"context"
"crypto/rand"
"database/sql"
"encoding/base64"
"errors"
"fmt"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"log"
"net/http"
"os"
"strings"
"time"
)
type Address struct {
Id int64 `json:"-"`
Street1 string `json:"street1" form:"street1"`
Street2 *string `json:"street2,omitempty" form:"street2,omitempty"`
City string `json:"city" form:"city"`
State *string `json:"state,omitempty" form:"state,omitempty"`
PostalCode string `json:"postal_code" form:"postal_code"`
Country string `json:"country" form:"country"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
type User struct {
Status int32 `json:"status"`
Id int64 `json:"-"`
UserName string `json:"user_name" form:"user_name"`
ContactName string `json:"contact_name" form:"contact_name"`
SecretCodes string `json:"-"`
CurrentToken *string `json:"-"`
ContactNumber string `json:"contact_number" form:"phone_number"`
EmailAddress string `json:"email_address" form:"email_address"`
MailingAddress Address `json:"mailing_address"`
}
func GenerateToken(n int) (string, error) {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("reading random bytes: %w\n", err)
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
func startCleanupLoop(ctx context.Context, db *sql.DB, interval, ttl time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if err := cleanupExpired(db, ttl); err != nil {
log.Printf("token cleanup error: %v\n", err)
}
}
}
}
func cleanupExpired(db *sql.DB, ttl time.Duration) error {
sqlStmt := `
DELETE FROM users
WHERE token_creation < DATE_SUB(NOW(), INTERVAL ? SECOND);
`
res, err := db.Exec(sqlStmt, int(ttl.Seconds()))
if err != nil {
return err
}
n, _ := res.RowsAffected()
log.Printf("token cleanup: deleted %d rows\n", n)
return nil
}
func main() {
db := connect()
defer db.db.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go startCleanupLoop(ctx, db.db, time.Hour, 24*time.Hour)
r := gin.Default()
gin.SetMode(gin.ReleaseMode)
allowedOrigins := strings.Split(os.Getenv("CORS_ALLOWED_ORIGINS"), ",")
r.Use(cors.New(cors.Config{
AllowOrigins: allowedOrigins,
AllowMethods: []string{"GET", "POST"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
ExposeHeaders: []string{"Content-Length", "Authorization"},
AllowCredentials: true,
}))
r.Static("/static", "./static")
r.StaticFile("/favicon.ico", "./static/favicon.ico")
r.LoadHTMLGlob("./templates/*")
r.GET("/", func(c *gin.Context) {
c.File("./html/home.html")
})
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
api := r.Group("/api")
api.GET("/u/:user", func(c *gin.Context) {
user, err := db.queryUser(c.Param("user"))
if user.CurrentToken == nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"status": 401,
"error": "Unauthorized",
})
} else if strings.Compare(c.Query("token"), *user.CurrentToken) == 0 {
if err != nil {
if errors.Is(err, NoEntriesFoundError) {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"status": 401,
"error": "Unauthorized",
})
} else {
fmt.Printf("Error: %s\n", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"status": 500,
"error": err.Error(),
})
}
} else {
c.JSON(http.StatusOK, user)
}
} else {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"status": 401,
"error": "Unauthorized",
})
}
})
api.GET("/verify/:user", func(c *gin.Context) {
user, err := db.queryUser(c.Param("user"))
if err != nil {
if errors.Is(err, NoEntriesFoundError) {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"status": 404,
"user": "",
"error": "User not found",
})
} else {
fmt.Printf("Error: %s\n", err)
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"status": 500,
"user": "",
"error": err.Error(),
})
}
} else {
codes := strings.Split(user.SecretCodes, "'")
responded := false
for _, code := range codes {
codeHeader := c.GetHeader("Authorization")
reqCode := strings.Split(codeHeader, " ")
if strings.Compare(code, reqCode[len(reqCode)-1]) == 0 {
token, err := GenerateToken(16)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": 500,
"user": "",
"error": err.Error(),
})
responded = true
break
} else {
err = db.updateToken(user.UserName, token)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"status": 500,
"user": "",
"error": err.Error(),
})
responded = true
break
} else {
user.Status = 200
c.JSON(http.StatusOK, gin.H{
"status": 200,
"user": user.UserName,
"error": "",
"token": token,
})
responded = true
break
}
}
}
}
if !responded {
c.JSON(http.StatusNotFound, gin.H{
"status": "404",
"user": "",
"error": "User not found",
})
}
}
})
api.POST("/register/:user", func(c *gin.Context) {
var newUser User
err := c.Bind(&newUser)
if err != nil {
fmt.Printf("Error: %v", err)
}
c.JSON(http.StatusOK, newUser)
})
user := r.Group("/u")
user.GET("/:user", func(c *gin.Context) {
c.File("./html/verify.html")
})
user.GET("/:user/info", func(c *gin.Context) {
user, err := db.queryUser(c.Param("user"))
if user.CurrentToken == nil {
c.Status(http.StatusUnauthorized)
c.File("./html/unauthorized.html")
} else if strings.Compare(c.Query("token"), *user.CurrentToken) == 0 {
if err != nil {
if errors.Is(err, NoEntriesFoundError) {
c.Status(http.StatusUnauthorized)
c.File("./html/unauthorized.html")
} else {
fmt.Printf("Error: %s\n", err)
c.HTML(http.StatusInternalServerError, "error.html.tmpl", gin.H{
"error": err.Error(),
})
}
} else {
addressStr := fmt.Sprintf("%s", user.MailingAddress.Street1)
if user.MailingAddress.Street2 == nil {
addressStr = fmt.Sprintf("%s, %s", addressStr, user.MailingAddress.City)
} else {
addressStr = fmt.Sprintf("%s, %s, %s", addressStr, *user.MailingAddress.Street2, user.MailingAddress.City)
}
if user.MailingAddress.State == nil {
addressStr = fmt.Sprintf("%s, %s, %s", addressStr, user.MailingAddress.PostalCode, user.MailingAddress.Country)
} else {
addressStr = fmt.Sprintf("%s, %s, %s, %s", addressStr, *user.MailingAddress.State, user.MailingAddress.PostalCode, user.MailingAddress.Country)
}
c.HTML(http.StatusOK, "info.html.tmpl", gin.H{
"contact_name": user.ContactName,
"phone_number": user.ContactNumber,
"email_address": user.EmailAddress,
"address": addressStr,
})
}
} else {
c.Status(http.StatusUnauthorized)
c.File("./html/unauthorized.html")
}
})
err := r.Run()
if err != nil {
log.Fatal(err)
}
}