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