273 lines
7.2 KiB
Go
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)
|
|
}
|
|
}
|