142 lines
7.7 KiB
JavaScript
142 lines
7.7 KiB
JavaScript
// Render-blocking auth gate — included as the FIRST <head> element on every
|
|
// protected page so unauthenticated visitors are redirected before any markup
|
|
// paints (prevents pages like /gstr1 from being viewable by direct URL access).
|
|
(function(){
|
|
try{
|
|
// Session-only auth: login is mirrored into a SESSION COOKIE (no
|
|
// expires/max-age) at login time — cookies, unlike sessionStorage,
|
|
// are shared across every tab of the same browser, so Ctrl+Click /
|
|
// "open in new tab" doesn't force a fresh login. A true session cookie
|
|
// is still wiped by the browser itself when it fully closes (not just a
|
|
// tab), so closing the browser still always ends the session — same
|
|
// guarantee as before, just no longer a per-tab-only one.
|
|
// Defensive cleanup: remove any leftover localStorage entries from an
|
|
// even older version of this scheme, so a lingering copy can't grant access.
|
|
try{ localStorage.removeItem('token'); localStorage.removeItem('portal_user'); }catch(e){}
|
|
function getCookie(name){
|
|
var m=document.cookie.match(new RegExp('(?:^|; )'+name+'=([^;]*)'));
|
|
return m?decodeURIComponent(m[1]):null;
|
|
}
|
|
if(!sessionStorage.getItem('portal_token')){
|
|
var cTok=getCookie('portal_token'), cUser=getCookie('portal_user');
|
|
if(cTok){
|
|
sessionStorage.setItem('portal_token',cTok);
|
|
if(cUser) sessionStorage.setItem('portal_user',cUser);
|
|
}
|
|
}
|
|
var tok=sessionStorage.getItem('portal_token');
|
|
if(!tok){ location.replace('/'); return; }
|
|
|
|
// Token PRESENCE alone isn't enough — a token issued at login lives in
|
|
// storage indefinitely even after its 12h JWT expiry (nothing clears it
|
|
// on its own), so a tab left open/idle past that would otherwise render
|
|
// the full page and let the user act right up until an API call 401s.
|
|
// Decode the JWT payload (no verification needed client-side, just the
|
|
// exp claim) and bounce immediately if it's already expired.
|
|
function jwtExpired(t){
|
|
try{
|
|
var payload=JSON.parse(atob(t.split('.')[1].replace(/-/g,'+').replace(/_/g,'/')));
|
|
return !payload.exp || (Date.now()>=payload.exp*1000);
|
|
}catch(e){ return true; }
|
|
}
|
|
function killSession(){
|
|
sessionStorage.clear();document.cookie='portal_token=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT';document.cookie='portal_user=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT';;
|
|
location.replace('/');
|
|
}
|
|
if(jwtExpired(tok)){ killSession(); return; }
|
|
// Re-check periodically so a tab left open through expiry gets logged
|
|
// out on its own, instead of only discovering it on the next API call.
|
|
setInterval(function(){
|
|
var t=sessionStorage.getItem('portal_token');
|
|
if(!t||jwtExpired(t)) killSession();
|
|
}, 60000);
|
|
|
|
// Page → required module key. Pages not listed here are accessible to any
|
|
// authenticated user (e.g. the dashboard itself).
|
|
var PAGE_MODULES={
|
|
'/gstr1':'gstr1', '/gstr2':'gstr2', '/itc04':'itc04', '/business-master':'business-master', '/admin':'admin',
|
|
'/bom':'bom', '/items':'items',
|
|
'/production':'production-create', '/issue-production':'production-issue', '/receipt-production':'production-receipt', '/close-production':'production-close',
|
|
'/requirements':'production-requirements', '/batch-issuance':'production-batch-issuance', '/work-order':'production-work-order',
|
|
'/purchase-request':'purchase-request', '/purchase-quotation':'purchase-quotation', '/purchase-order':'purchase-order', '/grpo':'purchase-grpo',
|
|
'/approvals':'approvals', '/sap-approvals':'sap-approvals',
|
|
'/vendor-register':'vendors', '/register':'customers',
|
|
'/documents':'documents', '/reports':'reports',
|
|
'/project-form':['projects-new','projects-view'], '/project-approvals':'projects-approvals',
|
|
'/costing-pl':'finance-monthly', '/costing-comparison':'finance-comparison', '/cost-sheet':'finance-costsheet', '/balance-sheet':'finance-balancesheet', '/cash-flow':'finance-cashflow',
|
|
'/salary':'salary',
|
|
'/sales-orders':'sales-orders', '/sales-samples':'sales-samples', '/sales-reports':'sales-reports', '/sales-admin':'sales-admin',
|
|
};
|
|
// Backward-compat: a user granted the OLD broad key (e.g. 'production',
|
|
// before it was split into per-page sub-modules) still gets every new
|
|
// sub-key under it, so nobody silently loses access when this ships.
|
|
var LEGACY_BROAD_MODULES={production:'production-',purchase:'purchase-',costing:'finance-',projects:'projects-'};
|
|
var path=location.pathname.replace(/\/$/,'')||'/';
|
|
var reqRaw=PAGE_MODULES[path];
|
|
if(reqRaw){
|
|
var required=Array.isArray(reqRaw)?reqRaw:[reqRaw]; // any ONE of these keys is enough
|
|
var raw=sessionStorage.getItem('portal_user');
|
|
var user=raw?JSON.parse(raw):null;
|
|
var role=user&&user.role;
|
|
var isAdmin=role==='admin'||role==='sap_adder';
|
|
var mods=user&&Array.isArray(user.modules)?user.modules:null;
|
|
var allowed=isAdmin; // no modules explicitly granted → no access (was: unchecked = full access)
|
|
if(!allowed&&mods){
|
|
for(var i=0;i<required.length&&!allowed;i++){
|
|
var req=required[i];
|
|
if(mods.indexOf(req)>-1){ allowed=true; break; }
|
|
for(var broad in LEGACY_BROAD_MODULES){
|
|
if(mods.indexOf(broad)>-1&&req.indexOf(LEGACY_BROAD_MODULES[broad])===0){ allowed=true; break; }
|
|
}
|
|
}
|
|
}
|
|
// '/production' also opens the Pre-PWO Store Review screen — a pure
|
|
// Store reviewer has no reason to hold the broad 'production-create'
|
|
// module (that would ALSO grant full Production Order creation), so
|
|
// let anyone holding EITHER Pre-PWO approval step in (any perm) reach
|
|
// the page even without the module — production.html's own per-tab
|
|
// permission checks (CAN_SHARE_PREPWO/CAN_REVIEW_PREPWO) still gate
|
|
// what they can actually see/do once there.
|
|
if(!allowed&&path==='/production'){
|
|
var steps=user&&user.approvalSteps;
|
|
var prepwoKeys=['production_order:prepwo_share','production_order:prepwo_review'];
|
|
for(var pi=0;pi<prepwoKeys.length&&!allowed;pi++){
|
|
var pk=prepwoKeys[pi];
|
|
if(Array.isArray(steps)){
|
|
for(var si=0;si<steps.length&&!allowed;si++){
|
|
var s=steps[si];
|
|
if(typeof s==='string'){ if(s===pk)allowed=true; }
|
|
else if(s&&s.step===pk&&Array.isArray(s.perms)&&s.perms.length>0){ allowed=true; }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if(!allowed){ location.replace('/'); }
|
|
}
|
|
|
|
// '/wo-header-profiles' has no module of its own (it's gated purely by
|
|
// the 'work_order:approved_mgr_qa' approval step — the same step that
|
|
// already represents "QA Manager" for Work Order approval — so it isn't
|
|
// in PAGE_MODULES above). Admin always passes; everyone else needs any
|
|
// perm on that step. wo-header-profiles.html's own CAN_EDIT check
|
|
// further gates whether they can actually add/edit/delete once there.
|
|
if(path==='/wo-header-profiles'){
|
|
var raw2=sessionStorage.getItem('portal_user');
|
|
var user2=raw2?JSON.parse(raw2):null;
|
|
var isAdmin2=user2&&(user2.role==='admin'||user2.role==='sap_adder');
|
|
var allowed2=!!isAdmin2;
|
|
if(!allowed2){
|
|
var steps2=user2&&user2.approvalSteps;
|
|
if(Array.isArray(steps2)){
|
|
for(var qi=0;qi<steps2.length&&!allowed2;qi++){
|
|
var qs=steps2[qi];
|
|
if(typeof qs==='string'){ if(qs==='work_order:approved_mgr_qa')allowed2=true; }
|
|
else if(qs&&qs.step==='work_order:approved_mgr_qa'&&Array.isArray(qs.perms)&&qs.perms.length>0){ allowed2=true; }
|
|
}
|
|
}
|
|
}
|
|
if(!allowed2){ location.replace('/'); }
|
|
}
|
|
}catch(e){ location.replace('/'); }
|
|
})();
|