UPDATE: Auth module, User module
Some checks failed
Build and Release / release (push) Failing after 1m25s

This commit is contained in:
2026-03-30 00:27:57 +07:00
parent 92d44bb00c
commit f04441bf2a
59 changed files with 4246 additions and 521 deletions

View File

@@ -2,19 +2,22 @@ package repositories
import (
"context"
"crypto/md5"
"encoding/json"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgtype"
"history-api/internal/gen/sqlc"
"history-api/internal/models"
"history-api/pkg/cache"
"history-api/pkg/constants"
"history-api/pkg/convert"
)
type RoleRepository interface {
GetByID(ctx context.Context, id pgtype.UUID) (*models.RoleEntity, error)
GetByIDs(ctx context.Context, ids []string) ([]*models.RoleEntity, error)
GetByname(ctx context.Context, name string) (*models.RoleEntity, error)
All(ctx context.Context) ([]*models.RoleEntity, error)
Create(ctx context.Context, name string) (*models.RoleEntity, error)
@@ -39,6 +42,56 @@ func NewRoleRepository(db sqlc.DBTX, c cache.Cache) RoleRepository {
}
}
func (r *roleRepository) generateQueryKey(prefix string, params any) string {
b, _ := json.Marshal(params)
hash := fmt.Sprintf("%x", md5.Sum(b))
return fmt.Sprintf("%s:%s", prefix, hash)
}
func (r *roleRepository) getByIDsWithFallback(ctx context.Context, ids []string) ([]*models.RoleEntity, error) {
if len(ids) == 0 {
return []*models.RoleEntity{}, nil
}
keys := make([]string, len(ids))
for i, id := range ids {
keys[i] = fmt.Sprintf("role:id:%s", id)
}
raws := r.c.MGet(ctx, keys...)
var roles []*models.RoleEntity
missingRolesToCache := make(map[string]any)
for i, b := range raws {
if len(b) > 0 {
var u models.RoleEntity
if err := json.Unmarshal(b, &u); err == nil {
roles = append(roles, &u)
}
} else {
pgId := pgtype.UUID{}
err := pgId.Scan(ids[i])
if err != nil {
continue
}
dbRole, err := r.GetByID(ctx, pgId)
if err == nil && dbRole != nil {
roles = append(roles, dbRole)
missingRolesToCache[keys[i]] = dbRole
}
}
}
if len(missingRolesToCache) > 0 {
_ = r.c.MSet(ctx, missingRolesToCache, constants.NormalCacheDuration)
}
return roles, nil
}
func (r *roleRepository) GetByIDs(ctx context.Context, ids []string) ([]*models.RoleEntity, error) {
return r.getByIDsWithFallback(ctx, ids)
}
func (r *roleRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.RoleEntity, error) {
cacheId := fmt.Sprintf("role:id:%s", convert.UUIDToString(id))
var role models.RoleEntity
@@ -59,7 +112,7 @@ func (r *roleRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.R
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
_ = r.c.Set(ctx, cacheId, role, 5*time.Minute)
_ = r.c.Set(ctx, cacheId, role, constants.NormalCacheDuration)
return &role, nil
}
@@ -83,7 +136,7 @@ func (r *roleRepository) GetByname(ctx context.Context, name string) (*models.Ro
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
_ = r.c.Set(ctx, cacheId, role, 5*time.Minute)
_ = r.c.Set(ctx, cacheId, role, constants.NormalCacheDuration)
return &role, nil
}
@@ -104,7 +157,7 @@ func (r *roleRepository) Create(ctx context.Context, name string) (*models.RoleE
fmt.Sprintf("role:name:%s", name): role,
fmt.Sprintf("role:id:%s", convert.UUIDToString(row.ID)): role,
}
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
_ = r.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
return &role, nil
}
@@ -125,7 +178,7 @@ func (r *roleRepository) Update(ctx context.Context, params sqlc.UpdateRoleParam
fmt.Sprintf("role:name:%s", row.Name): role,
fmt.Sprintf("role:id:%s", convert.UUIDToString(row.ID)): role,
}
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
_ = r.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
return &role, nil
}

View File

@@ -5,6 +5,7 @@ import (
"database/sql"
"fmt"
"history-api/pkg/cache"
"history-api/pkg/constants"
"time"
)
@@ -50,7 +51,7 @@ func (r *tileRepository) GetMetadata(ctx context.Context) (map[string]string, er
metadata[name] = value
}
_ = r.c.Set(ctx, cacheId, metadata, 10*time.Minute)
_ = r.c.Set(ctx, cacheId, metadata, constants.NormalCacheDuration)
return metadata, nil
}

View File

