Files
softplayer-backend/internal/controllers/accounts.go
Nikolai Rodionov e58eba1b16
All checks were successful
ci/woodpecker/push/build Pipeline was successful
Implement refresh token endpoint
Signed-off-by: Nikolai Rodionov <allanger@badhouseplants.net>
2026-05-09 21:36:23 +02:00

127 lines
3.4 KiB
Go

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) {
tokenID := uuid.New().String()
claims := jwt.MapClaims{
"user_id": userID,
"type": "access",
"exp": time.Now().Add(c.AccessTokenTTL).Unix(),
"token_id": tokenID,
}
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
}