Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
0ad54e9
added tdx plug-in folder
bellao314 Feb 2, 2026
d94f77c
clean up tdx add on
bellao314 Feb 2, 2026
ffdc6d0
tdx ui work in progress
bellao314 Feb 8, 2026
721a458
added input field to Jobs page (will continue to work on auth and oth…
bellao314 Feb 8, 2026
dd319b5
linting
bellao314 Feb 8, 2026
aeb0d01
linting pt 2
bellao314 Feb 8, 2026
ed968b3
renamed functions and changed alert to Toast
bellao314 Feb 9, 2026
e7715e1
fixed Toast implementation
bellao314 Feb 9, 2026
cab05d4
Merge pull request #120 from oss-slu/118-tdx-part-1
bellao314 Feb 9, 2026
1d68636
Merge branch 'main' of https://github.com/oss-slu/core_desk into 127-…
bellao314 Feb 13, 2026
85a075f
renamed add-ons folder
bellao314 Feb 13, 2026
924053f
getting somewhere
bellao314 Feb 14, 2026
b517564
success
bellao314 Feb 17, 2026
ed49454
linting
bellao314 Feb 17, 2026
f697b8a
clean up UI
bellao314 Feb 17, 2026
2980258
update dueDate to be week from import
bellao314 Feb 18, 2026
6e912a6
filter by ID instead of index for Attributes
bellao314 Feb 18, 2026
ee15f21
edited description
bellao314 Feb 18, 2026
cc9ea1f
linting?
bellao314 Feb 19, 2026
4f91ea5
lintttt
bellao314 Feb 19, 2026
4f1b68f
remove jobOptions array
bellao314 Feb 19, 2026
bb136a8
toast for error catching
bellao314 Feb 23, 2026
27c45dd
remove comments
bellao314 Feb 23, 2026
2f2e89e
more error catching
bellao314 Feb 23, 2026
f699b2c
loading message while fetching ticket
bellao314 Feb 23, 2026
fca20e7
fix fetching ticket
bellao314 Feb 23, 2026
6675eaf
Merge pull request #131 from oss-slu/127-tdx-pt-2
bellao314 Feb 24, 2026
f808e60
experimenting with auth
bellao314 Feb 24, 2026
284909d
added SSO (will return as unauthorized for now)
bellao314 Feb 25, 2026
50ae2cb
lint app
bellao314 Feb 25, 2026
e779afa
lint api
bellao314 Feb 25, 2026
4c4b398
lint api pt 2
bellao314 Feb 25, 2026
d96e85c
correct hardcoded redirection; added console log
bellao314 Mar 2, 2026
4456443
update index.js
bellao314 Mar 3, 2026
d97130e
use env var for base url
bellao314 Mar 4, 2026
b4821b8
change sessionStorage to localStorage
bellao314 Mar 4, 2026
3ba3208
toast.promise
bellao314 Mar 4, 2026
cbed631
added tdx input as modal, just need to get it to render
bellao314 Mar 4, 2026
ac18d67
lint?
bellao314 Mar 4, 2026
83ba575
TDX modal renders, and text input does not re-render with each new ch…
bellao314 Mar 5, 2026
65d44f4
lint app
bellao314 Mar 8, 2026
3f23dbe
rename token and auto reload after auth
bellao314 Mar 9, 2026
c4c1ed9
Merge branch 'main' into tdx-integration
jackcrane Mar 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,68 @@

// app.use("/api", await router());

// Routes for TDX Web API

app.get('/api/tdx-auth', async (req, res) => {
const { shopId } = req.query;

try {
const response = await fetch(`${process.env.TDX_URL}/auth/loginSSO`);
const r = await response.text();

// gets token from html code returned from fetch (matches token characters)
// basically an extraction (thanks, Gemini)
// eslint-disable-next-line max-len
const t = r.match(/([A-Za-z0-9-_]{20,}\.){2}[A-Za-z0-9-_]{20,}|[A-Za-z0-9]{32,}/);

if (t) {
const token = t[0];

const destination = `${process.env.BASE_URL}/shops/${shopId}/jobs?token=${encodeURIComponent(token)}#`;

res.redirect(destination);
} else {
res.status(500).send("SSO Success, but no token found in HTML output.");
console.log(`${res.status(500)}: SSO Success, but no token found in HTML output.`);
}
} catch (error) {
res.status(500).send(error);

Check warning

Code scanning / CodeQL

Information exposure through a stack trace Medium

This information exposed to the user depends on
stack trace information
.

Copilot Autofix

AI 3 months ago

In general, to fix this problem you should avoid sending raw error objects or stack traces to the client. Instead, log the detailed error information on the server (e.g., using console.error or a logging library), and send a generic, user-friendly error message with an appropriate HTTP status code.

For this specific code, the best fix that preserves existing functionality is to update the /api/tdx-auth route’s catch block (lines 405–407). Instead of res.status(500).send(error);, log the error server-side (if not already logged) and send a generic error message such as "Internal Server Error" or a more domain-specific but non-revealing message. The rest of the behavior (status code 500, route logic) remains unchanged. Concretely:

  • In api/index.js, within the /api/tdx-auth route, modify the catch (error) block to:
    • Call console.error("TDX Auth Error:", error); (or similar) to keep detailed diagnostics on the server.
    • Respond with res.status(500).send("Internal Server Error"); (or equivalent) instead of sending error directly.

No new imports or external dependencies are needed; console is globally available in Node.

Suggested changeset 1
api/index.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/index.js b/api/index.js
--- a/api/index.js
+++ b/api/index.js
@@ -403,7 +403,8 @@
         console.log(`${res.status(500)}: SSO Success, but no token found in HTML output.`);
       }
     } catch (error) {
-      res.status(500).send(error);  
+      console.error("TDX Auth Error:", error);
+      res.status(500).send("Internal Server Error");
     }
   });
 
