Files
bssapp/svc/routes/role_department_permissions.go
2026-02-19 01:34:56 +03:00

706 lines
16 KiB
Go

package routes
import (
"bssapp-backend/auth"
"bssapp-backend/internal/auditlog"
"bssapp-backend/permissions"
"bssapp-backend/queries"
"database/sql"
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"github.com/gorilla/mux"
"github.com/lib/pq"
)
type IdTitleOption struct {
ID string `json:"id"`
Title string `json:"title"`
}
type Row struct {
Route string `json:"route"`
CanAccess bool `json:"can_access"`
}
type RoleDeptPermissionSummary struct {
RoleID int `json:"role_id"`
RoleTitle string `json:"role_title"`
DepartmentCode string `json:"department_code"`
DepartmentTitle string `json:"department_title"`
ModuleFlags map[string]bool `json:"module_flags"`
}
type ModuleLookupOption struct {
Value string `json:"value"`
Label string `json:"label"`
}
type ModuleActionLookupOption struct {
ModuleCode string `json:"module_code"`
Action string `json:"action"`
}
type RoleDeptPermissionListResponse struct {
Modules []ModuleLookupOption `json:"modules"`
ModuleActions []ModuleActionLookupOption `json:"module_actions"`
Rows []RoleDeptPermissionSummary `json:"rows"`
}
type RoleDepartmentPermissionHandler struct {
DB *sql.DB
Repo *permissions.RoleDepartmentPermissionRepo
}
func NewRoleDepartmentPermissionHandler(db *sql.DB) *RoleDepartmentPermissionHandler {
return &RoleDepartmentPermissionHandler{
DB: db, // Added
Repo: permissions.NewRoleDepartmentPermissionRepo(db),
}
}
/* ======================================================
LIST
====================================================== */
func (h *RoleDepartmentPermissionHandler) List(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
search := strings.TrimSpace(r.URL.Query().Get("search"))
modRows, err := h.DB.Query(queries.GetModuleLookup)
if err != nil {
http.Error(w, "module lookup error", http.StatusInternalServerError)
return
}
defer modRows.Close()
modules := make([]ModuleLookupOption, 0, 32)
for modRows.Next() {
var m ModuleLookupOption
if err := modRows.Scan(&m.Value, &m.Label); err != nil {
http.Error(w, "module lookup scan error", http.StatusInternalServerError)
return
}
modules = append(modules, m)
}
if err := modRows.Err(); err != nil {
http.Error(w, "module lookup rows error", http.StatusInternalServerError)
return
}
actionRows, err := h.DB.Query(queries.GetModuleActionLookup)
if err != nil {
http.Error(w, "module action lookup error", http.StatusInternalServerError)
return
}
defer actionRows.Close()
moduleActions := make([]ModuleActionLookupOption, 0, 128)
for actionRows.Next() {
var a ModuleActionLookupOption
if err := actionRows.Scan(&a.ModuleCode, &a.Action); err != nil {
http.Error(w, "module action scan error", http.StatusInternalServerError)
return
}
moduleActions = append(moduleActions, a)
}
if err := actionRows.Err(); err != nil {
http.Error(w, "module action rows error", http.StatusInternalServerError)
return
}
rows, err := h.DB.Query(queries.ListRoleDepartmentPermissionSets, search)
if err != nil {
http.Error(w, "db error", http.StatusInternalServerError)
return
}
defer rows.Close()
list := make([]RoleDeptPermissionSummary, 0, 128)
for rows.Next() {
var item RoleDeptPermissionSummary
var rawFlags []byte
if err := rows.Scan(
&item.RoleID,
&item.RoleTitle,
&item.DepartmentCode,
&item.DepartmentTitle,
&rawFlags,
); err != nil {
http.Error(w, "scan error", http.StatusInternalServerError)
return
}
item.ModuleFlags = map[string]bool{}
if len(rawFlags) > 0 {
if err := json.Unmarshal(rawFlags, &item.ModuleFlags); err != nil {
http.Error(w, "module flags parse error", http.StatusInternalServerError)
return
}
}
list = append(list, item)
}
if err := rows.Err(); err != nil {
http.Error(w, "rows error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(RoleDeptPermissionListResponse{
Modules: modules,
ModuleActions: moduleActions,
Rows: list,
})
}
/* ======================================================
GET
====================================================== */
func (h *RoleDepartmentPermissionHandler) Get(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
vars := mux.Vars(r)
roleID, err := strconv.Atoi(vars["roleId"])
if err != nil || roleID <= 0 {
http.Error(w, "invalid roleId", http.StatusBadRequest)
return
}
deptCode := vars["deptCode"]
if deptCode == "" {
http.Error(w, "invalid deptCode", http.StatusBadRequest)
return
}
list, err := h.Repo.Get(roleID, deptCode)
if err != nil {
http.Error(w, "db error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(list)
}
/* ======================================================
POST
====================================================== */
func (h *RoleDepartmentPermissionHandler) Save(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
vars := mux.Vars(r)
roleID, err := strconv.Atoi(vars["roleId"])
if err != nil || roleID <= 0 {
http.Error(w, "invalid roleId", http.StatusBadRequest)
return
}
deptCode := vars["deptCode"]
if deptCode == "" {
http.Error(w, "invalid deptCode", http.StatusBadRequest)
return
}
var list []permissions.RoleDepartmentPermission
if err := json.NewDecoder(r.Body).Decode(&list); err != nil {
http.Error(w, "bad payload", http.StatusBadRequest)
return
}
// ================= OLD =================
oldRows, err := h.Repo.Get(roleID, deptCode)
if err != nil {
log.Println("OLD PERM LOAD ERROR:", err)
}
oldMap := map[string]bool{}
for _, p := range oldRows {
key := p.Module + ":" + p.Action
oldMap[key] = p.Allowed
}
// ================= DIFF =================
var changes []map[string]any
for _, p := range list {
key := p.Module + ":" + p.Action
oldVal := oldMap[key]
if oldVal != p.Allowed {
changes = append(changes, map[string]any{
"module": p.Module,
"action": p.Action,
"before": oldVal,
"after": p.Allowed,
})
}
}
// ================= SAVE =================
if err := h.Repo.Save(roleID, deptCode, list); err != nil {
http.Error(w, "save error", http.StatusInternalServerError)
return
}
// ================= AUDIT =================
if len(changes) > 0 {
auditlog.Enqueue(r.Context(), auditlog.ActivityLog{
ActionType: "role_department_permission_change",
ActionCategory: "role_permission",
ActionTarget: fmt.Sprintf(
"/api/roles/%d/departments/%s/permissions",
roleID,
deptCode,
),
Description: "role+department permissions updated",
Username: claims.Username,
RoleCode: claims.RoleCode,
DfUsrID: int64(claims.ID),
ChangeBefore: map[string]any{
"permissions": oldRows,
},
ChangeAfter: map[string]any{
"changes": changes,
},
IsSuccess: true,
})
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
func GetModuleLookupRoute(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query(queries.GetModuleLookup)
if err != nil {
http.Error(w, "db error", 500)
return
}
defer rows.Close()
type Row struct {
Value string `json:"value"`
Label string `json:"label"`
}
var list []Row
for rows.Next() {
var r Row
if err := rows.Scan(
&r.Value,
&r.Label,
); err != nil {
http.Error(w, "scan error", 500)
return
}
list = append(list, r)
}
json.NewEncoder(w).Encode(list)
}
}
func GetRolesForPermissionSelectRoute(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
rows, err := db.Query(queries.GetRolesForPermissionSelect)
if err != nil {
http.Error(w, "roles select error", 500)
return
}
defer rows.Close()
list := make([]IdTitleOption, 0)
for rows.Next() {
var o IdTitleOption
if err := rows.Scan(&o.ID, &o.Title); err == nil {
list = append(list, o)
}
}
_ = json.NewEncoder(w).Encode(list)
}
}
func GetDepartmentsForPermissionSelectRoute(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
rows, err := db.Query(queries.GetDepartmentsForPermissionSelect)
if err != nil {
http.Error(w, "departments select error", 500)
return
}
defer rows.Close()
list := make([]IdTitleOption, 0)
for rows.Next() {
var o IdTitleOption
if err := rows.Scan(&o.ID, &o.Title); err == nil {
list = append(list, o)
}
}
_ = json.NewEncoder(w).Encode(list)
}
}
func GetUsersForPermissionSelectRoute(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
rows, err := db.Query(queries.GetUserLookupForPermission)
if err != nil {
http.Error(w, "users lookup error", http.StatusInternalServerError)
return
}
defer rows.Close()
list := make([]IdTitleOption, 0, 128)
for rows.Next() {
var o IdTitleOption
if err := rows.Scan(&o.ID, &o.Title); err == nil {
list = append(list, o)
}
}
_ = json.NewEncoder(w).Encode(list)
}
}
func (h *PermissionHandler) GetUserOverrides(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
userID, err := strconv.ParseInt(mux.Vars(r)["id"], 10, 64)
if err != nil || userID <= 0 {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
list, err := h.Repo.GetUserOverridesByUserID(userID)
if err != nil {
log.Println("USER OVERRIDE LOAD ERROR:", err)
http.Error(w, "db error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
_ = json.NewEncoder(w).Encode(list)
}
type routePermissionSeed struct {
Module string
Action string
Path string
}
type moduleActionSeed struct {
Module string
Action string
}
type permissionSnapshot struct {
user map[string]bool
roleDept map[string]bool
role map[string]bool
}
func permissionKey(module, action string) string {
return module + "|" + action
}
func loadPermissionSnapshot(
db *sql.DB,
userID int64,
roleID int64,
deptCodes []string,
) (permissionSnapshot, error) {
snapshot := permissionSnapshot{
user: make(map[string]bool, 128),
roleDept: make(map[string]bool, 128),
role: make(map[string]bool, 128),
}
userRows, err := db.Query(`
SELECT module_code, action, allowed
FROM mk_sys_user_permissions
WHERE user_id = $1
`, userID)
if err != nil {
return snapshot, err
}
for userRows.Next() {
var module, action string
var allowed bool
if err := userRows.Scan(&module, &action, &allowed); err != nil {
_ = userRows.Close()
return snapshot, err
}
snapshot.user[permissionKey(module, action)] = allowed
}
if err := userRows.Err(); err != nil {
_ = userRows.Close()
return snapshot, err
}
_ = userRows.Close()
if len(deptCodes) > 0 {
roleDeptRows, err := db.Query(`
SELECT module_code, action, BOOL_OR(allowed) AS allowed
FROM vw_role_dept_permissions
WHERE role_id = $1
AND department_code = ANY($2)
GROUP BY module_code, action
`,
roleID,
pq.Array(deptCodes),
)
if err != nil {
return snapshot, err
}
for roleDeptRows.Next() {
var module, action string
var allowed bool
if err := roleDeptRows.Scan(&module, &action, &allowed); err != nil {
_ = roleDeptRows.Close()
return snapshot, err
}
snapshot.roleDept[permissionKey(module, action)] = allowed
}
if err := roleDeptRows.Err(); err != nil {
_ = roleDeptRows.Close()
return snapshot, err
}
_ = roleDeptRows.Close()
}
roleRows, err := db.Query(`
SELECT module_code, action, allowed
FROM mk_sys_role_permissions
WHERE role_id = $1
`, roleID)
if err != nil {
return snapshot, err
}
for roleRows.Next() {
var module, action string
var allowed bool
if err := roleRows.Scan(&module, &action, &allowed); err != nil {
_ = roleRows.Close()
return snapshot, err
}
snapshot.role[permissionKey(module, action)] = allowed
}
if err := roleRows.Err(); err != nil {
_ = roleRows.Close()
return snapshot, err
}
_ = roleRows.Close()
return snapshot, nil
}
func resolvePermissionFromSnapshot(
s permissionSnapshot,
module string,
action string,
) bool {
key := permissionKey(module, action)
if allowed, ok := s.user[key]; ok {
return allowed
}
if allowed, ok := s.roleDept[key]; ok {
return allowed
}
if allowed, ok := s.role[key]; ok {
return allowed
}
return false
}
func GetUserRoutePermissionsHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", 401)
return
}
snapshot, err := loadPermissionSnapshot(
db,
int64(claims.ID),
int64(claims.RoleID),
claims.DepartmentCodes,
)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
rows, err := db.Query(`
SELECT DISTINCT
module_code,
action,
path
FROM mk_sys_routes
`)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer rows.Close()
routeSeeds := make([]routePermissionSeed, 0, 128)
for rows.Next() {
var module, action, path string
if err := rows.Scan(
&module,
&action,
&path,
); err != nil {
continue
}
routeSeeds = append(routeSeeds, routePermissionSeed{
Module: module,
Action: action,
Path: path,
})
}
if err := rows.Err(); err != nil {
http.Error(w, err.Error(), 500)
return
}
list := make([]Row, 0, len(routeSeeds))
for _, route := range routeSeeds {
list = append(list, Row{
Route: route.Path,
CanAccess: resolvePermissionFromSnapshot(
snapshot,
route.Module,
route.Action,
),
})
}
_ = json.NewEncoder(w).Encode(list)
}
}
func GetMyEffectivePermissions(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
claims, ok := auth.GetClaimsFromContext(r.Context())
if !ok || claims == nil {
http.Error(w, "unauthorized", 401)
return
}
snapshot, err := loadPermissionSnapshot(
db,
int64(claims.ID),
int64(claims.RoleID),
claims.DepartmentCodes,
)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
all, err := db.Query(`
SELECT DISTINCT module_code, action
FROM mk_sys_routes
`)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
defer all.Close()
moduleActions := make([]moduleActionSeed, 0, 128)
for all.Next() {
var m, a string
if err := all.Scan(&m, &a); err != nil {
continue
}
moduleActions = append(moduleActions, moduleActionSeed{
Module: m,
Action: a,
})
}
if err := all.Err(); err != nil {
http.Error(w, err.Error(), 500)
return
}
type Row struct {
Module string `json:"module"`
Action string `json:"action"`
Allowed bool `json:"allowed"`
}
list := make([]Row, 0, len(moduleActions))
for _, item := range moduleActions {
list = append(list, Row{
Module: item.Module,
Action: item.Action,
Allowed: resolvePermissionFromSnapshot(
snapshot,
item.Module,
item.Action,
),
})
}
_ = json.NewEncoder(w).Encode(list)
}
}