@@ -0,0 +1,79 @@
package repositories
import (
"context"
"fmt"
"history-api/internal/models"
"history-api/pkg/cache"
"history-api/pkg/constants"
)
type TokenRepository interface {
CheckCooldown(ctx context.Context, email string, tokenType constants.TokenType) (bool, error)
Get(ctx context.Context, email string, tokenType constants.TokenType) (*models.TokenEntity, error)
Create(ctx context.Context, token *models.TokenEntity) error
Delete(ctx context.Context, email string, tokenType constants.TokenType) error
CheckVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) (bool, error)
CreateVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) error
DeleteVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) error
}
type tokenRepository struct {
c cache.Cache
}
func NewTokenRepository(c cache.Cache) TokenRepository {
return &tokenRepository{
c: c,
}
}
func (t *tokenRepository) CreateVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) error {
cacheKey := fmt.Sprintf("token:verified:%d:%s:%s", tokenType.Value(), email, id)
return t.c.Set(ctx, cacheKey, true, constants.TokenVerifiedDuration)
}
func (t *tokenRepository) DeleteVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) error {
cacheKey := fmt.Sprintf("token:verified:%d:%s:%s", tokenType.Value(), email, id)
return t.c.Del(ctx, cacheKey)
}
func (t *tokenRepository) CheckCooldown(ctx context.Context, email string, tokenType constants.TokenType) (bool, error) {
cacheKey := fmt.Sprintf("token:cooldown:%d:%s", tokenType.Value(), email)
exists, err := t.c.Exists(ctx, cacheKey)
return exists, err
}
func (t *tokenRepository) CheckVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) (bool, error) {
cacheKey := fmt.Sprintf("token:verified:%d:%s:%s", tokenType.Value(), email, id)
exists, err := t.c.Exists(ctx, cacheKey)
return exists, err
}
func (t *tokenRepository) Create(ctx context.Context, token *models.TokenEntity) error {
cacheKey := fmt.Sprintf("token:%d:%s", token.TokenType.Value(), token.Email)
err := t.c.Set(ctx, cacheKey, token, constants.TokenExpirationDuration)
if err != nil {
return err
}
cooldownKey := fmt.Sprintf("token:cooldown:%d:%s", token.TokenType.Value(), token.Email)
return t.c.Set(ctx, cooldownKey, true, constants.TokenCooldownDuration)
}
func (t *tokenRepository) Delete(ctx context.Context, email string, tokenType constants.TokenType) error {
cacheKey := fmt.Sprintf("token:%d:%s", tokenType.Value(), email)
cooldownKey := fmt.Sprintf("token:cooldown:%d:%s", tokenType.Value(), email)
_ = t.c.Del(ctx, cooldownKey)
return t.c.Del(ctx, cacheKey)
}
func (t *tokenRepository) Get(ctx context.Context, email string, tokenType constants.TokenType) (*models.TokenEntity, error) {
cacheKey := fmt.Sprintf("token:%d:%s", tokenType.Value(), email)
var token models.TokenEntity
err := t.c.Get(ctx, cacheKey, &token)
if err != nil {
return nil, err
}
return &token, nil
}

View File