EOF
@@ -403,7 +403,8 @@
console.log(`${res.status(500)}: SSO Success, but no token found in HTML output.`);
}
} catch (error) {
res.status(500).send(error);
console.error("TDX Auth Error:", error);
res.status(500).send("Internal Server Error");
}
});

Copilot is powered by AI and may make mistakes. Always verify output.

Check warning

Code scanning / CodeQL

Exception text reinterpreted as HTML Medium

Exception text
is reinterpreted as HTML without escaping meta-characters.

Copilot Autofix

AI 3 months ago

To fix the problem, avoid sending the raw error object (or its full, unsanitized string representation) directly in the HTTP response. Instead, send a safe, generic error message to the client, and log the detailed error server-side for debugging. If you need to expose details, restrict them to non-user-controlled parts or properly escape them.

Concretely, in api/index.js within the /api/tdx-auth route’s catch block (lines 405–407), replace res.status(500).send(error); with a safe response such as res.status(500).send("Internal Server Error"); (or a JSON body with a generic error message), and additionally log error using console.error or an existing logging mechanism. This preserves functionality (the endpoint still returns 500 on failure) while eliminating the possibility of reflected XSS through exception text. No new imports or dependencies are required; we can use the existing console for logging.

Suggested changeset 1
api/index.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/index.js b/api/index.js
--- a/api/index.js
+++ b/api/index.js
@@ -403,7 +403,8 @@
         console.log(`${res.status(500)}: SSO Success, but no token found in HTML output.`);
       }
     } catch (error) {
-      res.status(500).send(error);  
+      console.error("TDX SSO Error:", error);
+      res.status(500).send("Internal Server Error");
     }
   });
 
EOF
@@ -403,7 +403,8 @@
console.log(`${res.status(500)}: SSO Success, but no token found in HTML output.`);
}
} catch (error) {
res.status(500).send(error);
console.error("TDX SSO Error:", error);
res.status(500).send("Internal Server Error");
}
});

Copilot is powered by AI and may make mistakes. Always verify output.
}
});

