This commit is contained in:
Luciano Milani
2026-07-02 12:47:55 +01:00
commit 5de37bb512
132 changed files with 28495 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
package auth
import "golang.org/x/crypto/bcrypt"
const bcryptCost = 12
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
return string(bytes), err
}
func VerifyPassword(password, hash string) bool {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
}
+29
View File
@@ -0,0 +1,29 @@
package auth_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/techxcar/backend/internal/auth"
)
func TestHashPassword_isNotPlaintext(t *testing.T) {
hash, err := auth.HashPassword("mysecret")
require.NoError(t, err)
assert.NotEqual(t, "mysecret", hash)
assert.NotEmpty(t, hash)
}
func TestVerifyPassword_correct(t *testing.T) {
hash, err := auth.HashPassword("correctpassword")
require.NoError(t, err)
assert.True(t, auth.VerifyPassword("correctpassword", hash))
}
func TestVerifyPassword_wrong(t *testing.T) {
hash, err := auth.HashPassword("correctpassword")
require.NoError(t, err)
assert.False(t, auth.VerifyPassword("wrongpassword", hash))
}
+170
View File
@@ -0,0 +1,170 @@
package auth
import (
"context"
"strings"
"time"
"github.com/gofiber/fiber/v2"
goredis "github.com/redis/go-redis/v9"
"github.com/techxcar/backend/internal/config"
redispkg "github.com/techxcar/backend/pkg/redis"
)
// LoginRepository is a subset of tenant.Repository used by auth handlers.
// Uses local types to avoid a circular import with the tenant package.
type LoginRepository interface {
GetSuperAdminByEmail(ctx context.Context, email string) (*LoginSuperAdmin, error)
GetTenantBySlug(ctx context.Context, slug string) (*LoginTenant, error)
GetTenantUserByEmail(ctx context.Context, tenantID, email string) (*LoginUser, error)
}
type LoginSuperAdmin struct {
ID string
Email string
PasswordHash string
}
type LoginTenant struct {
ID string
Status string
}
type LoginUser struct {
ID string
Email string
PasswordHash string
Role string
Name string
Active bool
}
const refreshCookieName = "refresh_token"
const refreshCookieTTL = 30 * 24 * time.Hour
func SetRefreshCookie(c *fiber.Ctx, token string, cfg *config.Config) {
c.Cookie(&fiber.Cookie{
Name: refreshCookieName,
Value: token,
MaxAge: int(refreshCookieTTL.Seconds()),
HTTPOnly: true,
Secure: cfg.AppEnv == "production",
SameSite: "Strict",
Path: "/api/v1/auth",
})
}
type loginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
TenantSlug string `json:"tenant_slug"`
}
func LoginHandler(repo LoginRepository, rdb *redispkg.Redis, cfg *config.Config) fiber.Handler {
return func(c *fiber.Ctx) error {
var req loginRequest
if err := c.BodyParser(&req); err != nil {
return fiber.NewError(400, "corpo do pedido inválido")
}
req.Email = strings.TrimSpace(strings.ToLower(req.Email))
if req.Email == "" || req.Password == "" {
return fiber.NewError(400, "email e password são obrigatórios")
}
var userID, tenantID, role string
userPayload := fiber.Map{}
if req.TenantSlug == "" {
admin, err := repo.GetSuperAdminByEmail(c.Context(), req.Email)
if err != nil || admin == nil || !VerifyPassword(req.Password, admin.PasswordHash) {
return fiber.NewError(401, "credenciais inválidas")
}
userID, tenantID, role = admin.ID, "", "super_admin"
userPayload = fiber.Map{
"id": admin.ID, "email": admin.Email,
"name": admin.Email, "role": "super_admin",
}
} else {
ten, err := repo.GetTenantBySlug(c.Context(), req.TenantSlug)
if err != nil || ten == nil || ten.Status != "active" {
return fiber.NewError(401, "credenciais inválidas")
}
user, err := repo.GetTenantUserByEmail(c.Context(), ten.ID, req.Email)
if err != nil || user == nil || !user.Active || !VerifyPassword(req.Password, user.PasswordHash) {
return fiber.NewError(401, "credenciais inválidas")
}
userID, tenantID, role = user.ID, ten.ID, user.Role
userPayload = fiber.Map{
"id": user.ID, "email": user.Email,
"name": user.Name, "role": user.Role,
"tenantId": ten.ID,
}
}
access, err := GenerateAccessToken(userID, tenantID, role, cfg.JWTSecret)
if err != nil {
return fiber.NewError(500, "erro ao gerar token")
}
refresh, err := GenerateRefreshToken(userID, tenantID, role, cfg.JWTSecret)
if err != nil {
return fiber.NewError(500, "erro ao gerar token")
}
if rdb != nil {
ctx := c.Context()
rdb.Client.Set(ctx, "refresh:"+userID, refresh, refreshCookieTTL)
}
SetRefreshCookie(c, refresh, cfg)
return c.JSON(fiber.Map{"data": fiber.Map{"access_token": access, "user": userPayload}, "error": nil})
}
}
func RefreshHandler(rdb *redispkg.Redis, cfg *config.Config) fiber.Handler {
return func(c *fiber.Ctx) error {
refreshToken := c.Cookies(refreshCookieName)
if refreshToken == "" {
return fiber.NewError(401, "refresh token em falta")
}
claims, err := ValidateToken(refreshToken, cfg.JWTSecret)
if err != nil {
return fiber.NewError(401, "refresh token inválido ou expirado")
}
if rdb != nil {
stored, err := rdb.Client.Get(c.Context(), "refresh:"+claims.UserID).Result()
if err == goredis.Nil || stored != refreshToken {
return fiber.NewError(401, "sessão inválida")
}
}
access, err := GenerateAccessToken(claims.UserID, claims.TenantID, claims.Role, cfg.JWTSecret)
if err != nil {
return fiber.NewError(500, "erro ao renovar token")
}
newRefresh, err := GenerateRefreshToken(claims.UserID, claims.TenantID, claims.Role, cfg.JWTSecret)
if err != nil {
return fiber.NewError(500, "erro ao renovar token")
}
if rdb != nil {
rdb.Client.Set(c.Context(), "refresh:"+claims.UserID, newRefresh, refreshCookieTTL)
}
SetRefreshCookie(c, newRefresh, cfg)
return c.JSON(fiber.Map{"data": fiber.Map{"access_token": access}, "error": nil})
}
}
func LogoutHandler(rdb *redispkg.Redis) fiber.Handler {
return func(c *fiber.Ctx) error {
c.Cookie(&fiber.Cookie{
Name: refreshCookieName,
Value: "",
MaxAge: -1,
HTTPOnly: true,
Path: "/api/v1/auth",
})
return c.JSON(fiber.Map{"data": nil, "error": nil})
}
}
+152
View File
@@ -0,0 +1,152 @@
package auth_test
import (
"bytes"
"context"
"encoding/json"
"net/http/httptest"
"testing"
"time"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/techxcar/backend/internal/auth"
"github.com/techxcar/backend/internal/config"
)
// stubRepo is a minimal in-memory stub for testing auth handlers without a real DB.
type stubRepo struct {
superAdmins map[string]*auth.LoginSuperAdmin
tenants map[string]*auth.LoginTenant
users map[string]*auth.LoginUser
}
func (s *stubRepo) GetSuperAdminByEmail(_ context.Context, email string) (*auth.LoginSuperAdmin, error) {
a, _ := s.superAdmins[email]
return a, nil
}
func (s *stubRepo) GetTenantBySlug(_ context.Context, slug string) (*auth.LoginTenant, error) {
t, _ := s.tenants[slug]
return t, nil
}
func (s *stubRepo) GetTenantUserByEmail(_ context.Context, tenantID, email string) (*auth.LoginUser, error) {
key := tenantID + ":" + email
u, _ := s.users[key]
return u, nil
}
func newTestApp(repo auth.LoginRepository, cfg *config.Config) *fiber.App {
app := fiber.New(fiber.Config{ErrorHandler: func(c *fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError
if e, ok := err.(*fiber.Error); ok {
code = e.Code
}
return c.Status(code).JSON(fiber.Map{"data": nil, "error": err.Error()})
}})
app.Post("/api/v1/auth/login", auth.LoginHandler(repo, nil, cfg))
app.Post("/api/v1/auth/refresh", auth.RefreshHandler(nil, cfg))
app.Post("/api/v1/auth/logout", auth.LogoutHandler(nil))
return app
}
func TestLogin_superAdmin_success(t *testing.T) {
hash, _ := auth.HashPassword("secret123")
repo := &stubRepo{
superAdmins: map[string]*auth.LoginSuperAdmin{
"admin@example.com": {ID: "sa-1", Email: "admin@example.com", PasswordHash: hash},
},
}
cfg := &config.Config{JWTSecret: testSecret}
app := newTestApp(repo, cfg)
body, _ := json.Marshal(map[string]string{"email": "admin@example.com", "password": "secret123"})
req := httptest.NewRequest("POST", "/api/v1/auth/login", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
var result map[string]any
require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
data := result["data"].(map[string]any)
assert.NotEmpty(t, data["access_token"])
assert.NotNil(t, data["user"])
}
func TestLogin_wrongPassword(t *testing.T) {
hash, _ := auth.HashPassword("secret123")
repo := &stubRepo{
superAdmins: map[string]*auth.LoginSuperAdmin{
"admin@example.com": {ID: "sa-1", Email: "admin@example.com", PasswordHash: hash},
},
}
cfg := &config.Config{JWTSecret: testSecret}
app := newTestApp(repo, cfg)
body, _ := json.Marshal(map[string]string{"email": "admin@example.com", "password": "wrongpass"})
req := httptest.NewRequest("POST", "/api/v1/auth/login", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
require.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode)
}
func TestLogin_tenantUser_success(t *testing.T) {
hash, _ := auth.HashPassword("tenantpass")
repo := &stubRepo{
tenants: map[string]*auth.LoginTenant{
"my-workshop": {ID: "11111111-1111-1111-1111-111111111111", Status: "active"},
},
users: map[string]*auth.LoginUser{
"11111111-1111-1111-1111-111111111111:user@workshop.com": {
ID: "u-1", Email: "user@workshop.com", PasswordHash: hash,
Role: "tenant_admin", Name: "User", Active: true,
},
},
}
cfg := &config.Config{JWTSecret: testSecret}
app := newTestApp(repo, cfg)
body, _ := json.Marshal(map[string]string{
"email": "user@workshop.com", "password": "tenantpass", "tenant_slug": "my-workshop",
})
req := httptest.NewRequest("POST", "/api/v1/auth/login", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
var result map[string]any
require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
data := result["data"].(map[string]any)
token := data["access_token"].(string)
claims, err := auth.ValidateToken(token, testSecret)
require.NoError(t, err)
assert.Equal(t, "11111111-1111-1111-1111-111111111111", claims.TenantID)
assert.Equal(t, "tenant_admin", claims.Role)
_ = time.Now()
}
func TestLogin_unknownTenant(t *testing.T) {
repo := &stubRepo{tenants: map[string]*auth.LoginTenant{}}
cfg := &config.Config{JWTSecret: testSecret}
app := newTestApp(repo, cfg)
body, _ := json.Marshal(map[string]string{
"email": "user@workshop.com", "password": "pass", "tenant_slug": "nonexistent",
})
req := httptest.NewRequest("POST", "/api/v1/auth/login", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req)
require.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode)
}
+54
View File
@@ -0,0 +1,54 @@
package auth
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {
UserID string `json:"user_id"`
TenantID string `json:"tenant_id,omitempty"`
Role string `json:"role"`
jwt.RegisteredClaims
}
func GenerateAccessToken(userID, tenantID, role, secret string) (string, error) {
return generateToken(userID, tenantID, role, secret, 15*time.Minute)
}
func GenerateRefreshToken(userID, tenantID, role, secret string) (string, error) {
return generateToken(userID, tenantID, role, secret, 30*24*time.Hour)
}
func generateToken(userID, tenantID, role, secret string, ttl time.Duration) (string, error) {
claims := Claims{
UserID: userID,
TenantID: tenantID,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(ttl)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret))
}
func ValidateToken(tokenStr, secret string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("método de assinatura inesperado: %v", t.Header["alg"])
}
return []byte(secret), nil
})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, fmt.Errorf("token inválido")
}
return claims, nil
}
+51
View File
@@ -0,0 +1,51 @@
package auth_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/techxcar/backend/internal/auth"
)
const testSecret = "test-secret-32-chars-minimum-ok!"
func TestGenerateAndValidateAccessToken(t *testing.T) {
token, err := auth.GenerateAccessToken("user-1", "tenant-1", "tenant_admin", testSecret)
require.NoError(t, err)
assert.NotEmpty(t, token)
claims, err := auth.ValidateToken(token, testSecret)
require.NoError(t, err)
assert.Equal(t, "user-1", claims.UserID)
assert.Equal(t, "tenant-1", claims.TenantID)
assert.Equal(t, "tenant_admin", claims.Role)
assert.True(t, claims.ExpiresAt.After(time.Now()))
assert.True(t, claims.ExpiresAt.Before(time.Now().Add(16*time.Minute)))
}
func TestGenerateRefreshToken_longerExpiry(t *testing.T) {
token, err := auth.GenerateRefreshToken("user-1", "", "super_admin", testSecret)
require.NoError(t, err)
claims, err := auth.ValidateToken(token, testSecret)
require.NoError(t, err)
assert.Empty(t, claims.TenantID)
assert.Equal(t, "super_admin", claims.Role)
assert.True(t, claims.ExpiresAt.After(time.Now().Add(29*24*time.Hour)))
}
func TestValidateToken_wrongSecret(t *testing.T) {
token, err := auth.GenerateAccessToken("user-1", "t-1", "manager", testSecret)
require.NoError(t, err)
_, err = auth.ValidateToken(token, "different-secret-32chars-minimumx")
assert.Error(t, err)
}
func TestValidateToken_malformed(t *testing.T) {
_, err := auth.ValidateToken("not.a.jwt", testSecret)
assert.Error(t, err)
}
+74
View File
@@ -0,0 +1,74 @@
package auth
import (
"strings"
"github.com/gofiber/fiber/v2"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/techxcar/backend/pkg/database"
)
func RequireAuth(secret string) fiber.Handler {
return func(c *fiber.Ctx) error {
authHeader := c.Get("Authorization")
if authHeader == "" {
return fiber.NewError(401, "autenticação necessária")
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return fiber.NewError(401, "formato de autorização inválido")
}
claims, err := ValidateToken(parts[1], secret)
if err != nil {
return fiber.NewError(401, "token inválido ou expirado")
}
c.Locals("claims", claims)
return c.Next()
}
}
func RequireRole(roles ...string) fiber.Handler {
return func(c *fiber.Ctx) error {
claims, ok := c.Locals("claims").(*Claims)
if !ok {
return fiber.NewError(401, "autenticação necessária")
}
for _, role := range roles {
if claims.Role == role {
return c.Next()
}
}
return fiber.NewError(403, "acesso não autorizado")
}
}
// TenantMiddleware acquires a dedicated pgxpool connection per request,
// sets the tenant search_path, stores the connection in c.Locals("conn"),
// and releases the connection after the handler chain completes.
func TenantMiddleware(db *database.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
claims, ok := c.Locals("claims").(*Claims)
if !ok || claims.TenantID == "" {
return c.Next()
}
conn, err := db.Pool.Acquire(c.Context())
if err != nil {
return fiber.NewError(500, "erro interno ao adquirir conexão")
}
schema := `"tenant_` + strings.ReplaceAll(claims.TenantID, "-", "_") + `"`
if _, err := conn.Exec(c.Context(), "SET search_path = "+schema+", public"); err != nil {
conn.Release()
return fiber.NewError(500, "erro interno ao definir schema")
}
c.Locals("conn", conn)
err = c.Next()
conn.Release()
return err
}
}
// GetConn returns the tenant-scoped connection stored by TenantMiddleware.
func GetConn(c *fiber.Ctx) *pgxpool.Conn {
conn, _ := c.Locals("conn").(*pgxpool.Conn)
return conn
}
+117
View File
@@ -0,0 +1,117 @@
package auth_test
import (
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/techxcar/backend/internal/auth"
)
func TestRequireAuth_missingHeader(t *testing.T) {
app := fiber.New()
app.Get("/protected", auth.RequireAuth(testSecret), func(c *fiber.Ctx) error {
return c.SendString("ok")
})
req := httptest.NewRequest("GET", "/protected", nil)
resp, err := app.Test(req)
require.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode)
}
func TestRequireAuth_validToken(t *testing.T) {
token, _ := auth.GenerateAccessToken("user-1", "tenant-1", "manager", testSecret)
app := fiber.New()
app.Get("/protected", auth.RequireAuth(testSecret), func(c *fiber.Ctx) error {
claims := c.Locals("claims").(*auth.Claims)
return c.SendString(claims.UserID)
})
req := httptest.NewRequest("GET", "/protected", nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, err := app.Test(req)
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}
func TestRequireAuth_invalidToken(t *testing.T) {
app := fiber.New()
app.Get("/protected", auth.RequireAuth(testSecret), func(c *fiber.Ctx) error {
return c.SendString("ok")
})
req := httptest.NewRequest("GET", "/protected", nil)
req.Header.Set("Authorization", "Bearer invalid.token.here")
resp, err := app.Test(req)
require.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode)
}
func TestRequireRole_allowed(t *testing.T) {
token, _ := auth.GenerateAccessToken("user-1", "", "super_admin", testSecret)
app := fiber.New()
app.Get("/admin",
auth.RequireAuth(testSecret),
auth.RequireRole("super_admin"),
func(c *fiber.Ctx) error { return c.SendString("ok") },
)
req := httptest.NewRequest("GET", "/admin", nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, err := app.Test(req)
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}
func TestRequireRole_forbidden(t *testing.T) {
token, _ := auth.GenerateAccessToken("user-1", "t-1", "technician", testSecret)
app := fiber.New()
app.Get("/admin",
auth.RequireAuth(testSecret),
auth.RequireRole("super_admin", "tenant_admin"),
func(c *fiber.Ctx) error { return c.SendString("ok") },
)
req := httptest.NewRequest("GET", "/admin", nil)
req.Header.Set("Authorization", "Bearer "+token)
resp, err := app.Test(req)
require.NoError(t, err)
assert.Equal(t, 403, resp.StatusCode)
}
func TestRequireRole_allowsMatchingRole(t *testing.T) {
app := fiber.New()
app.Use(func(c *fiber.Ctx) error {
c.Locals("claims", &auth.Claims{Role: "tenant_admin"})
return c.Next()
})
app.Get("/test", auth.RequireRole("tenant_admin"), func(c *fiber.Ctx) error {
return c.SendStatus(200)
})
req := httptest.NewRequest("GET", "/test", nil)
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}
func TestRequireRole_rejectsMismatch(t *testing.T) {
app := fiber.New()
app.Use(func(c *fiber.Ctx) error {
c.Locals("claims", &auth.Claims{Role: "technician"})
return c.Next()
})
app.Get("/test", auth.RequireRole("super_admin"), func(c *fiber.Ctx) error {
return c.SendStatus(200)
})
req := httptest.NewRequest("GET", "/test", nil)
resp, err := app.Test(req)
assert.NoError(t, err)
assert.Equal(t, 403, resp.StatusCode)
}
+56
View File
@@ -0,0 +1,56 @@
package auth
import (
"context"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/limiter"
goredis "github.com/redis/go-redis/v9"
)
type redisStorage struct {
client *goredis.Client
}
func NewRedisStorage(client *goredis.Client) *redisStorage {
return &redisStorage{client: client}
}
func (s *redisStorage) Get(key string) ([]byte, error) {
val, err := s.client.Get(context.Background(), key).Bytes()
if err == goredis.Nil {
return nil, nil
}
return val, err
}
func (s *redisStorage) Set(key string, val []byte, exp time.Duration) error {
return s.client.Set(context.Background(), key, val, exp).Err()
}
func (s *redisStorage) Delete(key string) error {
return s.client.Del(context.Background(), key).Err()
}
func (s *redisStorage) Reset() error {
return s.client.FlushDB(context.Background()).Err()
}
func (s *redisStorage) Close() error {
return nil
}
func RateLimiter(storage *redisStorage) fiber.Handler {
return limiter.New(limiter.Config{
Max: 10,
Expiration: 1 * time.Minute,
KeyGenerator: func(c *fiber.Ctx) string {
return "ratelimit:auth:" + c.IP()
},
Storage: storage,
LimitReached: func(c *fiber.Ctx) error {
return fiber.NewError(429, "muitas tentativas, tente novamente em 1 minuto")
},
})
}
+23
View File
@@ -0,0 +1,23 @@
package auth
import (
"github.com/gofiber/fiber/v2"
"github.com/techxcar/backend/internal/config"
redispkg "github.com/techxcar/backend/pkg/redis"
)
func RegisterRoutes(app *fiber.App, repo LoginRepository, rdb *redispkg.Redis, cfg *config.Config) {
authGroup := app.Group("/api/v1/auth")
if rdb != nil {
storage := NewRedisStorage(rdb.Client)
rateLimiter := RateLimiter(storage)
authGroup.Post("/login", rateLimiter, LoginHandler(repo, rdb, cfg))
authGroup.Post("/refresh", rateLimiter, RefreshHandler(rdb, cfg))
} else {
authGroup.Post("/login", LoginHandler(repo, rdb, cfg))
authGroup.Post("/refresh", RefreshHandler(rdb, cfg))
}
authGroup.Post("/logout", LogoutHandler(rdb))
}