all the haul

This commit is contained in:
2026-04-07 23:56:31 +00:00
parent 87cce83030
commit 24f303355b
244 changed files with 33743 additions and 11249 deletions
+2
View File
@@ -12,6 +12,7 @@ import { unifi } from "./optima-api/modules/unifi";
import { procurement } from "./optima-api/modules/procurement";
import { sales } from "./optima-api/modules/sales";
import { cw } from "./optima-api/modules/cw";
import { schedule } from "./optima-api/modules/schedule";
export const optima = {
auth,
@@ -26,6 +27,7 @@ export const optima = {
procurement,
sales,
cw,
schedule,
};
/**
* @TODO
@@ -48,6 +48,7 @@ export interface CatalogItem {
id: string;
identifier?: string;
cwCatalogId?: number;
name?: string;
description?: string;
quantity?: number;
partNumber?: string;
+173 -74
View File
@@ -16,7 +16,7 @@ export interface SalesOpportunity {
stage?: { id?: number; name?: string } | null;
status?: { id?: number; name?: string } | null;
priority?: { id?: number; name?: string } | null;
rating?: { id?: number; name?: string } | null;
interest?: "HOT" | "WARM" | "COLD" | null;
source?: string | null;
campaign?: string | null;
primarySalesRep?: {
@@ -76,6 +76,7 @@ export interface SalesOpportunity {
customerPO?: string | null;
totalSalesTax?: number | null;
expectedSalesTaxRate?: number | null;
taxCodeDescription?: string | null;
location?: { id?: number; name?: string } | null;
department?: { id?: number; name?: string } | null;
expectedCloseDate?: string | null;
@@ -240,7 +241,7 @@ export const STATUS_ID_TO_KEY: Record<number, WorkflowStatusKey> =
Object.entries(WORKFLOW_STATUS_IDS).map(([k, v]) => [
v,
k as WorkflowStatusKey,
]),
])
) as Record<number, WorkflowStatusKey>;
/** Human-readable labels for each workflow status */
@@ -274,7 +275,7 @@ export const REOPENABLE_STATUSES: ReadonlySet<WorkflowStatusKey> = new Set([
/** Statuses where the quote has been confirmed (finalize is allowed) */
export const QUOTE_CONFIRMED_STATUSES: ReadonlySet<WorkflowStatusKey> = new Set(
["ConfirmedQuote", "ReadyToSend", "Active", "PendingWon", "PendingLost"],
["ConfirmedQuote", "ReadyToSend", "Active", "PendingWon", "PendingLost"]
);
export interface OpportunityType {
@@ -382,7 +383,7 @@ export interface CreateOpportunityBody {
name: string;
expectedCloseDate: string;
notes?: string;
rating?: { id: number };
interest?: "HOT" | "WARM" | "COLD" | null;
type?: { id: number };
stage?: { id: number };
status?: { id: number };
@@ -519,10 +520,11 @@ export interface MemberSalesMetrics {
export const sales = {
async fetchMetrics(accessToken: string): Promise<MemberSalesMetrics | null> {
const response = await api.get("/v1/sales/opportunities/metrics", {
params: { scope: "me" },
headers: { Authorization: `Bearer ${accessToken}` },
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return response.data?.data?.metrics ?? null;
return response.data?.data ?? null;
},
async fetchMe(accessToken: string, includeClosed: boolean = false) {
@@ -542,7 +544,7 @@ export const sales = {
page: number = 1,
search: string = "",
rpp: number = 30,
includeClosed: boolean = true,
includeClosed: boolean = true
) {
const params: Record<string, unknown> = { page, rpp };
if (search) params.search = search;
@@ -578,7 +580,7 @@ export const sales = {
async fetchOne(
accessToken: string,
identifier: string,
include?: ("notes" | "contacts" | "products" | "quotes")[],
include?: ("notes" | "contacts" | "products" | "quotes")[]
) {
const params: Record<string, string> = {};
if (include && include.length > 0) {
@@ -591,43 +593,49 @@ export const sales = {
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data;
},
async fetchForecasts(accessToken: string, identifier: string) {
const response = await api.get(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/forecasts`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/forecasts`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data;
},
async fetchProducts(accessToken: string, identifier: string) {
const response = await api.get(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/products`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/products`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data;
},
async fetchNotes(accessToken: string, identifier: string) {
const response = await api.get(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/notes`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/notes`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data;
},
@@ -635,16 +643,18 @@ export const sales = {
async createNote(
accessToken: string,
identifier: string,
data: { text: string; flagged?: boolean },
data: { text: string; flagged?: boolean }
) {
const response = await api.post(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/notes`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/notes`,
data,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data;
},
@@ -653,40 +663,46 @@ export const sales = {
accessToken: string,
identifier: string,
noteId: number,
data: { text?: string; flagged?: boolean },
data: { text?: string; flagged?: boolean }
) {
const response = await api.patch(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/notes/${noteId}`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/notes/${noteId}`,
data,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data;
},
async deleteNote(accessToken: string, identifier: string, noteId: number) {
const response = await api.delete(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/notes/${noteId}`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/notes/${noteId}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data;
},
async fetchContacts(accessToken: string, identifier: string) {
const response = await api.get(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/contacts`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/contacts`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data;
},
@@ -694,16 +710,18 @@ export const sales = {
async sequenceProducts(
accessToken: string,
identifier: string,
orderedIds: number[],
orderedIds: number[]
) {
const response = await api.patch(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/products/sequence`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/products/sequence`,
{ orderedIds },
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data;
},
@@ -711,16 +729,18 @@ export const sales = {
async addProduct(
accessToken: string,
identifier: string,
body: AddProductBody,
body: AddProductBody
) {
const response = await api.post(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/products`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/products`,
body,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data;
},
@@ -728,28 +748,32 @@ export const sales = {
async addSpecialOrder(
accessToken: string,
identifier: string,
body: SpecialOrderBody | SpecialOrderBody[],
body: SpecialOrderBody | SpecialOrderBody[]
) {
const response = await api.post(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/products/special-order`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/products/special-order`,
body,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data;
},
async fetchLaborOptions(accessToken: string, identifier: string) {
const response = await api.get(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/products/labor/options`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/products/labor/options`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data as {
status?: number;
@@ -761,13 +785,15 @@ export const sales = {
async addLabor(accessToken: string, identifier: string, body: AddLaborBody) {
const response = await api.post(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/products/labor`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/products/labor`,
body,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data;
},
@@ -776,16 +802,18 @@ export const sales = {
accessToken: string,
identifier: string,
productId: number,
body: EditOpportunityProductBody,
body: EditOpportunityProductBody
) {
const response = await api.patch(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/products/${productId}/edit`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/products/${productId}/edit`,
body,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data;
},
@@ -794,29 +822,33 @@ export const sales = {
accessToken: string,
identifier: string,
productId: number,
body: CancelOpportunityProductBody,
body: CancelOpportunityProductBody
) {
const response = await api.patch(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/products/${productId}/cancel`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/products/${productId}/cancel`,
body,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data;
},
async refreshOpportunity(accessToken: string, identifier: string) {
const response = await api.post(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/refresh`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/refresh`,
{},
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data;
},
@@ -824,7 +856,7 @@ export const sales = {
async updateOpportunity(
accessToken: string,
identifier: string,
body: Record<string, unknown>,
body: Record<string, unknown>
) {
const response = await api.patch(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}`,
@@ -833,7 +865,7 @@ export const sales = {
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data;
},
@@ -844,19 +876,21 @@ export const sales = {
options?: {
includeRegenData?: boolean;
includeRegenParams?: boolean;
},
}
) {
const params: Record<string, string> = {};
if (options?.includeRegenData) params.includeRegenData = "true";
if (options?.includeRegenParams) params.includeRegenParams = "true";
const response = await api.get(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/quotes`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/quotes`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data as {
status?: number;
@@ -873,16 +907,18 @@ export const sales = {
lineItemPricing?: boolean;
includeQuoteNarrative?: boolean;
includeItemNarratives?: boolean;
},
}
) {
const response = await api.post(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/quote/commit`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/quote/commit`,
body ?? {},
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data as {
status?: number;
@@ -905,14 +941,65 @@ export const sales = {
};
},
async previewQuote(accessToken: string, identifier: string, quoteId: string) {
async fetchQuoteNarrative(accessToken: string, identifier: string) {
const response = await api.get(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/quote/${encodeURIComponent(quoteId)}/preview`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/quotes/narrative`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
);
return response.data as {
status?: number;
message?: string;
data: {
quoteNarrative: string | null;
};
successful?: boolean;
};
},
async updateQuoteNarrative(
accessToken: string,
identifier: string,
quoteNarrative: string | null
) {
const response = await api.patch(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/quotes/narrative`,
{
quoteNarrative,
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
);
return response.data as {
status?: number;
message?: string;
data: {
quoteNarrative: string | null;
};
successful?: boolean;
};
},
async previewQuote(accessToken: string, identifier: string, quoteId: string) {
const response = await api.get(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/quote/${encodeURIComponent(quoteId)}/preview`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
);
return response.data as {
status?: number;
@@ -929,16 +1016,18 @@ export const sales = {
accessToken: string,
identifier: string,
quoteId: string,
fetchAction: "download" | "print",
fetchAction: "download" | "print"
) {
const response = await api.get(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/quote/${encodeURIComponent(quoteId)}/download`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/quote/${encodeURIComponent(quoteId)}/download`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
params: { fetchAction },
},
}
);
return response.data as {
status?: number;
@@ -955,12 +1044,14 @@ export const sales = {
async fetchQuoteDownloads(accessToken: string, identifier: string) {
const response = await api.get(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/quotes/downloads`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/quotes/downloads`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data as {
status?: number;
@@ -973,15 +1064,17 @@ export const sales = {
async deleteProduct(
accessToken: string,
identifier: string,
productId: number,
productId: number
) {
const response = await api.delete(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/products/${productId}`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/products/${productId}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data;
},
@@ -993,7 +1086,7 @@ export const sales = {
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data;
},
@@ -1002,12 +1095,14 @@ export const sales = {
async fetchWorkflowStatus(accessToken: string, identifier: string) {
const response = await api.get(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/workflow`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/workflow`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data as {
status?: number;
@@ -1021,16 +1116,18 @@ export const sales = {
accessToken: string,
identifier: string,
action: WorkflowAction,
payload: WorkflowActionPayload,
payload: WorkflowActionPayload
) {
const response = await api.post(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/workflow`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/workflow`,
{ action, payload },
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data as {
status?: number;
@@ -1043,18 +1140,20 @@ export const sales = {
async fetchWorkflowHistory(
accessToken: string,
identifier: string,
type?: string,
type?: string
) {
const params: Record<string, string> = {};
if (type) params.type = type;
const response = await api.get(
`/v1/sales/opportunities/opportunity/${encodeURIComponent(identifier)}/workflow/history`,
`/v1/sales/opportunities/opportunity/${encodeURIComponent(
identifier
)}/workflow/history`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data as {
status?: number;
+53
View File
@@ -0,0 +1,53 @@
import api from "../axios";
export interface ScheduleStatus {
id: string;
name: string;
color: string | null;
}
export interface ScheduleType {
id: string;
name: string;
displayColor: string | null;
}
export interface ScheduleSpan {
id: string;
scheduleSpanId: number | null;
spanDesc: string | null;
}
export interface ScheduleEntry {
id: string;
cwId: number;
memberId: string | null;
name: string;
description: string | null;
allDayFlag: boolean;
startDate: string | null;
endDate: string | null;
hoursScheduled: number | null;
status: ScheduleStatus | null;
type: ScheduleType | null;
scheduleSpan: ScheduleSpan | null;
}
export const schedule = {
async fetchMe(
accessToken: string,
start: Date,
end: Date,
): Promise<ScheduleEntry[]> {
const response = await api.get("/v1/schedule/@me", {
params: {
start: start.toISOString(),
end: end.toISOString(),
},
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
return (response.data?.data as ScheduleEntry[]) ?? [];
},
};
+28 -24
View File
@@ -1,25 +1,23 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const { mockGetRequestEvent, mockRedirect, mockAxiosPost, mockIo, mockApi } =
vi.hoisted(() => ({
mockGetRequestEvent: vi.fn(),
mockRedirect: vi.fn(),
mockAxiosPost: vi.fn(),
mockIo: vi.fn(),
mockApi: {
get: vi.fn(),
post: vi.fn(),
},
}));
vi.mock("$app/server", () => ({
getRequestEvent: mockGetRequestEvent,
const { mockRedirect, mockAxiosPost, mockIo, mockApi } = vi.hoisted(() => ({
mockRedirect: vi.fn(),
mockAxiosPost: vi.fn(),
mockIo: vi.fn(),
mockApi: {
get: vi.fn(),
post: vi.fn(),
},
}));
vi.mock("$env/static/public", () => ({
PUBLIC_API_URL: "https://api.example.com",
}));
vi.mock("$app/environment", () => ({
browser: false,
}));
vi.mock("@sveltejs/kit", () => ({
redirect: mockRedirect,
}));
@@ -45,24 +43,30 @@ describe("user module", () => {
vi.clearAllMocks();
});
it("isLoggedIn returns true when accessToken cookie exists", () => {
mockGetRequestEvent.mockReturnValueOnce({
it("isLoggedInServer returns true when accessToken cookie exists", () => {
const mockEvent = {
cookies: {
get: vi.fn().mockReturnValue("token"),
},
});
} as any;
expect(user.isLoggedIn()).toBe(true);
expect(user.isLoggedInServer(mockEvent)).toBe(true);
});
it("isLoggedIn returns false when accessToken cookie is missing", () => {
mockGetRequestEvent.mockReturnValueOnce({
it("isLoggedInServer returns false when accessToken cookie is missing", () => {
const mockEvent = {
cookies: {
get: vi.fn().mockReturnValue(undefined),
},
});
} as any;
expect(user.isLoggedIn()).toBe(false);
expect(user.isLoggedInServer(mockEvent)).toBe(false);
});
it("isLoggedIn throws an error when called without event", () => {
expect(() => user.isLoggedIn()).toThrow(
"user.isLoggedIn() should not be called without a request event. Use isLoggedInServer() instead."
);
});
it("refreshSession posts refresh header and returns token payload", async () => {
@@ -84,7 +88,7 @@ describe("user module", () => {
headers: {
"x-refresh-token": "refresh-123",
},
},
}
);
expect(result).toEqual({
accessToken: "new-access",
@@ -105,7 +109,7 @@ describe("user module", () => {
expect(mockApi.post).toHaveBeenCalledWith(
"/v1/user/@me/check-permission",
{ permissions: ["company.read"] },
{ headers: { Authorization: "Bearer token" } },
{ headers: { Authorization: "Bearer token" } }
);
});
+45 -83
View File
@@ -1,13 +1,31 @@
import { getRequestEvent } from "$app/server";
import { PUBLIC_API_URL } from "$env/static/public";
import { redirect, RequestEvent } from "@sveltejs/kit";
import { redirect, type RequestEvent } from "@sveltejs/kit";
import axios from "axios";
import api from "../axios";
import { io } from "socket.io-client";
import { browser } from "$app/environment";
export const user = {
isLoggedIn(): boolean {
const event = getRequestEvent();
// This function can't reliably check authentication status in client-side code
// since it doesn't have access to httpOnly cookies.
// It should be used only in server-side contexts like load functions.
if (browser) {
console.warn(
"user.isLoggedIn() called in browser context - this may not work as expected"
);
return false;
}
// In server context, this would need the request event to be passed in
// Rather than using getRequestEvent() which makes this module server-only
throw new Error(
"user.isLoggedIn() should not be called without a request event. Use isLoggedInServer() instead."
);
},
// Server-side version that takes a request event parameter
isLoggedInServer(event: RequestEvent): boolean {
const authToken = event.cookies.get("accessToken");
return !!authToken;
},
@@ -21,7 +39,7 @@ export const user = {
headers: {
"x-refresh-token": refreshToken,
},
},
}
)
).data.data;
@@ -55,96 +73,40 @@ export const user = {
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data;
},
/**
* @todo Get communication with server working and setup a key system so that the frontend can listen for a specific key from the backend so that nobody can poach off of login events.
*
* Note: This function no longer mutates SvelteKit request event/cookies asynchronously.
* It returns the tokens to the caller so the caller (within the same request lifecycle)
* can set cookies using the event object synchronously.
* Polls the API until the Microsoft OAuth callback has been processed and
* tokens are available, then returns them. Uses a simple HTTP long-poll
* pattern instead of socket.io — much more reliable through reverse proxies.
*/
async awaitAuthCallback(callbackKey: string): Promise<{
async awaitAuthCallback(callbackKey: string, socketUrl?: string): Promise<{
accessToken: string;
refreshToken: string;
}> {
const base = PUBLIC_API_URL || "";
const base = socketUrl ?? PUBLIC_API_URL ?? "";
const pollUrl = `${base}/v1/auth/callback/${encodeURIComponent(callbackKey)}`;
const timeoutMs = 5 * 60 * 1000; // 5 minutes
const intervalMs = 600;
const deadline = Date.now() + timeoutMs;
return new Promise((resolve, reject) => {
let settled = false;
const socket = io(`${base}/auth_callback`, {
transports: ["websocket"],
rejectUnauthorized: false,
});
const timeout = setTimeout(
() => {
if (settled) return;
settled = true;
try {
socket.disconnect();
} catch {}
reject(new Error("Timed out waiting for auth callback"));
},
5 * 60 * 1000,
); // 5 minutes
const handlePayload = (payload: any) => {
try {
const { accessToken, refreshToken } = payload ?? {};
if (accessToken && refreshToken) {
if (settled) return;
settled = true;
clearTimeout(timeout);
try {
socket.disconnect();
} catch {}
resolve({ accessToken, refreshToken });
}
} catch {
// ignore parse errors
}
};
socket.on("connect", () => {});
// listen for a specific callback key if provided
if (callbackKey) {
socket.on(`auth:login:callback:${callbackKey}`, handlePayload);
} else {
socket.on("auth-callback", handlePayload);
while (Date.now() < deadline) {
const res = await fetch(pollUrl);
if (res.status === 200) {
const body = await res.json() as { data: { accessToken: string; refreshToken: string } };
return body.data;
}
socket.on("message", console.log);
socket.on("connect_error", (err: any) => {
if (settled) return;
settled = true;
clearTimeout(timeout);
try {
socket.disconnect();
} catch {}
reject(err instanceof Error ? err : new Error("Socket connect_error"));
});
socket.on("error", (err: any) => {
if (settled) return;
settled = true;
clearTimeout(timeout);
try {
socket.disconnect();
} catch {}
reject(err instanceof Error ? err : new Error("Socket error"));
});
socket.on("disconnect", (reason: any) => {
if (settled) return;
settled = true;
clearTimeout(timeout);
reject(
new Error(
"Socket disconnected before auth was received: " + String(reason),
),
);
});
});
if (res.status !== 202) {
throw new Error(`Unexpected status ${res.status} from auth callback endpoint`);
}
// 202 = still pending — wait and retry
await new Promise((r) => setTimeout(r, intervalMs));
}
throw new Error("Timed out waiting for auth callback");
},
};
+10 -6
View File
@@ -4,6 +4,8 @@ import type { Role } from "./roles";
export interface User {
id: string;
name: string;
firstName?: string | null;
lastName?: string | null;
email: string;
login: string;
image?: string;
@@ -38,7 +40,7 @@ export const users = {
*/
async fetch(
accessToken: string,
identifier: string,
identifier: string
): Promise<{ data: User }> {
const response = await api.get(`/v1/user/users/${identifier}`, {
headers: {
@@ -58,10 +60,12 @@ export const users = {
identifier: string,
updates: {
name?: string;
firstName?: string | null;
lastName?: string | null;
image?: string;
roles?: string[];
permissions?: string[];
},
}
): Promise<{ data: User }> {
const response = await api.patch(`/v1/user/users/${identifier}`, updates, {
headers: {
@@ -77,7 +81,7 @@ export const users = {
*/
async delete(
accessToken: string,
identifier: string,
identifier: string
): Promise<{ data: User }> {
const response = await api.delete(`/v1/user/users/${identifier}`, {
headers: {
@@ -93,7 +97,7 @@ export const users = {
*/
async fetchRoles(
accessToken: string,
identifier: string,
identifier: string
): Promise<{ data: Role[] }> {
const response = await api.get(`/v1/user/users/${identifier}/roles`, {
headers: {
@@ -110,7 +114,7 @@ export const users = {
async checkPermissions(
accessToken: string,
identifier: string,
permissions: string[],
permissions: string[]
): Promise<{ data: { results: PermissionCheckResult[] } }> {
const response = await api.post(
`/v1/user/users/${identifier}/check-permission`,
@@ -119,7 +123,7 @@ export const users = {
headers: {
Authorization: `Bearer ${accessToken}`,
},
},
}
);
return response.data;
},
+7 -1
View File
@@ -1,6 +1,8 @@
/**
* Format an ISO date string into a human-readable locale date.
* Returns an empty string (or "—" when `dash` is true) for missing/invalid values.
* Date-only ISO strings (e.g. "2025-03-15T00:00:00.000Z") are interpreted as
* calendar dates to avoid off-by-one errors west of UTC.
*/
export function formatDate(
dateStr?: string | null,
@@ -9,7 +11,11 @@ export function formatDate(
const fallback = opts.dash ? "—" : "";
if (!dateStr) return fallback;
try {
return new Date(dateStr).toLocaleDateString("en-US", {
// Strip time portion for date-only fields to prevent timezone shift
const dateOnly = dateStr.split("T")[0];
const date = new Date(dateOnly + "T00:00:00");
if (isNaN(date.getTime())) return fallback;
return date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",