From 902caa222f9c1c363afbbb7f282afa92ef228f75 Mon Sep 17 00:00:00 2001 From: Tran Anh Tuan Date: Tue, 12 May 2026 14:36:50 +0700 Subject: [PATCH] feat: implement user profile retrieval with roles and permissions caching --- configs/constants/constants.go | 29 +-- db/queries/permission.sql | 6 + internal/middlewares/auth_middleware.go | 46 +++- internal/models/config_model.go | 4 +- internal/models/permission_model.go | 7 + internal/models/responses/auth_response.go | 19 ++ internal/repositories/auth_repository.go | 18 ++ .../repositories/permission_repository.go | 29 +++ .../repositories/redis/permission_redis.go | 78 +++++++ internal/repositories/role_repository.go | 23 ++ internal/routers/router.go | 7 + internal/services/auth_service.go | 47 ++++- internal/services/profile_service.go | 121 +++++++++++ pkg/helper/jwt.go | 7 +- sqlc_gen/invoice_item.sql.go | 197 ++++++++++++++++++ sqlc_gen/permission.sql.go | 46 ++++ sqlc_gen/querier.go | 6 + 17 files changed, 671 insertions(+), 19 deletions(-) create mode 100644 internal/models/permission_model.go create mode 100644 internal/repositories/permission_repository.go create mode 100644 internal/services/profile_service.go create mode 100644 sqlc_gen/invoice_item.sql.go create mode 100644 sqlc_gen/permission.sql.go diff --git a/configs/constants/constants.go b/configs/constants/constants.go index 383b28e..7bfe6b0 100644 --- a/configs/constants/constants.go +++ b/configs/constants/constants.go @@ -10,20 +10,20 @@ const ( ) const ( - API_GROUP_AUTH = "/auth" - API_GROUP_WAREHOUSE = "/warehouses" - API_GROUP_ROOM = "/rooms" - API_GROUP_CABINET = "/cabinets" - API_GROUP_SHELF = "/shelves" - API_GROUP_CONTAINER = "/containers" - API_GROUP_COMPONENT_TYPE = "/component-types" - API_GROUP_COMPONENT = "/components" - API_GROUP_COMPONENT_CODE = "/component-codes" - API_GROUP_COMPONENT_ITEM = "/component-items" - API_GROUP_INVOICE_CONFIG = "/invoice-configs" - API_GROUP_INVOICE_CONFIG_ITEM = "/invoice-config-items" - API_GROUP_INVOICE = "/invoices" - API_GROUP_ALTERNATIVE_COMPONENT = "/alternative-components" + API_GROUP_AUTH = "/auth" + API_GROUP_WAREHOUSE = "/warehouses" + API_GROUP_ROOM = "/rooms" + API_GROUP_CABINET = "/cabinets" + API_GROUP_SHELF = "/shelves" + API_GROUP_CONTAINER = "/containers" + API_GROUP_COMPONENT_TYPE = "/component-types" + API_GROUP_COMPONENT = "/components" + API_GROUP_COMPONENT_CODE = "/component-codes" + API_GROUP_COMPONENT_ITEM = "/component-items" + API_GROUP_INVOICE_CONFIG = "/invoice-configs" + API_GROUP_INVOICE_CONFIG_ITEM = "/invoice-config-items" + API_GROUP_INVOICE = "/invoices" + API_GROUP_ALTERNATIVE_COMPONENT = "/alternative-components" ) const ( @@ -31,4 +31,5 @@ const ( API_PATH_DOCS = "/swagger/*any" API_PATH_AUTH_REGISTER = "/register" API_PATH_AUTH_LOGIN = "/login" + API_PATH_PROFILE = "/profile" ) diff --git a/db/queries/permission.sql b/db/queries/permission.sql index e69de29..d1e0b37 100644 --- a/db/queries/permission.sql +++ b/db/queries/permission.sql @@ -0,0 +1,6 @@ +-- name: GetPermissionsByUserID :many +SELECT DISTINCT p.name, p.description +FROM user_roles ur +JOIN role_permissions rp ON rp.role_id = ur.role_id +JOIN permissions p ON p.id = rp.permission_id +WHERE ur.user_id = sqlc.arg(user_id); diff --git a/internal/middlewares/auth_middleware.go b/internal/middlewares/auth_middleware.go index 00e28bf..ca8af3c 100644 --- a/internal/middlewares/auth_middleware.go +++ b/internal/middlewares/auth_middleware.go @@ -1,10 +1,54 @@ package middlewares -import "github.com/gin-gonic/gin" +import ( + "net/http" + "strings" + + "wm-backend/pkg/helper" + "wm-backend/response" + + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" +) func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // 1. Get the Authorization header + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + response.UnauthorizedError(c, http.StatusUnauthorized, "Missing Authorization header") + c.Abort() + return + } + + // 2. Extract Bearer token + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") { + response.UnauthorizedError(c, http.StatusUnauthorized, "Invalid Authorization header format") + c.Abort() + return + } + tokenString := parts[1] + + // 3. Parse JWT token + claims, err := helper.ParseToken(tokenString) + if err != nil { + log.Error().Err(err).Msg("Failed to parse JWT token") + response.UnauthorizedError(c, http.StatusUnauthorized, "Invalid or expired token") + c.Abort() + return + } + + // 4. Extract user_id from claims + userID, ok := claims["user_id"].(string) + if !ok || userID == "" { + response.UnauthorizedError(c, http.StatusUnauthorized, "Invalid token: missing user_id") + c.Abort() + return + } + + // 5. Set user_id in gin context for downstream handlers + c.Set("user_id", userID) c.Next() } } diff --git a/internal/models/config_model.go b/internal/models/config_model.go index 96b8586..043796b 100644 --- a/internal/models/config_model.go +++ b/internal/models/config_model.go @@ -16,8 +16,8 @@ type AdminConfig struct { } type JWTConfig struct { - SecretKey string - ExpirationHours int + SecretKey string `mapstructure:"secretkey"` + ExpirationHours int `mapstructure:"expirehours"` } type DatabaseConfig struct { diff --git a/internal/models/permission_model.go b/internal/models/permission_model.go new file mode 100644 index 0000000..d286f25 --- /dev/null +++ b/internal/models/permission_model.go @@ -0,0 +1,7 @@ +package models + +// Permission represents a system permission (e.g., "read:warehouse", "write:component") +type Permission struct { + Name string `json:"name"` + Description string `json:"description"` +} diff --git a/internal/models/responses/auth_response.go b/internal/models/responses/auth_response.go index a14cda1..14c92ce 100644 --- a/internal/models/responses/auth_response.go +++ b/internal/models/responses/auth_response.go @@ -3,6 +3,25 @@ package responses type BodyRegisterResponse struct { ID string `json:"id"` } + type BodyLoginResponse struct { Token string `json:"token"` } + +// RoleItem represents a role assigned to the user in the profile response. +type RoleItem struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` +} + +// BodyProfileResponse is the response body for GET /profile. +type BodyProfileResponse struct { + ID string `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + FullName string `json:"fullName"` + IsActive bool `json:"isActive"` + Roles []RoleItem `json:"roles"` + Permissions []string `json:"permissions"` +} diff --git a/internal/repositories/auth_repository.go b/internal/repositories/auth_repository.go index 65739c6..cc2c194 100644 --- a/internal/repositories/auth_repository.go +++ b/internal/repositories/auth_repository.go @@ -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 +} diff --git a/internal/repositories/permission_repository.go b/internal/repositories/permission_repository.go new file mode 100644 index 0000000..97afea6 --- /dev/null +++ b/internal/repositories/permission_repository.go @@ -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 +} diff --git a/internal/repositories/redis/permission_redis.go b/internal/repositories/redis/permission_redis.go index 65a229e..090d175 100644 --- a/internal/repositories/redis/permission_redis.go +++ b/internal/repositories/redis/permission_redis.go @@ -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") + } +} diff --git a/internal/repositories/role_repository.go b/internal/repositories/role_repository.go index 46cc24e..f3c650c 100644 --- a/internal/repositories/role_repository.go +++ b/internal/repositories/role_repository.go @@ -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 +} diff --git a/internal/routers/router.go b/internal/routers/router.go index 7e28f97..9b026b7 100644 --- a/internal/routers/router.go +++ b/internal/routers/router.go @@ -29,6 +29,13 @@ func NewRouter() *gin.Engine { auth.POST(constants.API_PATH_AUTH_LOGIN, utils.AsyncHandler(services.Login)) } + // Protected routes (require JWT authentication) + protected := v1.Group("") + protected.Use(middlewares.AuthMiddleware()) + { + protected.GET(constants.API_PATH_PROFILE, utils.AsyncHandler(services.GetProfile)) + } + warehouse := v1.Group(constants.API_GROUP_WAREHOUSE) { warehouse.GET("", utils.AsyncHandler(services.WareHouseList)) diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go index dc46223..db99eab 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -1,12 +1,15 @@ package services import ( + "context" "net/http" + "time" "wm-backend/global" "wm-backend/internal/models" "wm-backend/internal/models/requests" "wm-backend/internal/models/responses" "wm-backend/internal/repositories" + redisRepo "wm-backend/internal/repositories/redis" "wm-backend/pkg/helper" "wm-backend/response" db "wm-backend/sqlc_gen" @@ -120,9 +123,51 @@ func Login(c *gin.Context) error { return nil } - // 5. Return token + // 5. Cache roles & permissions in Redis + go cacheUserRBAC(user.ID) + + // 6. Return token response.Ok(c, "Login successful", responses.BodyLoginResponse{ Token: token, }) return nil } + +// cacheUserRBAC fetches roles and permissions for a user and stores them in Redis. +func cacheUserRBAC(userID string) { + ctx := context.Background() + + roles, err := repositories.GetUserRolesByUserID(ctx, global.Queries, userID) + if err != nil { + log.Error().Err(err).Str("userID", userID).Msg("Failed to fetch roles for caching") + return + } + + permissions, err := repositories.GetPermissionsByUserID(ctx, global.Queries, userID) + if err != nil { + log.Error().Err(err).Str("userID", userID).Msg("Failed to fetch permissions for caching") + return + } + + roleItems := make([]redisRepo.RoleCacheItem, 0, len(roles)) + permNames := make([]string, 0, len(permissions)) + for _, r := range roles { + roleItems = append(roleItems, redisRepo.RoleCacheItem{ + ID: r.ID, + Name: r.Name, + Description: r.Description, + }) + } + for _, p := range permissions { + permNames = append(permNames, p.Name) + } + + data := redisRepo.RBACCachedData{ + Roles: roleItems, + Permissions: permNames, + } + + if err := redisRepo.CacheUserPermissions(ctx, userID, data, 60*time.Minute); err != nil { + log.Error().Err(err).Str("userID", userID).Msg("Failed to cache RBAC data") + } +} diff --git a/internal/services/profile_service.go b/internal/services/profile_service.go new file mode 100644 index 0000000..55f2bc4 --- /dev/null +++ b/internal/services/profile_service.go @@ -0,0 +1,121 @@ +package services + +import ( + "context" + "net/http" + "time" + + "wm-backend/global" + "wm-backend/internal/models/responses" + "wm-backend/internal/repositories" + redisRepo "wm-backend/internal/repositories/redis" + "wm-backend/response" + + "github.com/gin-gonic/gin" + "github.com/rs/zerolog/log" +) + +// GetProfile returns the authenticated user's profile including roles and permissions. +// It first attempts to read from Redis cache; on miss it queries the database. +// +// @Summary Get current user profile +// @Description Returns user info with roles and permissions (requires auth) +// @Tags auth +// @Security BearerAuth +// @Produce json +// @Success 200 {object} response.SuccessResponse{data=responses.BodyProfileResponse} +// @Failure 401 {object} response.ErrorResponse +// @Router /profile [get] +func GetProfile(c *gin.Context) error { + userID := c.GetString("user_id") + ctx := c.Request.Context() + + var roles []responses.RoleItem + var permissions []string + + // 1. Try Redis cache first (graceful fallback on error) + cachedData, found := redisRepo.GetCachedUserPermissions(ctx, userID) + if found { + // Cache hit - refresh TTL + redisRepo.RefreshTTL(ctx, userID, 60*time.Minute) + roles = make([]responses.RoleItem, 0, len(cachedData.Roles)) + for _, r := range cachedData.Roles { + roles = append(roles, responses.RoleItem{ + ID: r.ID, + Name: r.Name, + Description: r.Description, + }) + } + permissions = cachedData.Permissions + } else { + // 2. Cache miss - fetch from DB + dbRoles, err := repositories.GetUserRolesByUserID(ctx, global.Queries, userID) + if err != nil { + log.Error().Err(err).Str("userID", userID).Msg("Failed to fetch roles from DB") + response.InternalServerError(c, http.StatusInternalServerError) + return nil + } + roles = make([]responses.RoleItem, 0, len(dbRoles)) + for _, r := range dbRoles { + roles = append(roles, responses.RoleItem{ + ID: r.ID, + Name: r.Name, + Description: r.Description, + }) + } + + dbPerms, err := repositories.GetPermissionsByUserID(ctx, global.Queries, userID) + if err != nil { + log.Error().Err(err).Str("userID", userID).Msg("Failed to fetch permissions from DB") + response.InternalServerError(c, http.StatusInternalServerError) + return nil + } + permissions = make([]string, 0, len(dbPerms)) + for _, p := range dbPerms { + permissions = append(permissions, p.Name) + } + + // 3. Save to Redis cache for future requests (use background context) + cacheRoles := make([]redisRepo.RoleCacheItem, 0, len(dbRoles)) + for _, r := range dbRoles { + cacheRoles = append(cacheRoles, redisRepo.RoleCacheItem{ + ID: r.ID, + Name: r.Name, + Description: r.Description, + }) + } + go func() { + bgCtx := context.Background() + if err := redisRepo.CacheUserPermissions(bgCtx, userID, redisRepo.RBACCachedData{ + Roles: cacheRoles, + Permissions: permissions, + }, 60*time.Minute); err != nil { + log.Error().Err(err).Str("userID", userID).Msg("Failed to cache RBAC data in profile") + } + }() + } + + // 4. Fetch user info from DB + user, err := repositories.GetUserByID(ctx, global.Queries, userID) + if err != nil { + log.Error().Err(err).Str("userID", userID).Msg("Failed to fetch user from DB") + response.InternalServerError(c, http.StatusInternalServerError) + return nil + } + if user == nil { + response.NotFoundError(c, http.StatusNotFound, "User not found") + return nil + } + + // 5. Return response + response.Ok(c, "Profile fetched", responses.BodyProfileResponse{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + FullName: user.FullName, + IsActive: user.IsActive, + Roles: roles, + Permissions: permissions, + }) + return nil +} diff --git a/pkg/helper/jwt.go b/pkg/helper/jwt.go index f1ce5d0..b310e20 100644 --- a/pkg/helper/jwt.go +++ b/pkg/helper/jwt.go @@ -6,6 +6,7 @@ import ( "wm-backend/global" "github.com/golang-jwt/jwt/v5" + "github.com/rs/zerolog/log" ) // GenerateToken tạo JWT token cho user @@ -21,19 +22,23 @@ func GenerateToken(userID string) (string, error) { } func ParseToken(tokenString string) (jwt.MapClaims, error) { + log.Debug().Str("token", tokenString).Msg("Parsing JWT token") token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(global.Cfg.JWT.SecretKey), nil // <-- lấy từ config }) + log.Debug().Interface("token", token).Msg("Parsed JWT token object") if err != nil { return nil, err } + log.Debug().Interface("claims", token.Claims).Msg("Parsed JWT claims") if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + log.Debug().Interface("claims", claims).Msg("Valid JWT claims extracted") return claims, nil } - + log.Error().Msg("Invalid JWT token: claims not valid or token not valid") return nil, fmt.Errorf("invalid token") } diff --git a/sqlc_gen/invoice_item.sql.go b/sqlc_gen/invoice_item.sql.go new file mode 100644 index 0000000..f622683 --- /dev/null +++ b/sqlc_gen/invoice_item.sql.go @@ -0,0 +1,197 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: invoice_item.sql + +package db + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const createInvoiceItem = `-- name: CreateInvoiceItem :one +INSERT INTO invoice_items (invoice_id,component_id,original_component_id, required_quantity,actual_quantity, is_substituted, is_short, shortage_quantity, note, metadata) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10 +) +RETURNING id, invoice_id, component_id, original_component_id, required_quantity, actual_quantity, is_substituted, is_short, shortage_quantity, note, metadata +` + +type CreateInvoiceItemParams struct { + InvoiceID int64 `db:"invoice_id" json:"invoiceId"` + ComponentID int64 `db:"component_id" json:"componentId"` + OriginalComponentID pgtype.Int8 `db:"original_component_id" json:"originalComponentId"` + RequiredQuantity int32 `db:"required_quantity" json:"requiredQuantity"` + ActualQuantity int32 `db:"actual_quantity" json:"actualQuantity"` + IsSubstituted bool `db:"is_substituted" json:"isSubstituted"` + IsShort bool `db:"is_short" json:"isShort"` + ShortageQuantity int32 `db:"shortage_quantity" json:"shortageQuantity"` + Note pgtype.Text `db:"note" json:"note"` + Metadata []byte `db:"metadata" json:"metadata"` +} + +func (q *Queries) CreateInvoiceItem(ctx context.Context, arg CreateInvoiceItemParams) (InvoiceItem, error) { + row := q.db.QueryRow(ctx, createInvoiceItem, + arg.InvoiceID, + arg.ComponentID, + arg.OriginalComponentID, + arg.RequiredQuantity, + arg.ActualQuantity, + arg.IsSubstituted, + arg.IsShort, + arg.ShortageQuantity, + arg.Note, + arg.Metadata, + ) + var i InvoiceItem + err := row.Scan( + &i.ID, + &i.InvoiceID, + &i.ComponentID, + &i.OriginalComponentID, + &i.RequiredQuantity, + &i.ActualQuantity, + &i.IsSubstituted, + &i.IsShort, + &i.ShortageQuantity, + &i.Note, + &i.Metadata, + ) + return i, err +} + +const deleteInvoiceItem = `-- name: DeleteInvoiceItem :execrows +DELETE FROM invoice_items +WHERE id = $1 +` + +func (q *Queries) DeleteInvoiceItem(ctx context.Context, id int64) (int64, error) { + result, err := q.db.Exec(ctx, deleteInvoiceItem, id) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} + +const getInvoiceItemByID = `-- name: GetInvoiceItemByID :one +SELECT id, invoice_id, component_id, original_component_id, required_quantity, actual_quantity, is_substituted, is_short, shortage_quantity, note, metadata FROM invoice_items +WHERE id = $1 +` + +func (q *Queries) GetInvoiceItemByID(ctx context.Context, id int64) (InvoiceItem, error) { + row := q.db.QueryRow(ctx, getInvoiceItemByID, id) + var i InvoiceItem + err := row.Scan( + &i.ID, + &i.InvoiceID, + &i.ComponentID, + &i.OriginalComponentID, + &i.RequiredQuantity, + &i.ActualQuantity, + &i.IsSubstituted, + &i.IsShort, + &i.ShortageQuantity, + &i.Note, + &i.Metadata, + ) + return i, err +} + +const listInvoiceItems = `-- name: ListInvoiceItems :many +SELECT id, invoice_id, component_id, original_component_id, required_quantity, actual_quantity, is_substituted, is_short, shortage_quantity, note, metadata FROM invoice_items +` + +func (q *Queries) ListInvoiceItems(ctx context.Context) ([]InvoiceItem, error) { + rows, err := q.db.Query(ctx, listInvoiceItems) + if err != nil { + return nil, err + } + defer rows.Close() + var items []InvoiceItem + for rows.Next() { + var i InvoiceItem + if err := rows.Scan( + &i.ID, + &i.InvoiceID, + &i.ComponentID, + &i.OriginalComponentID, + &i.RequiredQuantity, + &i.ActualQuantity, + &i.IsSubstituted, + &i.IsShort, + &i.ShortageQuantity, + &i.Note, + &i.Metadata, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const updateInvoiceItem = `-- name: UpdateInvoiceItem :one +UPDATE invoice_items +SET required_quantity = coalesce($1, required_quantity), + actual_quantity = coalesce($2, actual_quantity), + is_substituted = coalesce($3, is_substituted), + is_short = coalesce($4, is_short), + shortage_quantity = coalesce($5, shortage_quantity), + note = coalesce($6, note), + metadata = coalesce($7, metadata) +WHERE id = $8 +RETURNING id, invoice_id, component_id, original_component_id, required_quantity, actual_quantity, is_substituted, is_short, shortage_quantity, note, metadata +` + +type UpdateInvoiceItemParams struct { + RequiredQuantity int32 `db:"required_quantity" json:"requiredQuantity"` + ActualQuantity int32 `db:"actual_quantity" json:"actualQuantity"` + IsSubstituted bool `db:"is_substituted" json:"isSubstituted"` + IsShort bool `db:"is_short" json:"isShort"` + ShortageQuantity int32 `db:"shortage_quantity" json:"shortageQuantity"` + Note pgtype.Text `db:"note" json:"note"` + Metadata []byte `db:"metadata" json:"metadata"` + ID int64 `db:"id" json:"id"` +} + +func (q *Queries) UpdateInvoiceItem(ctx context.Context, arg UpdateInvoiceItemParams) (InvoiceItem, error) { + row := q.db.QueryRow(ctx, updateInvoiceItem, + arg.RequiredQuantity, + arg.ActualQuantity, + arg.IsSubstituted, + arg.IsShort, + arg.ShortageQuantity, + arg.Note, + arg.Metadata, + arg.ID, + ) + var i InvoiceItem + err := row.Scan( + &i.ID, + &i.InvoiceID, + &i.ComponentID, + &i.OriginalComponentID, + &i.RequiredQuantity, + &i.ActualQuantity, + &i.IsSubstituted, + &i.IsShort, + &i.ShortageQuantity, + &i.Note, + &i.Metadata, + ) + return i, err +} diff --git a/sqlc_gen/permission.sql.go b/sqlc_gen/permission.sql.go new file mode 100644 index 0000000..2f5003a --- /dev/null +++ b/sqlc_gen/permission.sql.go @@ -0,0 +1,46 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: permission.sql + +package db + +import ( + "context" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) + +const getPermissionsByUserID = `-- name: GetPermissionsByUserID :many +SELECT DISTINCT p.name, p.description +FROM user_roles ur +JOIN role_permissions rp ON rp.role_id = ur.role_id +JOIN permissions p ON p.id = rp.permission_id +WHERE ur.user_id = $1 +` + +type GetPermissionsByUserIDRow struct { + Name string `db:"name" json:"name"` + Description pgtype.Text `db:"description" json:"description"` +} + +func (q *Queries) GetPermissionsByUserID(ctx context.Context, userID uuid.UUID) ([]GetPermissionsByUserIDRow, error) { + rows, err := q.db.Query(ctx, getPermissionsByUserID, userID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetPermissionsByUserIDRow + for rows.Next() { + var i GetPermissionsByUserIDRow + if err := rows.Scan(&i.Name, &i.Description); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/sqlc_gen/querier.go b/sqlc_gen/querier.go index f77ac41..5f616a7 100644 --- a/sqlc_gen/querier.go +++ b/sqlc_gen/querier.go @@ -24,6 +24,7 @@ type Querier interface { CreateInvoice(ctx context.Context, arg CreateInvoiceParams) (Invoice, error) CreateInvoiceConfig(ctx context.Context, arg CreateInvoiceConfigParams) (InvoiceConfig, error) CreateInvoiceConfigItem(ctx context.Context, arg CreateInvoiceConfigItemParams) (InvoiceConfigItem, error) + CreateInvoiceItem(ctx context.Context, arg CreateInvoiceItemParams) (InvoiceItem, error) CreateRole(ctx context.Context, arg CreateRoleParams) (Role, error) CreateRoom(ctx context.Context, arg CreateRoomParams) (Room, error) CreateShelve(ctx context.Context, arg CreateShelveParams) (Shelf, error) @@ -39,6 +40,7 @@ type Querier interface { DeleteInvoice(ctx context.Context, id int64) (int64, error) DeleteInvoiceConfig(ctx context.Context, id int64) (int64, error) DeleteInvoiceConfigItem(ctx context.Context, id int64) (int64, error) + DeleteInvoiceItem(ctx context.Context, id int64) (int64, error) DeleteRole(ctx context.Context, id uuid.UUID) (int64, error) DeleteRoom(ctx context.Context, id int64) (int64, error) DeleteShelve(ctx context.Context, id int64) (int64, error) @@ -55,6 +57,8 @@ type Querier interface { GetInvoiceByID(ctx context.Context, id int64) (Invoice, error) GetInvoiceConfigByID(ctx context.Context, id int64) (InvoiceConfig, error) GetInvoiceConfigItemByID(ctx context.Context, id int64) (InvoiceConfigItem, error) + GetInvoiceItemByID(ctx context.Context, id int64) (InvoiceItem, error) + GetPermissionsByUserID(ctx context.Context, userID uuid.UUID) ([]GetPermissionsByUserIDRow, error) GetRoleByID(ctx context.Context, id uuid.UUID) (Role, error) GetRoomByID(ctx context.Context, id int64) (Room, error) GetShelveByID(ctx context.Context, id int64) (Shelf, error) @@ -74,6 +78,7 @@ type Querier interface { ListContainers(ctx context.Context) ([]Container, error) ListInvoiceConfigItems(ctx context.Context) ([]InvoiceConfigItem, error) ListInvoiceConfigs(ctx context.Context) ([]InvoiceConfig, error) + ListInvoiceItems(ctx context.Context) ([]InvoiceItem, error) ListInvoices(ctx context.Context) ([]Invoice, error) ListRoles(ctx context.Context) ([]Role, error) ListRooms(ctx context.Context) ([]Room, error) @@ -93,6 +98,7 @@ type Querier interface { UpdateInvoice(ctx context.Context, arg UpdateInvoiceParams) (Invoice, error) UpdateInvoiceConfig(ctx context.Context, arg UpdateInvoiceConfigParams) (InvoiceConfig, error) UpdateInvoiceConfigItem(ctx context.Context, arg UpdateInvoiceConfigItemParams) (InvoiceConfigItem, error) + UpdateInvoiceItem(ctx context.Context, arg UpdateInvoiceItemParams) (InvoiceItem, error) UpdateRole(ctx context.Context, arg UpdateRoleParams) (Role, error) UpdateRoom(ctx context.Context, arg UpdateRoomParams) (Room, error) UpdateShelve(ctx context.Context, arg UpdateShelveParams) (Shelf, error)