706 lines
16 KiB
Go
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)
|
|
}
|
|
}
|