@@ -2,21 +2,25 @@ package repositories
import (
"context"
"crypto/md5"
"encoding/json"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgtype"
"history-api/internal/gen/sqlc"
"history-api/internal/models"
"history-api/pkg/cache"
"history-api/pkg/constants"
"history-api/pkg/convert"
)
type UserRepository interface {
GetByID(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error)
GetByIDWithoutDeleted(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error)
GetByEmail(ctx context.Context, email string) (*models.UserEntity, error)
All(ctx context.Context) ([]*models.UserEntity, error)
All(ctx context.Context, params sqlc.GetUsersParams) ([]*models.UserEntity, error)
Search(ctx context.Context, params sqlc.SearchUsersParams) ([]*models.UserEntity, error)
UpsertUser(ctx context.Context, params sqlc.UpsertUserParams) (*models.UserEntity, error)
CreateProfile(ctx context.Context, params sqlc.CreateUserProfileParams) (*models.UserProfileSimple, error)
UpdateProfile(ctx context.Context, params sqlc.UpdateUserProfileParams) (*models.UserEntity, error)
@@ -24,7 +28,6 @@ type UserRepository interface {
UpdateRefreshToken(ctx context.Context, params sqlc.UpdateUserRefreshTokenParams) error
GetTokenVersion(ctx context.Context, id pgtype.UUID) (int32, error)
UpdateTokenVersion(ctx context.Context, params sqlc.UpdateTokenVersionParams) error
Verify(ctx context.Context, id pgtype.UUID) error
Delete(ctx context.Context, id pgtype.UUID) error
Restore(ctx context.Context, id pgtype.UUID) error
}
@@ -41,6 +44,52 @@ func NewUserRepository(db sqlc.DBTX, c cache.Cache) UserRepository {
}
}
func (r *userRepository) generateQueryKey(prefix string, params any) string {
b, _ := json.Marshal(params)
hash := fmt.Sprintf("%x", md5.Sum(b))
return fmt.Sprintf("%s:%s", prefix, hash)
}
func (r *userRepository) getByIDsWithFallback(ctx context.Context, ids []string) ([]*models.UserEntity, error) {
if len(ids) == 0 {
return []*models.UserEntity{}, nil
}
keys := make([]string, len(ids))
for i, id := range ids {
keys[i] = fmt.Sprintf("user:id:%s", id)
}
raws := r.c.MGet(ctx, keys...)
var users []*models.UserEntity
missingUsersToCache := make(map[string]any)
for i, b := range raws {
if len(b) > 0 {
var u models.UserEntity
if err := json.Unmarshal(b, &u); err == nil {
users = append(users, &u)
}
} else {
pgId := pgtype.UUID{}
err := pgId.Scan(ids[i])
if err != nil {
continue
}
dbUser, err := r.GetByID(ctx, pgId)
if err == nil && dbUser != nil {
users = append(users, dbUser)
missingUsersToCache[keys[i]] = dbUser
}
}
}
if len(missingUsersToCache) > 0 {
_ = r.c.MSet(ctx, missingUsersToCache, constants.NormalCacheDuration)
}
return users, nil
}
func (r *userRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error) {
cacheId := fmt.Sprintf("user:id:%s", convert.UUIDToString(id))
var user models.UserEntity
@@ -58,7 +107,6 @@ func (r *userRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.U
ID: convert.UUIDToString(row.ID),
Email: row.Email,
PasswordHash: convert.TextToString(row.PasswordHash),
IsVerified: row.IsVerified,
TokenVersion: row.TokenVersion,
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
@@ -73,7 +121,43 @@ func (r *userRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.U
return nil, err
}
_ = r.c.Set(ctx, cacheId, user, 5*time.Minute)
_ = r.c.Set(ctx, cacheId, user, constants.NormalCacheDuration)
return &user, nil
}
func (r *userRepository) GetByIDWithoutDeleted(ctx context.Context, id pgtype.UUID) (*models.UserEntity, error) {
cacheId := fmt.Sprintf("user:deleted:id:%s", convert.UUIDToString(id))
var user models.UserEntity
err := r.c.Get(ctx, cacheId, &user)
if err == nil {
return &user, nil
}
row, err := r.q.GetUserByID(ctx, id)
if err != nil {
return nil, err
}
user = models.UserEntity{
ID: convert.UUIDToString(row.ID),
Email: row.Email,
PasswordHash: convert.TextToString(row.PasswordHash),
TokenVersion: row.TokenVersion,
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
if err := user.ParseRoles(row.Roles); err != nil {
return nil, err
}
if err := user.ParseProfile(row.Profile); err != nil {
return nil, err
}
_ = r.c.Set(ctx, cacheId, user, constants.NormalCacheDuration)
return &user, nil
}
@@ -96,7 +180,6 @@ func (r *userRepository) GetByEmail(ctx context.Context, email string) (*models.
ID: convert.UUIDToString(row.ID),
Email: row.Email,
PasswordHash: convert.TextToString(row.PasswordHash),
IsVerified: row.IsVerified,
TokenVersion: row.TokenVersion,
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
@@ -111,7 +194,7 @@ func (r *userRepository) GetByEmail(ctx context.Context, email string) (*models.
return nil, err
}
_ = r.c.Set(ctx, cacheId, user, 5*time.Minute)
_ = r.c.Set(ctx, cacheId, user, constants.NormalCacheDuration)
return &user, nil
}
@@ -121,12 +204,17 @@ func (r *userRepository) UpsertUser(ctx context.Context, params sqlc.UpsertUserP
if err != nil {
return nil, err
}
go func() {
bgCtx := context.Background()
_ = r.c.DelByPattern(bgCtx, "user:all*")
_ = r.c.DelByPattern(bgCtx, "user:search*")
}()
return &models.UserEntity{
ID: convert.UUIDToString(row.ID),
Email: row.Email,
PasswordHash: convert.TextToString(row.PasswordHash),
IsVerified: row.IsVerified,
TokenVersion: row.TokenVersion,
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
@@ -161,7 +249,7 @@ func (r *userRepository) UpdateProfile(ctx context.Context, params sqlc.UpdateUs
fmt.Sprintf("user:email:%s", user.Email): user,
fmt.Sprintf("user:id:%s", user.ID): user,
}
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
_ = r.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
return user, nil
}
@@ -183,19 +271,27 @@ func (r *userRepository) CreateProfile(ctx context.Context, params sqlc.CreateUs
}, nil
}
func (r *userRepository) All(ctx context.Context) ([]*models.UserEntity, error) {
rows, err := r.q.GetUsers(ctx)
func (r *userRepository) All(ctx context.Context, params sqlc.GetUsersParams) ([]*models.UserEntity, error) {
queryKey := r.generateQueryKey("user:all", params)
var cachedIDs []string
if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
return r.getByIDsWithFallback(ctx, cachedIDs)
}
rows, err := r.q.GetUsers(ctx, params)
if err != nil {
return nil, err
}
var users []*models.UserEntity
var ids []string
usersToCache := make(map[string]any)
for _, row := range rows {
user := &models.UserEntity{
ID: convert.UUIDToString(row.ID),
Email: row.Email,
PasswordHash: convert.TextToString(row.PasswordHash),
IsVerified: row.IsVerified,
TokenVersion: row.TokenVersion,
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
@@ -205,44 +301,75 @@ func (r *userRepository) All(ctx context.Context) ([]*models.UserEntity, error)
if err := user.ParseRoles(row.Roles); err != nil {
return nil, err
}
if err := user.ParseProfile(row.Profile); err != nil {
return nil, err
}
users = append(users, user)
ids = append(ids, user.ID)
usersToCache[fmt.Sprintf("user:id:%s", user.ID)] = user
}
if len(usersToCache) > 0 {
_ = r.c.MSet(ctx, usersToCache, constants.NormalCacheDuration)
}
if len(ids) > 0 {
_ = r.c.Set(ctx, queryKey, ids, constants.ListCacheDuration)
}
return users, nil
}
func (r *userRepository) Verify(ctx context.Context, id pgtype.UUID) error {
user, err := r.GetByID(ctx, id)
if err != nil {
return err
func (r *userRepository) Search(ctx context.Context, params sqlc.SearchUsersParams) ([]*models.UserEntity, error) {
queryKey := r.generateQueryKey("user:search", params)
var cachedIDs []string
if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
return r.getByIDsWithFallback(ctx, cachedIDs)
}
err = r.q.VerifyUser(ctx, id)
rows, err := r.q.SearchUsers(ctx, params)
if err != nil {
return err
}
err = r.q.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
ID: id,
TokenVersion: user.TokenVersion + 1,
})
if err != nil {
return err
return nil, err
}
user.IsVerified = true
user.TokenVersion += 1
var users []*models.UserEntity
var ids []string
usersToCache := make(map[string]any)
mapCache := map[string]any{
fmt.Sprintf("user:email:%s", user.Email): user,
fmt.Sprintf("user:id:%s", user.ID): user,
for _, row := range rows {
user := &models.UserEntity{
ID: convert.UUIDToString(row.ID),
Email: row.Email,
PasswordHash: convert.TextToString(row.PasswordHash),
TokenVersion: row.TokenVersion,
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
if err := user.ParseRoles(row.Roles); err != nil {
return nil, err
}
if err := user.ParseProfile(row.Profile); err != nil {
return nil, err
}
users = append(users, user)
ids = append(ids, user.ID)
usersToCache[fmt.Sprintf("user:id:%s", user.ID)] = user
}
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
return nil
if len(usersToCache) > 0 {
_ = r.c.MSet(ctx, usersToCache, constants.NormalCacheDuration)
}
if len(ids) > 0 {
_ = r.c.Set(ctx, queryKey, ids, constants.ListCacheDuration)
}
return users, nil
}
func (r *userRepository) Delete(ctx context.Context, id pgtype.UUID) error {
@@ -288,7 +415,7 @@ func (r *userRepository) GetTokenVersion(ctx context.Context, id pgtype.UUID) (i
return 0, err
}
_ = r.c.Set(ctx, cacheId, raw, 5*time.Minute)
_ = r.c.Set(ctx, cacheId, raw, constants.NormalCacheDuration)
return raw, nil
}
@@ -299,7 +426,7 @@ func (r *userRepository) UpdateTokenVersion(ctx context.Context, params sqlc.Upd
}
cacheId := fmt.Sprintf("user:token:%s", convert.UUIDToString(params.ID))
_ = r.c.Set(ctx, cacheId, params.TokenVersion, 5*time.Minute)
_ = r.c.Set(ctx, cacheId, params.TokenVersion, constants.NormalCacheDuration)
return nil
}
@@ -328,7 +455,7 @@ func (r *userRepository) UpdatePassword(ctx context.Context, params sqlc.UpdateU
fmt.Sprintf("user:token:%s", user.ID): user.TokenVersion,
}
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
_ = r.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
return nil
}
@@ -349,6 +476,6 @@ func (r *userRepository) UpdateRefreshToken(ctx context.Context, params sqlc.Upd
fmt.Sprintf("user:token:%s", user.ID): user.TokenVersion,
}
_ = r.c.MSet(ctx, mapCache, 5*time.Minute)
_ = r.c.MSet(ctx, mapCache, constants.NormalCacheDuration)
return nil
}