package controllers import ( "context" "database/sql" "errors" "fmt" "time" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/hash" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/tools/logger" "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 { Password string Email string UUID string } func (c *AccountController) Create(ctx context.Context, data *AccountData) (string, error) { log := logger.FromContext(ctx) data.UUID = uuid.New().String() passwordHash, err := hash.HashPassword(data.Password, int(c.HashCost)) if err != nil { log.Error(err, "Couldn't crate the password hash") return "", nil } query := "INSERT INTO accounts (uuid, email, password_hash) VALUES ($1, $2, $3)" if _, err := c.DB.Query(query, data.UUID, data.Email, passwordHash); err != nil { log.Error(err, "Couldn't create a user in the database") return "", err } return data.UUID, nil } func (c *AccountController) Login(ctx context.Context, email, password string) (string, error) { log := logger.FromContext(ctx) query := "SELECT uuid, password_hash FROM accounts WHERE email = $1;" var passwordHash string var uuid string if err := c.DB.QueryRow(query, email).Scan(&uuid, &passwordHash); err != nil { log.Error(err, "Couldn't get a user from the database") return "", err } if err := hash.CheckPasswordHash(password, passwordHash); err != nil { log.Error(err, "Wrong password") return "", err } return 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 redisKey(id string) string { return fmt.Sprintf("refresh:%s", id) } 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) if err := c.Redis.Set(ctx, redisKey(tokenID), 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, tokenID, userID string) (string, error) { log := logger.FromContext(ctx) userIDRedis := c.Redis.Get(ctx, redisKey(tokenID)).Val() if err := c.Redis.Del(ctx, redisKey(tokenID)).Err(); err != nil { log.Error(err, "Couldn't delete redis entry") return "", err } if userID != userIDRedis { return "", errors.New("user id doesn't match") } return userIDRedis, nil }