app.get('/api/tdx-ticket', async (req, res) => {
const { ticketId, token } = req.query;

if (!token) {
return res.status(400).json({error: "Token is required"});
}

if (!ticketId) {
return res.status(400).json({ error: "Ticket ID is required" });
}

try {
const ticketRes = await fetch(`${process.env.TDX_URL}/${process.env.TDX_APP_ID}/tickets/${ticketId}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
Comment on lines +422 to +427

Check failure

Code scanning / CodeQL

Server-side request forgery Critical

The
URL
of this request depends on a
user-provided value
.

Copilot Autofix

AI 3 months ago

To fix the problem, the user-controlled ticketId must be constrained so it cannot arbitrarily alter the URL path. The safest approach without changing functionality is to validate ticketId against a strict allow‑list pattern (for example, only allow alphanumeric characters and a few safe symbols) and reject requests that do not match. This ensures the request will always target /tickets/<well‑formed-id> and prevents path traversal or additional path segment injection.

Concretely, in api/index.js inside the /api/tdx-ticket route:

  1. After checking that ticketId is present (lines 417–419), add a format validation step, e.g., a regular expression such as /^[A-Za-z0-9_-]+$/ (or a stricter pattern if you know the exact ticket format). If ticketId fails this check, respond with 400 Bad Request.
  2. Use the validated ticketId when constructing the URL in the fetch call (line 422). Since we’re only validating and not transforming the value, existing successful use cases will continue to work as long as the IDs are well‑formed.

No new methods or external libraries are needed; we can rely on native RegExp and simple checks in the same file and function.

Suggested changeset 1
api/index.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/index.js b/api/index.js
--- a/api/index.js
+++ b/api/index.js
@@ -418,8 +418,15 @@
       return res.status(400).json({ error: "Ticket ID is required" });
     }
 
+    // Validate ticketId to prevent SSRF/path manipulation
+    const ticketIdString = String(ticketId);
+    const ticketIdPattern = /^[A-Za-z0-9_-]+$/;
+    if (!ticketIdPattern.test(ticketIdString)) {
+      return res.status(400).json({ error: "Invalid Ticket ID format" });
+    }
+
     try {
-      const ticketRes = await fetch(`${process.env.TDX_URL}/${process.env.TDX_APP_ID}/tickets/${ticketId}`, {
+      const ticketRes = await fetch(`${process.env.TDX_URL}/${process.env.TDX_APP_ID}/tickets/${ticketIdString}`, {
         headers: { 
           'Authorization': `Bearer ${token}`,
           'Content-Type': 'application/json'
EOF
@@ -418,8 +418,15 @@
return res.status(400).json({ error: "Ticket ID is required" });
}

// Validate ticketId to prevent SSRF/path manipulation
const ticketIdString = String(ticketId);
const ticketIdPattern = /^[A-Za-z0-9_-]+$/;
if (!ticketIdPattern.test(ticketIdString)) {
return res.status(400).json({ error: "Invalid Ticket ID format" });
}

try {
const ticketRes = await fetch(`${process.env.TDX_URL}/${process.env.TDX_APP_ID}/tickets/${ticketId}`, {
const ticketRes = await fetch(`${process.env.TDX_URL}/${process.env.TDX_APP_ID}/tickets/${ticketIdString}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
Copilot is powered by AI and may make mistakes. Always verify output.

if (!ticketRes.ok) {
return res.status(ticketRes.status).json({ error: "Ticket not found" });
}

const data = await ticketRes.json();

res.json(data);

} catch (error) {
console.error("TDX Proxy Error:", error);
res.status(500).json({ error: "Internal Server Error", details: error.message });
}
});

// This route is used for training purposes only. It is not used in production.
app.use((req, res, next) => {
if (req.url.includes("joke/emoji")) {
Expand Down
57 changes: 33 additions & 24 deletions api/routes/shop/[shopId]/job/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,45 +100,54 @@ export const post = [
}

if (onBehalfOfUserEmail) {
const user = await prisma.user.create({
data: {
email: onBehalfOfUserEmail,
firstName: onBehalfOfUserFirstName,
lastName: onBehalfOfUserLastName,
},
});

const shopsToJoin = await prisma.shop.findMany({
let user = await prisma.user.findFirst({
where: {
autoJoin: true,
email: onBehalfOfUserEmail,
},
});

for (const shop of shopsToJoin) {
await prisma.userShop.create({
if (!user) {
const user = await prisma.user.create({
data: {
userId: user.id,
shopId: shop.id,
active: true,
email: onBehalfOfUserEmail,
firstName: onBehalfOfUserFirstName,
lastName: onBehalfOfUserLastName,
},
});


const shopsToJoin = await prisma.shop.findMany({
where: {
autoJoin: true,
},
});

for (const shop of shopsToJoin) {
await prisma.userShop.create({
data: {
userId: user.id,
shopId: shop.id,
active: true,
},
});

await prisma.logs.create({
data: {
userId: user.id,
type: LogType.USER_CONNECTED_TO_SHOP,
shopId: shop.id,
},
});
}

await prisma.logs.create({
data: {
userId: user.id,
type: LogType.USER_CONNECTED_TO_SHOP,
shopId: shop.id,
type: LogType.USER_CREATED,
},
});
}

await prisma.logs.create({
data: {
userId: user.id,
type: LogType.USER_CREATED,
},
});

userToCreateJobAs = user.id;
}
}
Expand Down
1 change: 1 addition & 0 deletions app/src/hooks/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ export { useBillingGroup } from "./useBillingGroup";
export { useBillingGroupInvitations } from "./useBillingGroupInvitations";
export { useBillingGroupInvitation } from "./useBillingGroupInvitation";
export { useBillingGroupUser } from "./useBillingGroupUser";
export { useTDXTickets } from "./useTDXTickets";
export { useBillingGroupLedger } from "./useBillingGroupLedger";
export { useShopLedger } from "./useShopLedger";
81 changes: 81 additions & 0 deletions app/src/hooks/useJobs.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from "react";
import toast from "react-hot-toast";
import { authFetch } from "#url";
import { useModal } from "#modal";
import { Input, Spinner, Util, Switch, Card } from "tabler-react-2";
Expand All @@ -8,6 +9,7 @@ import { useUserShop } from "./useUserShop";
import { useAuth } from "#useAuth";
import { ShopUserPicker } from "#shopUserPicker";
import { BillingGroupPicker } from "../components/billingGroupPicker/BillingGroupPicker";
import { useTDXTickets } from "./useTDXTickets";

const toLocalDateInputValue = (date) => {
const localDate = new Date(date);
Expand Down Expand Up @@ -185,6 +187,69 @@ const CreateJobModalContent = ({
);
};

const CreateTDXImportModalContent = ({
onSubmit
}) => {
const {shopId} = useParams('');
const [auth] = useState({ tdxToken: localStorage.getItem('tdx_bearer_token') });
const [ticketId, setTicketId] = useState('');
const { loading, handleLogin, fetchTicket } = useTDXTickets();
if (!auth.tdxToken) {
return (
<div>
<div>
<Button
loading={loading}
onClick={async () => {
handleLogin(shopId);
}}
>
Log Into TDNext
</Button>
</div>
</div>
)
}
return (
<div className="gap-2 border-b-5 border-b-black">
<Input
value={ticketId}
onChange={(e) => setTicketId(e)}
label="TDNext Ticket ID"
placeholder="Ticket ID"
/>
<Button
loading={loading}
onClick={async () => {
const data = await fetchTicket(ticketId);
toast.promise(data, {
loading: "Fetching ticket from TDX...",
success: "Successfully imported ticket.",
error: "Error when fetching",
});
if (data && onSubmit) {
// Call onSubmit with positional arguments to match _createJob
onSubmit(
data.title,
data.description,
data.dueDate,
data.onBehalfOf,
data.onBehalfOfUserId,
data.onBehalfOfUserEmail,
data.onBehalfOfUserFirstName,
data.onBehalfOfUserLastName,
data.onBehalfOfBillingGroup,
data.onBehalfOfBillingGroupId
);
}
}}
>
Import Ticket
</Button>
</div>
);
};

export const useJobs = (shopId) => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
Expand Down Expand Up @@ -235,6 +300,11 @@ export const useJobs = (shopId) => {
text: <CreateJobModalContent onSubmit={_createJob} />,
});

const { modal: tdxModal, ModalElement: TDXModalElement } = useModal({
title: "Import from TDNext",
text: <CreateTDXImportModalContent onSubmit={_createJob}/>
});

const CreateSimpleSubPage = () => {
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
Expand Down Expand Up @@ -337,6 +407,15 @@ export const useJobs = (shopId) => {
});
};

const importTDXJob = async () => {
tdxModal({
title: "Import ticket from TDNext",
text: (
<CreateTDXImportModalContent onSubmit={_createJob} />
)
})
};

const updateJob = async (jobId, newJob) => {
const job = jobs.find((j) => j.id === jobId);
if (job.finalized) {
Expand Down Expand Up @@ -383,7 +462,9 @@ export const useJobs = (shopId) => {
refetch: fetchJobs,
CreateSimpleSubPage,
ModalElement,
TDXModalElement,
createJob,
importTDXJob,
opLoading,
updateJob,
};
Expand Down
99 changes: 99 additions & 0 deletions app/src/hooks/useTDXTickets.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { useState } from 'react';
import { authFetch } from "#url";
import toast from 'react-hot-toast';

export const useTDXTickets = () => {
const [auth, setAuth] = useState({ tdxToken: localStorage.getItem('tdx_bearer_token') });
const [ticketId, setTicketId] = useState('');
const [ticket, setTicket] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

const currentDate = new Date();
const nextWeekDate = new Date(currentDate);

const handleLogin = (shopId) => {
setLoading(true);

const relayUrl = `${import.meta.env.VITE_API_BASE_URL}/api/tdx-auth?shopId=${shopId}`; // update later
const loginWindow = window.open(relayUrl, "OktaLogin", "width=500,height=600");

const timer = setInterval(() => {
try {
if (loginWindow.location.href.includes(window.location.hostname)) {
const urlParams = new URLSearchParams(loginWindow.location.search);
const token = urlParams.get('token');

if (token) {
localStorage.setItem('tdx_bearer_token', token);
setAuth({ tdxToken: token });

loginWindow.close();
clearInterval(timer);
setLoading(false);
toast.success("TDX Authenticated");
window.location.reload();
}
}
} catch (e) {
toast.error(e);
setError(e);
}
}, 1000);
};

const fetchTicket = async (id) => {
var ticket;

if (!id) {
toast.error("Please enter a Ticket ID");
return null;
}

try {
// Pointing to your proxy endpoint
try {
const r = await authFetch(`/api/tdx-ticket?ticketId=${id}&token=${auth.tdxToken}`);
ticket = await r.json();
setTicket(ticket);
} catch (error) {
setError(error);
toast.error(`Ticket fetch failed: ${error}`);
}

// Extract values from TDX Attributes
// const costAttr = ticket?.Attributes.find(item => item.ID === 2312)?.Value || 0.00;
const descriptionAttr = `${ticket?.Attributes.find(item => item.ID === 2328)?.Value}; ${ticket?.Attributes.find(item => item.ID === 2311)?.Value}`;

// Map the TDX response to the specific order _createJob expects
return {
title: ticket?.Title ? `TDX: ${ticket?.Title}` : "No Title Available",
description: descriptionAttr,
dueDate: nextWeekDate.setDate(currentDate.getDate() + 7), // Defaulting to week from current day
onBehalfOf: true,
onBehalfOfUserId: ticket?.RequestorUID || '',
onBehalfOfUserEmail: ticket?.RequestorEmail || '',
onBehalfOfUserFirstName: ticket?.RequestorFullName?.split(" ")[0] || "Unknown",
onBehalfOfUserLastName: ticket?.RequestorFullName?.split(" ")[1] || "Unknown",
onBehalfOfBillingGroup: false, // Defaulting as per your previous state
onBehalfOfBillingGroupId: null
};
} catch (error) {
setError(error.message);
toast.error(error.message);
return null;
} finally {
setLoading(false);
}
};

return {
ticket,
ticketId,
setTicketId,
loading,
handleLogin,
fetchTicket,
error
};
};
Loading
Loading