feat: implement user profile retrieval with roles and permissions caching

This commit is contained in:
Tran Anh Tuan
2026-05-12 14:36:50 +07:00
parent e81a248a61
commit 902caa222f
17 changed files with 671 additions and 19 deletions

View File

@@ -7,6 +7,7 @@ import (
"wm-backend/internal/models"
db "wm-backend/sqlc_gen"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)
@@ -43,3 +44,20 @@ func CreateUser(ctx context.Context, queries *db.Queries, params db.CreateUserPa
}
return id.String(), nil
}
// GetUserByID retrieves a user by their ID using SQLC-generated queries.
// Returns nil, nil if no user is found.
func GetUserByID(ctx context.Context, queries *db.Queries, id string) (*models.User, error) {
uid, err := uuid.Parse(id)
if err != nil {
return nil, err
}
user, err := queries.GetUserByID(ctx, uid)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return nil, err
}
return mapper.ToDomainUser(user), nil
}

View File

@@ -0,0 +1,29 @@
package repositories
import (
"context"
"wm-backend/internal/models"
db "wm-backend/sqlc_gen"
"github.com/google/uuid"
)
// GetPermissionsByUserID retrieves all permission names for a given user.
func GetPermissionsByUserID(ctx context.Context, queries *db.Queries, userID string) ([]models.Permission, error) {
uid, err := uuid.Parse(userID)
if err != nil {
return nil, err
}
rows, err := queries.GetPermissionsByUserID(ctx, uid)
if err != nil {
return nil, err
}
permissions := make([]models.Permission, 0, len(rows))
for _, row := range rows {
permissions = append(permissions, models.Permission{
Name: row.Name,
Description: row.Description.String,
})
}
return permissions, nil
}

View File

@@ -1 +1,79 @@
package redis
import (
"context"
"encoding/json"
"fmt"
"time"
"wm-backend/global"
"github.com/rs/zerolog/log"
)
const (
userRBACPrefix = "user:rbac:"
defaultTTL = 60 * time.Minute
)
// RBACCachedData represents the cached roles and permissions for a user.
type RBACCachedData struct {
Roles []RoleCacheItem `json:"roles"`
Permissions []string `json:"permissions"`
}
// RoleCacheItem represents a cached role item.
type RoleCacheItem struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
}
func buildKey(userID string) string {
return fmt.Sprintf("%s%s", userRBACPrefix, userID)
}
// CacheUserPermissions stores the user's roles and permissions in Redis with TTL.
func CacheUserPermissions(ctx context.Context, userID string, data RBACCachedData, ttl time.Duration) error {
jsonData, err := json.Marshal(data)
if err != nil {
log.Error().Err(err).Str("userID", userID).Msg("Failed to marshal RBAC data for caching")
return err
}
if err := global.Cache.Set(ctx, buildKey(userID), jsonData, ttl).Err(); err != nil {
log.Error().Err(err).Str("userID", userID).Msg("Failed to cache user permissions in Redis")
return err
}
return nil
}
// GetCachedUserPermissions retrieves the cached RBAC data from Redis.
// Returns (nil, false) if not found or on error (graceful fallback).
func GetCachedUserPermissions(ctx context.Context, userID string) (*RBACCachedData, bool) {
val, err := global.Cache.Get(ctx, buildKey(userID)).Result()
if err != nil {
// Key doesn't exist or Redis error; silently fallback to DB
return nil, false
}
var data RBACCachedData
if err := json.Unmarshal([]byte(val), &data); err != nil {
log.Error().Err(err).Str("userID", userID).Msg("Failed to unmarshal cached RBAC data")
return nil, false
}
return &data, true
}
// RefreshTTL resets the TTL on a cached RBAC entry when a cache hit occurs.
func RefreshTTL(ctx context.Context, userID string, ttl time.Duration) {
if err := global.Cache.Expire(ctx, buildKey(userID), ttl).Err(); err != nil {
log.Error().Err(err).Str("userID", userID).Msg("Failed to refresh TTL for RBAC cache")
}
}
// DeleteUserPermissionsCache removes the cached RBAC data for a user (for cache invalidation).
func DeleteUserPermissionsCache(ctx context.Context, userID string) {
if err := global.Cache.Del(ctx, buildKey(userID)).Err(); err != nil {
log.Error().Err(err).Str("userID", userID).Msg("Failed to delete RBAC cache")
}
}

View File

@@ -5,6 +5,8 @@ import (
"wm-backend/internal/mapper"
"wm-backend/internal/models"
db "wm-backend/sqlc_gen"
"github.com/google/uuid"
)
func CreateRole(ctx context.Context, queries *db.Queries, body models.Role) (models.Role, error) {
@@ -14,3 +16,24 @@ func CreateRole(ctx context.Context, queries *db.Queries, body models.Role) (mod
}
return *mapper.ToDomainRole(role), nil
}
// GetUserRolesByUserID retrieves all roles assigned to a user with role info.
func GetUserRolesByUserID(ctx context.Context, queries *db.Queries, userID string) ([]models.Role, error) {
uid, err := uuid.Parse(userID)
if err != nil {
return nil, err
}
rows, err := queries.GetUserRolesByUserID(ctx, uid)
if err != nil {
return nil, err
}
roles := make([]models.Role, 0, len(rows))
for _, row := range rows {
roles = append(roles, models.Role{
ID: row.RoleID.String(),
Name: row.RoleName,
Description: row.RoleDescription.String,
})
}
return roles, nil
}