added authorizations in IT controller

This commit is contained in:
HARRIS BIN MUSLISHAM 2025-11-12 10:21:08 +08:00
parent d002954b69
commit fa75a383ba
5 changed files with 219 additions and 52 deletions

View File

@ -1,5 +1,9 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using PSTW_CentralSystem.DBContext;
using PSTW_CentralSystem.Models;
namespace PSTW_CentralSystem.Areas.IT.Controllers namespace PSTW_CentralSystem.Areas.IT.Controllers
{ {
@ -7,41 +11,69 @@ namespace PSTW_CentralSystem.Areas.IT.Controllers
[Authorize] [Authorize]
public class ApprovalDashboardController : Controller public class ApprovalDashboardController : Controller
{ {
public IActionResult Approval() private readonly CentralSystemContext _db;
private readonly UserManager<UserModel> _userManager;
public ApprovalDashboardController(CentralSystemContext db, UserManager<UserModel> userManager)
{ {
_db = db;
_userManager = userManager;
}
// ===== helpers =====
private int GetCurrentUserId() => int.Parse(_userManager.GetUserId(User)!);
private async Task<bool> IsItTeamAsync(int userId) =>
await _db.ItTeamMembers.AnyAsync(t => t.UserId == userId);
private async Task<bool> IsApproverInAnyFlowAsync(int userId) =>
await _db.ItApprovalFlows.AnyAsync(f =>
f.HodUserId == userId ||
f.GroupItHodUserId == userId ||
f.FinHodUserId == userId ||
f.MgmtUserId == userId);
private async Task<bool> IsRequestFormManagerAsync(int userId) =>
await _db.RequestFormManagers.AnyAsync(m => m.UserId == userId);
// ===== routes =====
// Approval is only available for approvers and IT team members
public async Task<IActionResult> Approval()
{
var uid = GetCurrentUserId();
var isAllowed = await IsItTeamAsync(uid) || await IsApproverInAnyFlowAsync(uid);
if (!isAllowed) return Forbid(); // or: return View("AccessDenied");
return View(); // ~/Areas/IT/Views/ApprovalDashboard/Approval.cshtml return View(); // ~/Areas/IT/Views/ApprovalDashboard/Approval.cshtml
} }
public IActionResult Create() // Assignings (Admin) is only available for Request Form Managers
public async Task<IActionResult> Admin()
{ {
return View(); // ~/Areas/IT/Views/ApprovalDashboard/Create.cshtml var uid = GetCurrentUserId();
}
public IActionResult MyRequests() var isManager = await IsRequestFormManagerAsync(uid);
{ if (!isManager) return Forbid(); // or: return View("AccessDenied");
return View(); // ~/Areas/IT/Views/ApprovalDashboard/MyRequests.cshtml
} return View(); // ~/Areas/IT/Views/ApprovalDashboard/Admin.cshtml
public IActionResult Admin()
{
return View(); // ~/Areas/IT/Views/ApprovalDashboard/MyRequests.cshtml
} }
// Open to any authenticated user
public IActionResult Create() => View(); // ~/Areas/IT/Views/ApprovalDashboard/Create.cshtml
public IActionResult MyRequests() => View(); // ~/Areas/IT/Views/ApprovalDashboard/MyRequests.cshtml
// Use the same gate as Approval (reviewing a specific request)
public IActionResult RequestReview(int statusId) public IActionResult RequestReview(int statusId)
{ {
ViewBag.StatusId = statusId; ViewBag.StatusId = statusId;
return View(); // ~/Areas/IT/Views/ApprovalDashboard/RequestReview.cshtml return View(); // ~/Areas/IT/Views/ApprovalDashboard/RequestReview.cshtml
} }
public IActionResult SectionB()
{
return View();
}
public IActionResult Edit() // Leave these open unless you want extra guards
{ public IActionResult SectionB() => View();
return View(); public IActionResult Edit() => View();
} public IActionResult SectionBEdit() => View();
public IActionResult SectionBEdit()
{
return View();
}
} }
} }

View File

@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace PSTW_CentralSystem.Areas.IT.Models
{
[Table("request_form_managers")]
public class RequestFormManager
{
public int Id { get; set; }
public int UserId { get; set; }
}
}

View File

