Start testing tokens
All checks were successful
ci/woodpecker/push/build Pipeline was successful

Signed-off-by: Nikolai Rodionov <allanger@badhouseplants.net>
This commit is contained in:
2026-05-17 22:06:36 +02:00
parent 0178ae2a3f
commit 68e166d90f
5 changed files with 218 additions and 69 deletions

View File

@@ -55,7 +55,7 @@ func (srv *TokensServer) CreateToken(ctx context.Context, in *tokens.CreateToken
Scopes: permissions,
}
token, err := srv.tokenCtrl.Create(ctx, tokenData)
token, _, err := srv.tokenCtrl.Create(ctx, tokenData)
if err != nil {
if errors.Is(err, controllers.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side")

View File

@@ -14,6 +14,7 @@ import (
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/logger"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/token"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/repository"
"github.com/google/uuid"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
@@ -35,6 +36,7 @@ var (
ErrServerError = errors.New("internal server error")
ErrUserTokenMismatch = errors.New("user doesn't match the token")
ErrBadToken = errors.New("bad token")
ErrTokenNotFound = errors.New("token not found")
)
type TokenData struct {
@@ -96,15 +98,26 @@ func (ctrl *TokenController) VerifyTokenOwner(ctx context.Context, userID, token
}
// Create a new token, store its hash in the database and return the token value
func (ctrl *TokenController) Create(ctx context.Context, data *TokenData) (string, error) {
func (ctrl *TokenController) Create(ctx context.Context, data *TokenData) (string, string, error) {
id := uuid.NewString()
log := logger.FromContext(ctx).WithValues("uuid", id)
log.V(2).Info("Creating a new token")
userExists, err := repository.IsAccountExist(ctx, ctrl.DB, data.UserID)
if err != nil {
log.Error(err, "Couldn't check whether a user exists")
return "", "", ErrServerError
}
// If user doesn't exist, do not generate a token
if !userExists {
return "", "", ErrUserNotFound
}
tokenValue, err := token.GenerateToken()
if err != nil {
log.Error(err, "Couldn't create a token")
return "", ErrServerError
return "", "", ErrServerError
}
tokenHash := hashSHA256(tokenValue)
@@ -112,29 +125,26 @@ func (ctrl *TokenController) Create(ctx context.Context, data *TokenData) (strin
scopesJson, err := json.Marshal(data.Scopes)
if err != nil {
log.Error(err, "Couldn't marshal permissions into json")
return "", ErrServerError
return "", "", ErrServerError
}
query := `
INSERT INTO tokens (uuid, description, token_hash, user_id, scopes, created_at, generated_at, expires_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`
if _, err := ctrl.DB.Query(
query,
id,
data.Name,
tokenHash,
data.UserID,
scopesJson,
time.Now(),
time.Now(),
data.ExpiresAt,
); err != nil {
log.Error(err, "Couldn't insert a token in the database")
return "", ErrServerError
queryData := &repository.TokenData{
UUID: id,
Decsription: data.Name,
TokenHash: tokenHash,
UserID: data.UserID,
CreatedAt: time.Now(),
GeneratedAt: time.Now(),
ExpiresAt: data.ExpiresAt,
Scope: string(scopesJson),
}
return tokenValue, nil
if err := repository.CreateToken(ctx, ctrl.DB, queryData); err != nil {
log.Error(err, "Couldn't create a token")
return "", "", ErrServerError
}
return tokenValue, id, nil
}
// Update token name or permissions, other changes are ignored by this method
@@ -148,9 +158,14 @@ func (ctrl *TokenController) Update(ctx context.Context, data *TokenData) error
return ErrServerError
}
query := "UPDATE tokens SET description = $1, scopes = $2 WHERE uuid = $3;"
if _, err := ctrl.DB.Query(query, data.Name, scopesJson, data.UUID); err != nil {
log.Error(err, "Couldn't update a token in the database")
queryData := &repository.TokenData{
UUID: data.UUID,
Scope: string(scopesJson),
Decsription: data.Name,
}
if err := repository.UpdateToken(ctx, ctrl.DB, queryData); err != nil {
log.Error(err, "Couldn't update a token")
return ErrServerError
}
@@ -203,40 +218,36 @@ func (ctrl *TokenController) Regenerate(ctx context.Context, id string) (string,
// Get an existing token data
func (ctrl *TokenController) Get(ctx context.Context, id, userID string) (*TokenData, error) {
log := logger.FromContext(ctx).WithValues("uuid", id, "user_id", userID)
log.V(2).Info("Regenerating a token")
query := `
SELECT uuid, description, generated_at, expires_at, last_used_at, revoked_at, created_at
FROM tokens
WHERE uuid = $1 AND user_id = $2`
token := &TokenData{}
var generatedAt sql.NullTime
var expiresAt sql.NullTime
var revokedAt sql.NullTime
var lastUsedAt sql.NullTime
var createdAt sql.NullTime
log.V(2).Info("Getting a token")
if err := ctrl.DB.QueryRowContext(ctx, query, id, userID).Scan(
&token.UUID,
&token.Name,
&generatedAt,
&expiresAt,
&lastUsedAt,
&revokedAt,
&createdAt,
); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, err
queryResult, err := repository.GetToken(ctx, ctrl.DB, id, userID)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
return nil, ErrTokenNotFound
}
log.Error(err, "Couldn't find a token")
log.Error(err, "Couldn't get a token from DB")
return nil, ErrServerError
}
token.GeneratedAt = generatedAt.Time
token.ExpiresAt = expiresAt.Time
token.RevokedAt = revokedAt.Time
token.LastUsedAt = lastUsedAt.Time
token.CreatedAt = createdAt.Time
return token, nil
scope := map[string][]string{}
if err := json.Unmarshal([]byte(queryResult.Scope), &scope); err != nil {
log.Error(err, "Couldn't unmarshal scope into json")
return nil, ErrServerError
}
result := &TokenData{
UUID: queryResult.UUID,
Name: queryResult.Decsription,
CreatedAt: queryResult.CreatedAt,
LastUsedAt: queryResult.LastUsedAt,
RevokedAt: queryResult.RevokedAt,
ExpiresAt: queryResult.ExpiresAt,
GeneratedAt: queryResult.GeneratedAt,
Scopes: scope,
}
return result, nil
}
// List all available token
@@ -381,6 +392,7 @@ func hashSHA256(s string) string {
return hex.EncodeToString(hash[:])
}
// Unit Tests
func TestUnitHashPersistence(t *testing.T) {
password := "qwertyu9"
hash1 := hashSHA256(password)

View File

@@ -6,6 +6,7 @@ import (
"time"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
@@ -36,24 +37,16 @@ func TestCreateToken_Success(t *testing.T) {
}
ctrl := newTestTokensController(t.Context())
tokenID, err := ctrl.Create(t.Context(), tokenData)
tokenVal, tokenID, err := ctrl.Create(t.Context(), tokenData)
assert.NoError(t, err)
assert.NotEmpty(t, tokenID)
assert.NotEmpty(t, tokenVal)
}
func TestCreateToken_Success(t *testing.T) {
// Create a user for the token
ctrlAccount := newTestAccountController(t.Context())
accountData := &controllers.AccountData{
Password: "qwertyu9",
Email: newTestUniqueEmail("accounts"),
}
id, err := ctrlAccount.Create(t.Context(), accountData)
assert.NoError(t, err)
func TestCreateToken_UserNotExist(t *testing.T) {
tokenData := &controllers.TokenData{
Name: "Test Token",
UserID: id,
UserID: uuid.NewString(),
ExpiresAt: time.Now().Add(time.Second * 5),
Scopes: map[string][]string{
"Test": {"test", "test2"},
@@ -61,7 +54,44 @@ func TestCreateToken_Success(t *testing.T) {
}
ctrl := newTestTokensController(t.Context())
tokenID, err := ctrl.Create(t.Context(), tokenData)
assert.NoError(t, err)
assert.NotEmpty(t, tokenID)
tokenVal, tokenID, err := ctrl.Create(t.Context(), tokenData)
assert.Error(t, err)
assert.ErrorIs(t, err, controllers.ErrUserNotFound)
assert.Empty(t, tokenID)
assert.Empty(t, tokenVal)
}
func TestGetToken_Success(t *testing.T) {
// Create a user for the token
ctrlAccount := newTestAccountController(t.Context())
accountData := &controllers.AccountData{
Password: "qwertyu9",
Email: newTestUniqueEmail("accounts"),
}
now := time.Now()
userID, err := ctrlAccount.Create(t.Context(), accountData)
assert.NoError(t, err)
tokenData := &controllers.TokenData{
Name: "Test Token",
UserID: userID,
ExpiresAt: time.Now().Add(time.Second * 5),
Scopes: map[string][]string{
"Test": {"test", "test2"},
},
}
ctrl := newTestTokensController(t.Context())
_, tokenID, err := ctrl.Create(t.Context(), tokenData)
assert.NoError(t, err)
token, err := ctrl.Get(t.Context(), tokenID, userID)
assert.NoError(t, err)
assert.Equal(t, now.Truncate(time.Second), token.GeneratedAt.Truncate(time.Second))
assert.Equal(t, now.Truncate(time.Second), token.CreatedAt.Truncate(time.Second))
assert.Equal(t, tokenData.Scopes, token.Scopes)
assert.Equal(t, tokenData.Name, token.Name)
assert.Equal(t, tokenData.ExpiresAt.Truncate(time.Second), token.ExpiresAt.Truncate(time.Second))
}

View File

@@ -60,3 +60,16 @@ func GetUUIDForEmail(ctx context.Context, db *sql.DB, email string) (uuid string
}
return
}
// IsAccountExist checks if an account with a UUID exists in the db
func IsAccountExist(ctx context.Context, db *sql.DB, uuid string) (bool, error) {
exists := false
err := db.QueryRowContext(
ctx,
`SELECT EXISTS(
SELECT 1 FROM accounts WHERE uuid = $1
)`,
uuid,
).Scan(&exists)
return exists, err
}

View File

@@ -0,0 +1,94 @@
package repository
import (
"context"
"database/sql"
"errors"
"time"
)
type TokenData struct {
UUID string
Decsription string
TokenHash string
UserID string
CreatedAt time.Time
GeneratedAt time.Time
ExpiresAt time.Time
RevokedAt time.Time
LastUsedAt time.Time
Scope string
}
// CreateTokens adds a new token to a database
func CreateToken(ctx context.Context, db *sql.DB, data *TokenData) error {
query := `
INSERT INTO tokens
(uuid, description, token_hash, user_id, scopes, created_at, generated_at, expires_at)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8);`
if _, err := db.QueryContext(
ctx,
query,
data.UUID,
data.Decsription,
data.TokenHash,
data.UserID,
data.Scope,
data.CreatedAt,
data.GeneratedAt,
data.ExpiresAt,
); err != nil {
return err
}
return nil
}
// UpdateToken updates token description and scope
func UpdateToken(ctx context.Context, db *sql.DB, data *TokenData) error {
query := "UPDATE tokens SET description = $1, scopes = $2 WHERE uuid = $3;"
if _, err := db.QueryContext(ctx, query, data.Decsription, data.Scope, data.UUID); err != nil {
return err
}
return nil
}
func GetToken(ctx context.Context, db *sql.DB, tokenID, userID string) (*TokenData, error) {
query := `
SELECT
uuid, description, generated_at, expires_at,
last_used_at, revoked_at, created_at, scopes
FROM tokens
WHERE uuid = $1 AND user_id = $2;`
var generatedAt sql.NullTime
var expiresAt sql.NullTime
var revokedAt sql.NullTime
var lastUsedAt sql.NullTime
var createdAt sql.NullTime
result := &TokenData{}
if err := db.QueryRowContext(ctx, query, tokenID, userID).Scan(
&result.UUID,
&result.Decsription,
&generatedAt,
&expiresAt,
&lastUsedAt,
&revokedAt,
&createdAt,
&result.Scope,
); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
result.GeneratedAt = generatedAt.Time
result.ExpiresAt = expiresAt.Time
result.RevokedAt = revokedAt.Time
result.LastUsedAt = lastUsedAt.Time
result.CreatedAt = createdAt.Time
return result, nil
}