LuggageTracker/main.go
2025-07-08 23:55:20 -04:00

228 lines
5.4 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"
"strings"
"time"
)
type Address struct {
Id int64 `json:"address_id"`
Street1 string `json:"street1"`
Street2 *string `json:"street2,omitempty"`
City string `json:"city"`
State *string `json:"state,omitempty"`
PostalCode string `json:"postal_code"`
Country string `json:"country"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type User struct {
Status int32 `json:"status"`
Id int64 `json:"user_id"`
UserName string `json:"user_name"`
SecretCodes string `json:"secret_codes"`
CurrentToken *string
ContactNumber string `json:"contact_number"`
EmailAddress string `json:"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()
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:8080", "http://localhost:8000"},
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.GET("/", func(c *gin.Context) {
c.File("./static/index.html")
})
api := r.Group("/api")
api.GET("/luggage/: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{
"error": "User not found",
})
} else {
fmt.Printf("Error: %s\n", err)
c.AbortWithStatus(http.StatusBadRequest)
}
} else {
c.JSON(http.StatusOK, user)
}
})
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.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.GET("/u/:user/info", func(c *gin.Context) {
user, err := db.queryUser(c.Param("user"))
if user.CurrentToken == nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"status": 403,
"error": "Unauthorized",
})
} else if strings.Compare(c.Query("token"), *user.CurrentToken) == 0 {
if err != nil {
if errors.Is(err, NoEntriesFoundError) {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
"error": "User not found",
})
} else {
fmt.Printf("Error: %s\n", err)
c.AbortWithStatus(http.StatusBadRequest)
}
} else {
c.JSON(http.StatusOK, user)
}
} else {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"status": 403,
"user": "",
"error": "Unauthorized",
})
}
})
err := r.Run()
if err != nil {
log.Fatal(err)
}
}