mirror of
https://github.com/OpenSignLabs/OpenSign.git
synced 2026-09-22 09:34:54 +02:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6271fedcbf | ||
|
|
d3cb8d575c | ||
|
|
0c295961c3 | ||
|
|
5fbd1fc6f2 | ||
|
|
3f4007d564 | ||
|
|
b49dc6bc6a | ||
|
|
b325095d60 |
+22
-31
@@ -39,27 +39,9 @@ const mime = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function contentType(filePath) {
|
function contentType(filePath) {
|
||||||
// Files under /.well-known/ are JSON by convention (WebAuthn, apple-app-site-association, etc.)
|
|
||||||
// and are extensionless, so explicitly set application/json there.
|
|
||||||
const rel = path.relative(root, filePath).split(path.sep).join("/");
|
|
||||||
if (rel.startsWith(".well-known/")) {
|
|
||||||
return "application/json; charset=utf-8";
|
|
||||||
}
|
|
||||||
return mime[path.extname(filePath).toLowerCase()] || "application/octet-stream";
|
return mime[path.extname(filePath).toLowerCase()] || "application/octet-stream";
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyWellKnownCors(req, res, filePath) {
|
|
||||||
const rel = path.relative(root, filePath).split(path.sep).join("/");
|
|
||||||
if (!rel.startsWith(".well-known/")) return;
|
|
||||||
// .well-known/* must be readable cross-origin (e.g. WebAuthn related-origins
|
|
||||||
// requests come from any RP host). Echo the request origin when present so
|
|
||||||
// credentialed fetches still work; otherwise allow all.
|
|
||||||
const origin = req.headers.origin;
|
|
||||||
res.setHeader("Access-Control-Allow-Origin", origin || "*");
|
|
||||||
res.setHeader("Vary", "Origin");
|
|
||||||
res.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
|
|
||||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeJoin(reqPath) {
|
function safeJoin(reqPath) {
|
||||||
let decoded;
|
let decoded;
|
||||||
@@ -96,23 +78,25 @@ function streamFile(req, res, filePath, stats) {
|
|||||||
"Last-Modified": stats.mtime.toUTCString(),
|
"Last-Modified": stats.mtime.toUTCString(),
|
||||||
"Cache-Control": cacheControl(filePath)
|
"Cache-Control": cacheControl(filePath)
|
||||||
};
|
};
|
||||||
applyWellKnownCors(req, res, filePath);
|
|
||||||
if (req.method === "HEAD") {
|
if (req.method === "HEAD") {
|
||||||
res.writeHead(200, headers);
|
res.writeHead(200, headers);
|
||||||
return res.end();
|
return res.end();
|
||||||
}
|
}
|
||||||
const stream = fs.createReadStream(filePath);
|
const stream = fs.createReadStream(filePath);
|
||||||
stream.on("error", () => {
|
stream.on("error", (err) => {
|
||||||
|
console.error("Stream error:", err);
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.writeHead(500, { "Content-Type": "text/plain" });
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
||||||
return res.end("Internal Server Error");
|
res.end("Internal Server Error");
|
||||||
|
} else {
|
||||||
|
res.destroy();
|
||||||
}
|
}
|
||||||
res.destroy();
|
|
||||||
});
|
});
|
||||||
res.on("close", () => {
|
res.on("close", () => {
|
||||||
if (!stream.destroyed) stream.destroy();
|
if (!stream.destroyed) stream.destroy();
|
||||||
});
|
});
|
||||||
res.on("error", () => {
|
res.on("error", (err) => {
|
||||||
|
console.error("Response error:", err);
|
||||||
if (!stream.destroyed) stream.destroy();
|
if (!stream.destroyed) stream.destroy();
|
||||||
});
|
});
|
||||||
res.writeHead(200, headers);
|
res.writeHead(200, headers);
|
||||||
@@ -122,7 +106,7 @@ function streamFile(req, res, filePath, stats) {
|
|||||||
function sendIndex(req, res) {
|
function sendIndex(req, res) {
|
||||||
const indexPath = path.join(root, "index.html");
|
const indexPath = path.join(root, "index.html");
|
||||||
fs.stat(indexPath, (err, stats) => {
|
fs.stat(indexPath, (err, stats) => {
|
||||||
if (err || !stats.isFile()) {
|
if (err || !stats || !stats.isFile()) {
|
||||||
res.writeHead(500, { "Content-Type": "text/plain" });
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
||||||
return res.end("index.html not found");
|
return res.end("index.html not found");
|
||||||
}
|
}
|
||||||
@@ -133,14 +117,15 @@ function sendIndex(req, res) {
|
|||||||
const server = http.createServer((req, res) => {
|
const server = http.createServer((req, res) => {
|
||||||
if (req.method === "OPTIONS") {
|
if (req.method === "OPTIONS") {
|
||||||
const filePath = safeJoin(req.url || "/");
|
const filePath = safeJoin(req.url || "/");
|
||||||
if (filePath) applyWellKnownCors(req, res, filePath);
|
|
||||||
res.writeHead(204);
|
res.writeHead(204);
|
||||||
return res.end();
|
return res.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method !== "GET" && req.method !== "HEAD") {
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
||||||
res.writeHead(405, { Allow: "GET, HEAD, OPTIONS" });
|
res.writeHead(405, { Allow: "GET, HEAD, OPTIONS" });
|
||||||
return res.end();
|
return res.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
const reqUrl = req.url || "/";
|
const reqUrl = req.url || "/";
|
||||||
const filePath = safeJoin(reqUrl);
|
const filePath = safeJoin(reqUrl);
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
@@ -149,18 +134,24 @@ const server = http.createServer((req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs.stat(filePath, (err, stats) => {
|
fs.stat(filePath, (err, stats) => {
|
||||||
if (!err && stats.isFile()) {
|
if (err) {
|
||||||
|
// No file at this path → SPA fallback to index.html
|
||||||
|
return sendIndex(req, res);
|
||||||
|
}
|
||||||
|
if (stats.isFile()) {
|
||||||
return streamFile(req, res, filePath, stats);
|
return streamFile(req, res, filePath, stats);
|
||||||
}
|
}
|
||||||
if (!err && stats.isDirectory()) {
|
if (stats.isDirectory()) {
|
||||||
const indexInDir = path.join(filePath, "index.html");
|
const indexInDir = path.join(filePath, "index.html");
|
||||||
return fs.stat(indexInDir, (e, s) => {
|
return fs.stat(indexInDir, (dirErr, dirStats) => {
|
||||||
if (!e && s.isFile()) return streamFile(req, res, indexInDir, s);
|
if (!dirErr && dirStats && dirStats.isFile()) {
|
||||||
|
return streamFile(req, res, indexInDir, dirStats);
|
||||||
|
}
|
||||||
return sendIndex(req, res);
|
return sendIndex(req, res);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// No file at this path → SPA fallback to index.html
|
// Path exists but is neither file nor directory → SPA fallback
|
||||||
sendIndex(req, res);
|
return sendIndex(req, res);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,12 @@ const PrefillWidgets = ({ prefills = [], setPrefills, onNext }) => {
|
|||||||
>
|
>
|
||||||
<div className="py-3 px-[10px] op-card border-[1px] border-gray-400">
|
<div className="py-3 px-[10px] op-card border-[1px] border-gray-400">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-10 gap-y-4 w-full">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-10 gap-y-4 w-full">
|
||||||
{prefills.map((widget, index) => (
|
{[...prefills]
|
||||||
|
.sort((a, b) =>
|
||||||
|
a.pageNumber !== b.pageNumber
|
||||||
|
? a.pageNumber - b.pageNumber
|
||||||
|
: (a.yPosition ?? 0) - (b.yPosition ?? 0)
|
||||||
|
).map((widget, index) => (
|
||||||
<RenderWidgets
|
<RenderWidgets
|
||||||
key={widget.key}
|
key={widget.key}
|
||||||
showLabel
|
showLabel
|
||||||
|
|||||||
@@ -148,15 +148,25 @@ function PrefillWidgetModal(props) {
|
|||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
//latten the filtered array and exclude read-only widgets
|
// Flatten the filtered array, exclude read-only widgets,
|
||||||
const flatArray = filteredArray?.flatMap((page) =>
|
// carry yPosition for sorting, then sort by pageNumber asc → yPosition asc
|
||||||
page.pos
|
// (mirrors the newSignPos.sort in PdfRequestFiles so widgets appear in
|
||||||
.filter((widget) => !widget.options?.isReadOnly)
|
// the same top-to-bottom, page-1-first order as they do in the document)
|
||||||
.map((widget) => ({
|
const flatArray = filteredArray
|
||||||
widget,
|
?.flatMap((page) =>
|
||||||
pageNumber: page.pageNumber
|
page.pos
|
||||||
}))
|
.filter((widget) => !widget.options?.isReadOnly)
|
||||||
);
|
.map((widget) => ({
|
||||||
|
widget,
|
||||||
|
pageNumber: page.pageNumber,
|
||||||
|
yPosition: widget.yPosition ?? 0
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
?.sort((a, b) =>
|
||||||
|
a.pageNumber !== b.pageNumber
|
||||||
|
? a.pageNumber - b.pageNumber // primary: page order (page 1 first)
|
||||||
|
: a.yPosition - b.yPosition // secondary: top-to-bottom within page
|
||||||
|
);
|
||||||
|
|
||||||
return flatArray || [];
|
return flatArray || [];
|
||||||
}, [props.prefillData]);
|
}, [props.prefillData]);
|
||||||
|
|||||||
@@ -577,8 +577,8 @@ function PdfRequestFiles(
|
|||||||
setContractName("_Contactbook");
|
setContractName("_Contactbook");
|
||||||
setSignerUserId(contact?.objectId);
|
setSignerUserId(contact?.objectId);
|
||||||
handleTourStatus(isTourEnabled, contact?.TourStatus);
|
handleTourStatus(isTourEnabled, contact?.TourStatus);
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
console.log("err while getting tourstatus", err);
|
console.log("err while getting tourstatus", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ export default async function docxtopdf(req, res) {
|
|||||||
try {
|
try {
|
||||||
// ---- Auth: current user ----
|
// ---- Auth: current user ----
|
||||||
const userRes = await axios.get(`${serverUrl}/users/me`, { headers: sessionHeader });
|
const userRes = await axios.get(`${serverUrl}/users/me`, { headers: sessionHeader });
|
||||||
|
const uploadedSizeBytes = req.file.size ?? req.file.buffer.length;
|
||||||
|
|
||||||
// ---- contracts_Users ----
|
// ---- contracts_Users ----
|
||||||
const whereUser = JSON.stringify({
|
const whereUser = JSON.stringify({
|
||||||
|
|||||||
@@ -425,7 +425,27 @@ async function PDF(req) {
|
|||||||
className = 'contracts_Users';
|
className = 'contracts_Users';
|
||||||
signUser = _resDoc.ExtUserPtr;
|
signUser = _resDoc.ExtUserPtr;
|
||||||
}
|
}
|
||||||
|
// Strict-order gating: when both `SendinOrder` and `SendInOrderStrict`
|
||||||
|
// are enabled the document creator wants the signing flow locked to a
|
||||||
|
// strict sequence — a signer/approver may only act once every previous
|
||||||
|
// signer/approver placeholder has a Signed/Approved audit entry. We
|
||||||
|
// skip this check entirely for the document owner (className=Users)
|
||||||
|
// because owners never sign through this path.
|
||||||
|
if (reqUserId && _resDoc?.SendinOrder === true && _resDoc?.SendInOrderStrict === true) {
|
||||||
|
const placeholders = Array.isArray(_resDoc?.Placeholders)
|
||||||
|
? _resDoc.Placeholders.filter(p => p?.Role !== 'prefill')
|
||||||
|
: [];
|
||||||
|
const myIdx = findPlaceholderIndex(placeholders, reqUserId);
|
||||||
|
if (myIdx > 0) {
|
||||||
|
const pendingId = findPendingPriorSigner(placeholders, myIdx, _resDoc?.AuditTrail);
|
||||||
|
if (pendingId) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.OPERATION_FORBIDDEN,
|
||||||
|
'Strict signing order is enabled — please wait for the previous signers to complete their action before signing.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const username = signUser.Name;
|
const username = signUser.Name;
|
||||||
const userEmail = signUser.Email;
|
const userEmail = signUser.Email;
|
||||||
if (req.params.pdfFile) {
|
if (req.params.pdfFile) {
|
||||||
|
|||||||
Reference in New Issue
Block a user