@ -3,7 +3,7 @@
Layout = "~/Views/Shared/_Layout.cshtml"; Layout = "~/Views/Shared/_Layout.cshtml";
} }
<!-- Bootstrap Icons (kill this if you already include it in _Layout) --> <!-- Bootstrap Icons (remove if already added in _Layout) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
<style> <style>
@ -36,7 +36,7 @@
width: 110px; width: 110px;
} }
/* === IT Team nicer UI === */ /* === Lists & chips (shared by IT Team & Managers) === */
.it-card .card-header { .it-card .card-header {
background: #f8faff; background: #f8faff;
} }
@ -110,10 +110,10 @@
<div id="flowApp" style="max-width:1200px; margin:auto; font-size:13px;"> <div id="flowApp" style="max-width:1200px; margin:auto; font-size:13px;">
<!-- FLOWS CARD --> <!-- ===================== FLOWS CARD ===================== -->
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<h5 class="m-0"></h5> <h5 class="m-0">Approval Flows</h5>
<div> <div>
<button class="btn btn-primary btn-sm" @@click="openCreate">New Flow</button> <button class="btn btn-primary btn-sm" @@click="openCreate">New Flow</button>
<button class="btn btn-outline-secondary btn-sm ms-2" @@click="load">Refresh</button> <button class="btn btn-outline-secondary btn-sm ms-2" @@click="load">Refresh</button>
@ -140,13 +140,10 @@
<tr v-for="f in flows" :key="f.itApprovalFlowId"> <tr v-for="f in flows" :key="f.itApprovalFlowId">
<td>{{ f.itApprovalFlowId }}</td> <td>{{ f.itApprovalFlowId }}</td>
<td class="text-start">{{ f.flowName }}</td> <td class="text-start">{{ f.flowName }}</td>
<!-- show id + resolved name so admins aren't guessing -->
<td>{{ f.hodUserId ? `${f.hodUserId} — ${resolveUserName(f.hodUserId)}` : '-' }}</td> <td>{{ f.hodUserId ? `${f.hodUserId} — ${resolveUserName(f.hodUserId)}` : '-' }}</td>
<td>{{ f.groupItHodUserId ? `${f.groupItHodUserId} — ${resolveUserName(f.groupItHodUserId)}` : '-' }}</td> <td>{{ f.groupItHodUserId ? `${f.groupItHodUserId} — ${resolveUserName(f.groupItHodUserId)}` : '-' }}</td>
<td>{{ f.finHodUserId ? `${f.finHodUserId} — ${resolveUserName(f.finHodUserId)}` : '-' }}</td> <td>{{ f.finHodUserId ? `${f.finHodUserId} — ${resolveUserName(f.finHodUserId)}` : '-' }}</td>
<td>{{ f.mgmtUserId ? `${f.mgmtUserId} — ${resolveUserName(f.mgmtUserId)}` : '-' }}</td> <td>{{ f.mgmtUserId ? `${f.mgmtUserId} — ${resolveUserName(f.mgmtUserId)}` : '-' }}</td>
<td> <td>
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-sm btn-outline-primary" @@click="openEdit(f)">Edit</button> <button class="btn btn-sm btn-outline-primary" @@click="openEdit(f)">Edit</button>
@ -169,7 +166,7 @@
</div> </div>
</div> </div>
<!-- IT TEAM CARD --> <!-- ===================== IT TEAM CARD ===================== -->
<div class="card mt-3 it-card"> <div class="card mt-3 it-card">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<h6 class="m-0">IT Team Members</h6> <h6 class="m-0">IT Team Members</h6>
@ -184,7 +181,7 @@
</div> </div>
<div class="row g-3 align-items-start"> <div class="row g-3 align-items-start">
<!-- LEFT: Search + Available --> <!-- LEFT -->
<div class="col-md-7"> <div class="col-md-7">
<div class="input-group input-group-sm mb-2"> <div class="input-group input-group-sm mb-2">
<span class="input-group-text"><i class="bi bi-search"></i></span> <span class="input-group-text"><i class="bi bi-search"></i></span>
@ -196,13 +193,11 @@
<input type="checkbox" :value="u.id" v-model="itTeamUserIds"> <input type="checkbox" :value="u.id" v-model="itTeamUserIds">
<span class="it-name">{{ u.name }}</span> <span class="it-name">{{ u.name }}</span>
</label> </label>
<div v-if="!filteredUsers.length" class="text-muted small p-2"> <div v-if="!filteredUsers.length" class="text-muted small p-2">No users match your search.</div>
No users match your search.
</div>
</div> </div>
</div> </div>
<!-- RIGHT: Selected chips --> <!-- RIGHT -->
<div class="col-md-5"> <div class="col-md-5">
<div class="d-flex justify-content-between align-items-center mb-2"> <div class="d-flex justify-content-between align-items-center mb-2">
<strong>Selected ({{ selectedUsers.length }})</strong> <strong>Selected ({{ selectedUsers.length }})</strong>
@ -225,7 +220,61 @@
</div> </div>
</div> </div>
<!-- FLOW MODAL --> <!-- ===================== REQUEST FORM MANAGERS CARD ===================== -->
<div class="card mt-3 it-card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="m-0">Request Form Managers</h6>
<button class="btn btn-sm btn-primary" @@click="saveFormManagers" :disabled="savingManagers">
{{ savingManagers ? 'Saving…' : 'Save' }}
</button>
</div>
<div class="card-body">
<div class="text-muted mb-3">
<small>Select users who can access Assignings/Admin features (manage request assignments, etc.).</small>
</div>
<div class="row g-3 align-items-start">
<!-- LEFT -->
<div class="col-md-7">
<div class="input-group input-group-sm mb-2">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" class="form-control" placeholder="Search users by name…" v-model.trim="rfmSearch">
</div>
<div class="it-list">
<label v-for="u in filteredUsersForManagers" :key="'rfm-avail-'+u.id" class="it-item">
<input type="checkbox" :value="u.id" v-model="rfmUserIds">
<span class="it-name">{{ u.name }}</span>
</label>
<div v-if="!filteredUsersForManagers.length" class="text-muted small p-2">No users match your search.</div>
</div>
</div>
<!-- RIGHT -->
<div class="col-md-5">
<div class="d-flex justify-content-between align-items-center mb-2">
<strong>Selected ({{ selectedManagers.length }})</strong>
<button class="btn btn-link btn-sm text-decoration-none"
@@click="rfmUserIds = []"
:disabled="!selectedManagers.length">
Clear all
</button>
</div>
<div class="it-selected">
<span v-for="u in selectedManagers" :key="'rfm-sel-'+u.id" class="chip">
{{ u.name }}
<button class="chip-x" @@click="removeRfm(u.id)" aria-label="Remove">&times;</button>
</span>
<div v-if="!selectedManagers.length" class="text-muted small">Nobody selected yet.</div>
</div>
</div>
</div>
</div>
</div>
<!-- ===================== FLOW MODAL ===================== -->
<div class="modal fade" id="flowModal" tabindex="-1" aria-hidden="true" ref="modal"> <div class="modal fade" id="flowModal" tabindex="-1" aria-hidden="true" ref="modal">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
@ -296,13 +345,19 @@
data() { data() {
return { return {
flows: [], flows: [],
users: [], // all users (name only for display) users: [], // all users for display (id + name)
itTeamUserIds: [], // selected user IDs for IT team // IT Team state
itSearch: '', // search term itTeamUserIds: [],
itSearch: '',
savingTeam: false,
// Request Form Managers state
rfmUserIds: [],
rfmSearch: '',
savingManagers: false,
// UI state
busy: false, busy: false,
error: null, error: null,
saving: false, saving: false,
savingTeam: false,
formError: null, formError: null,
form: { form: {
itApprovalFlowId: null, itApprovalFlowId: null,
@ -316,6 +371,7 @@
}; };
}, },
computed: { computed: {
/* IT Team list filter */
filteredUsers() { filteredUsers() {
const q = (this.itSearch || '').toLowerCase(); const q = (this.itSearch || '').toLowerCase();
if (!q) return this.users; if (!q) return this.users;
@ -324,9 +380,20 @@
selectedUsers() { selectedUsers() {
const set = new Set(this.itTeamUserIds); const set = new Set(this.itTeamUserIds);
return this.users.filter(u => set.has(u.id)); return this.users.filter(u => set.has(u.id));
},
/* Managers list filter */
filteredUsersForManagers() {
const q = (this.rfmSearch || '').toLowerCase();
if (!q) return this.users;
return this.users.filter(u => (u.name || '').toLowerCase().includes(q));
},
selectedManagers() {
const set = new Set(this.rfmUserIds);
return this.users.filter(u => set.has(u.id));
} }
}, },
methods: { methods: {
/* ===== Flows ===== */
async load() { async load() {
try { try {
this.busy = true; this.error = null; this.busy = true; this.error = null;
@ -353,10 +420,6 @@
const res = await fetch('/ItRequestAPI/users'); const res = await fetch('/ItRequestAPI/users');
this.users = await res.json(); this.users = await res.json();
}, },
async loadItTeam() {
const r = await fetch('/ItRequestAPI/itTeam');
this.itTeamUserIds = await r.json(); // array<int>
},
resolveUserName(id) { resolveUserName(id) {
const u = this.users.find(x => x.id === id); const u = this.users.find(x => x.id === id);
return u ? u.name : '(unknown)'; return u ? u.name : '(unknown)';
@ -439,11 +502,12 @@
this.bsModal = new bootstrap.Modal(el); this.bsModal = new bootstrap.Modal(el);
this.bsModal.show(); this.bsModal.show();
}, },
closeModal() { closeModal() { if (this.bsModal) this.bsModal.hide(); },
if (this.bsModal) this.bsModal.hide();
}, /* ===== IT Team endpoints ===== */
removeIt(uid) { async loadItTeam() {
this.itTeamUserIds = this.itTeamUserIds.filter(id => id !== uid); const r = await fetch('/ItRequestAPI/itTeam');
this.itTeamUserIds = await r.json(); // array<int>
}, },
async saveItTeam() { async saveItTeam() {
try { try {
@ -463,10 +527,46 @@
} finally { } finally {
this.savingTeam = false; this.savingTeam = false;
} }
},
removeIt(uid) {
this.itTeamUserIds = this.itTeamUserIds.filter(id => id !== uid);
},
/* ===== Request Form Managers endpoints ===== */
async loadFormManagers() {
const r = await fetch('/ItRequestAPI/formManagers');
this.rfmUserIds = await r.json(); // array<int>
},
async saveFormManagers() {
try {
this.savingManagers = true;
const r = await fetch('/ItRequestAPI/formManagers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userIds: this.rfmUserIds })
});
if (!r.ok) {
const j = await r.json().catch(() => ({}));
throw new Error(j.message || `Save failed (${r.status})`);
}
alert('Request Form Managers updated.');
} catch (e) {
alert(e.message || 'Failed to update Request Form Managers.');
} finally {
this.savingManagers = false;
}
},
removeRfm(uid) {
this.rfmUserIds = this.rfmUserIds.filter(id => id !== uid);
} }
}, },
async mounted() { async mounted() {
await Promise.all([this.load(), this.loadUsers(), this.loadItTeam()]); await Promise.all([
this.load(),
this.loadUsers(),
this.loadItTeam(),
this.loadFormManagers()
]);
} }
}); });
flowApp.mount('#flowApp'); flowApp.mount('#flowApp');

View File

@ -765,6 +765,29 @@ namespace PSTW_CentralSystem.Controllers.API
} }
// GET: /ItRequestAPI/formManagers -> returns [userId, ...]
[HttpGet("formManagers")]
public async Task<IActionResult> GetFormManagers() =>
Ok(await _db.RequestFormManagers.Select(m => m.UserId).ToListAsync());
// POST: /ItRequestAPI/formManagers body: { userIds: [1,2,3] }
public class ManagersDto { public List<int> UserIds { get; set; } = new(); }
[HttpPost("formManagers")]
public async Task<IActionResult> SetFormManagers([FromBody] ManagersDto dto)
{
var all = _db.RequestFormManagers;
_db.RequestFormManagers.RemoveRange(all);
await _db.SaveChangesAsync();
foreach (var uid in dto.UserIds.Distinct())
_db.RequestFormManagers.Add(new RequestFormManager { UserId = uid });
await _db.SaveChangesAsync();
return Ok(new { message = "Request Form Managers updated." });
}
// -------------------------------------------------------------------- // --------------------------------------------------------------------
// GET: /ItRequestAPI/me // GET: /ItRequestAPI/me
// Pull snapshot from aspnetusers -> departments -> companies // Pull snapshot from aspnetusers -> departments -> companies

View File

@ -129,6 +129,7 @@ namespace PSTW_CentralSystem.DBContext
public DbSet<ItRequestStatus> ItRequestStatus { get; set; } public DbSet<ItRequestStatus> ItRequestStatus { get; set; }
public DbSet<ItRequestAssetInfo> ItRequestAssetInfo { get; set; } public DbSet<ItRequestAssetInfo> ItRequestAssetInfo { get; set; }
public DbSet<ItTeamMember> ItTeamMembers { get; set; } public DbSet<ItTeamMember> ItTeamMembers { get; set; }
public DbSet<RequestFormManager> RequestFormManagers { get; set; }