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) } }