114 lines
3.0 KiB
Go
114 lines
3.0 KiB
Go
package controllers
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/hash"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
type AccountController struct {
|
|
DB *sql.DB
|
|
Redis *redis.Client
|
|
DevMode bool
|
|
HashCost int16
|
|
AccessTokenTTL time.Duration
|
|
RefreshTokenTTL time.Duration
|
|
JWTSecret []byte
|
|
}
|
|
|
|
type JWT struct {
|
|
RefreshToken string
|
|
AccessToken string
|
|
}
|
|
|
|
type AccountParams struct{}
|
|
|
|
type AccountData struct {
|
|
Username string
|
|
Password string
|
|
Email string
|
|
UUID string
|
|
}
|
|
|
|
func (c *AccountController) Create(ctx context.Context, data *AccountData) (string, error) {
|
|
data.UUID = uuid.New().String()
|
|
|
|
passwordHash, err := hash.HashPassword(data.Password, int(c.HashCost))
|
|
if err != nil {
|
|
return "", nil
|
|
}
|
|
|
|
query := "INSERT INTO users (uuid, username, email, password_hash) VALUES ($1, $2, $3, $4)"
|
|
if _, err := c.DB.Query(query, data.UUID, data.Username, data.Email, passwordHash); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return data.UUID, nil
|
|
}
|
|
|
|
func (c *AccountController) GenerateAccessToken(userID string) (string, error) {
|
|
claims := jwt.MapClaims{
|
|
"user_id": userID,
|
|
"type": "access",
|
|
"exp": time.Now().Add(c.AccessTokenTTL).Unix(),
|
|
}
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
return token.SignedString(c.JWTSecret)
|
|
}
|
|
|
|
func (c *AccountController) GenerateRefreshToken(ctx context.Context, userID string) (string, error) {
|
|
tokenID := uuid.New().String()
|
|
claims := jwt.MapClaims{
|
|
"user_id": userID,
|
|
"token_id": tokenID,
|
|
"type": "refresh",
|
|
"exp": time.Now().Add(c.RefreshTokenTTL).Unix(),
|
|
}
|
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
redisKey := fmt.Sprintf("refresh_token:%s", tokenID)
|
|
if err := c.Redis.Set(ctx, redisKey, userID, c.RefreshTokenTTL).Err(); err != nil {
|
|
return "", err
|
|
}
|
|
return token.SignedString(c.JWTSecret)
|
|
}
|
|
|
|
// It must validate the refresh token
|
|
// Get it's id from the content
|
|
// Find a corresponding token in redis, and if it's found, remove it and create a new one
|
|
func (c *AccountController) ValidateRefreshToken(ctx context.Context, tokenString string) (string, error) {
|
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
|
|
// hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key")
|
|
return c.JWTSecret, nil
|
|
}, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var tokenID string
|
|
var userID string
|
|
if claims, ok := token.Claims.(jwt.MapClaims); ok {
|
|
tokenID = claims["token_id"].(string)
|
|
userID = claims["user_id"].(string)
|
|
} else {
|
|
return "", errors.New("token id is not set")
|
|
}
|
|
|
|
userIDRedis := c.Redis.Get(ctx, tokenID).String()
|
|
if c.Redis.Del(ctx, tokenID).Err() != nil {
|
|
return "", err
|
|
}
|
|
if userID != userIDRedis {
|
|
return "", errors.New("user id doesn't match")
|
|
}
|
|
return userIDRedis, nil
|
|
}
|