feat: implement workspace session management and enhance API proxy handling
This commit is contained in:
parent
4f0a2f790f
commit
ccb9eacd86
10
apps/dashboard/.env.local.example
Normal file
10
apps/dashboard/.env.local.example
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Fallback backend URL — only used if the per-session `api_base_url` cookie
|
||||||
|
# is not set. The SaaS → /activate/[token] handoff sets that cookie, and the
|
||||||
|
# server proxy at /api/proxy/* forwards to whatever URL it contains.
|
||||||
|
NEXT_PUBLIC_API_URL=http://localhost:8001
|
||||||
|
|
||||||
|
# Used by the "Workspace session not found" page to link back to the SaaS.
|
||||||
|
NEXT_PUBLIC_SAAS_URL=http://localhost:8000
|
||||||
|
|
||||||
|
# Image remotePatterns wildcard for SaaS-provisioned subdomains.
|
||||||
|
NEXT_PUBLIC_GARAGE_HOST_PATTERN=*.reparee.com
|
||||||
100
apps/dashboard/app/activate/[token]/page.tsx
Normal file
100
apps/dashboard/app/activate/[token]/page.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { redirect } from "next/navigation"
|
||||||
|
import { AlertCircle, ShieldCheck } from "lucide-react"
|
||||||
|
|
||||||
|
import { applyHandoff } from "@/modules/auth/auth.actions"
|
||||||
|
|
||||||
|
type SearchParams = { ws?: string; api?: string }
|
||||||
|
|
||||||
|
export default async function ActivatePage(props: {
|
||||||
|
params: Promise<{ token: string }>
|
||||||
|
searchParams: Promise<SearchParams>
|
||||||
|
}) {
|
||||||
|
const { token } = await props.params
|
||||||
|
const { ws, api } = await props.searchParams
|
||||||
|
|
||||||
|
if (!ws || !api) {
|
||||||
|
return (
|
||||||
|
<ActivationError
|
||||||
|
title="Activation link is incomplete"
|
||||||
|
detail="The link is missing the workspace identifier or backend URL. Please open the garage again from the SaaS dashboard."
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: URL
|
||||||
|
try {
|
||||||
|
parsed = new URL(api)
|
||||||
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||||
|
throw new Error("Backend URL must be http(s).")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return (
|
||||||
|
<ActivationError
|
||||||
|
title="Activation link is invalid"
|
||||||
|
detail="The backend URL provided in the activation link is malformed."
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = `${parsed.toString().replace(/\/$/, "")}/api/saas/handoff/exchange`
|
||||||
|
|
||||||
|
let response: Response
|
||||||
|
try {
|
||||||
|
response = await fetch(endpoint, {
|
||||||
|
method: "POST",
|
||||||
|
cache: "no-store",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
"X-Workspace-UUID": ws,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
handoff_token: token,
|
||||||
|
saas_workspace_uuid: ws,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
return (
|
||||||
|
<ActivationError
|
||||||
|
title="Cannot reach the garage backend"
|
||||||
|
detail={(err as Error).message}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload: { token?: string; user?: any; message?: string } = {}
|
||||||
|
try {
|
||||||
|
payload = await response.json()
|
||||||
|
} catch {
|
||||||
|
// ignore — handled below
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok || !payload.token || !payload.user) {
|
||||||
|
return (
|
||||||
|
<ActivationError
|
||||||
|
title="Activation failed"
|
||||||
|
detail={payload.message ?? `HTTP ${response.status}`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await applyHandoff(payload.token, payload.user, parsed.toString().replace(/\/$/, ""), ws)
|
||||||
|
|
||||||
|
redirect("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
function ActivationError({ title, detail }: { title: string; detail: string }) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-50 to-gray-100 p-6">
|
||||||
|
<div className="max-w-md w-full text-center space-y-3 bg-white rounded-xl shadow-sm border p-8">
|
||||||
|
<AlertCircle className="size-12 mx-auto text-rose-500" />
|
||||||
|
<h1 className="text-lg font-semibold">{title}</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">{detail}</p>
|
||||||
|
<p className="text-xs text-muted-foreground pt-2 flex items-center justify-center gap-1.5">
|
||||||
|
<ShieldCheck className="size-3.5" />
|
||||||
|
Re-open the garage from the SaaS dashboard to get a fresh link.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
97
apps/dashboard/app/api/proxy/[...path]/route.ts
Normal file
97
apps/dashboard/app/api/proxy/[...path]/route.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { cookies } from "next/headers"
|
||||||
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
|
|
||||||
|
const HOP_BY_HOP = new Set([
|
||||||
|
"host",
|
||||||
|
"connection",
|
||||||
|
"content-length",
|
||||||
|
"keep-alive",
|
||||||
|
"transfer-encoding",
|
||||||
|
"upgrade",
|
||||||
|
"te",
|
||||||
|
"trailer",
|
||||||
|
"proxy-authorization",
|
||||||
|
"proxy-authenticate",
|
||||||
|
])
|
||||||
|
|
||||||
|
async function forward(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
|
||||||
|
const cookieStore = await cookies()
|
||||||
|
const apiBase = cookieStore.get("api_base_url")?.value
|
||||||
|
const wsUuid = cookieStore.get("workspace_uuid")?.value
|
||||||
|
const token = cookieStore.get("auth_token")?.value
|
||||||
|
|
||||||
|
if (!apiBase || !wsUuid) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
message: "Workspace session not found. Please open your garage from the SaaS dashboard.",
|
||||||
|
code: "workspace_session_missing",
|
||||||
|
},
|
||||||
|
{ status: 412 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { path } = await ctx.params
|
||||||
|
const url = new URL(req.url)
|
||||||
|
// The OpenAPI clients keep `/api/...` in their route patterns, so when the
|
||||||
|
// client uses baseUrl=`/api/proxy` the resulting URL is
|
||||||
|
// `/api/proxy/api/customers`. Forward the path verbatim and rejoin so the
|
||||||
|
// upstream becomes `{apiBase}/api/customers`.
|
||||||
|
const target = `${apiBase.replace(/\/$/, "")}/${(path ?? []).join("/")}${url.search}`
|
||||||
|
|
||||||
|
const headers = new Headers()
|
||||||
|
req.headers.forEach((value, key) => {
|
||||||
|
if (!HOP_BY_HOP.has(key.toLowerCase()) && key.toLowerCase() !== "cookie") {
|
||||||
|
headers.set(key, value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
headers.set("Accept", "application/json")
|
||||||
|
headers.set("X-Workspace-UUID", wsUuid)
|
||||||
|
if (token) {
|
||||||
|
headers.set("Authorization", `Bearer ${token}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const init: RequestInit = {
|
||||||
|
method: req.method,
|
||||||
|
headers,
|
||||||
|
redirect: "manual",
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
||||||
|
init.body = await req.arrayBuffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
let upstream: Response
|
||||||
|
try {
|
||||||
|
upstream = await fetch(target, init)
|
||||||
|
} catch (err) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
message: "Failed to reach garage backend.",
|
||||||
|
code: "backend_unreachable",
|
||||||
|
error: (err as Error).message,
|
||||||
|
},
|
||||||
|
{ status: 502 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const respHeaders = new Headers()
|
||||||
|
upstream.headers.forEach((value, key) => {
|
||||||
|
const k = key.toLowerCase()
|
||||||
|
if (HOP_BY_HOP.has(k) || k === "set-cookie") return
|
||||||
|
respHeaders.set(key, value)
|
||||||
|
})
|
||||||
|
|
||||||
|
return new NextResponse(upstream.body, {
|
||||||
|
status: upstream.status,
|
||||||
|
statusText: upstream.statusText,
|
||||||
|
headers: respHeaders,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET = forward
|
||||||
|
export const POST = forward
|
||||||
|
export const PUT = forward
|
||||||
|
export const PATCH = forward
|
||||||
|
export const DELETE = forward
|
||||||
|
export const OPTIONS = forward
|
||||||
|
export const HEAD = forward
|
||||||
@ -5,8 +5,12 @@ import type { AuthUser } from "@garage/api"
|
|||||||
|
|
||||||
const TOKEN_COOKIE = "auth_token"
|
const TOKEN_COOKIE = "auth_token"
|
||||||
const USER_COOKIE = "auth_user"
|
const USER_COOKIE = "auth_user"
|
||||||
|
const WORKSPACE_COOKIE = "workspace_uuid"
|
||||||
|
const API_BASE_COOKIE = "api_base_url"
|
||||||
const DEFAULT_EXPIRES_IN = 60 * 60 * 24 * 7 // 7 days in seconds
|
const DEFAULT_EXPIRES_IN = 60 * 60 * 24 * 7 // 7 days in seconds
|
||||||
|
|
||||||
|
const isProd = process.env.NODE_ENV === "production"
|
||||||
|
|
||||||
export async function setAuthCookies(
|
export async function setAuthCookies(
|
||||||
token: string,
|
token: string,
|
||||||
user: AuthUser,
|
user: AuthUser,
|
||||||
@ -19,12 +23,16 @@ export async function setAuthCookies(
|
|||||||
expires,
|
expires,
|
||||||
path: "/",
|
path: "/",
|
||||||
sameSite: "strict",
|
sameSite: "strict",
|
||||||
|
httpOnly: true,
|
||||||
|
secure: isProd,
|
||||||
})
|
})
|
||||||
|
|
||||||
cookieStore.set(USER_COOKIE, JSON.stringify(user), {
|
cookieStore.set(USER_COOKIE, JSON.stringify(user), {
|
||||||
expires,
|
expires,
|
||||||
path: "/",
|
path: "/",
|
||||||
sameSite: "strict",
|
sameSite: "strict",
|
||||||
|
httpOnly: true,
|
||||||
|
secure: isProd,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,6 +40,8 @@ export async function clearAuthCookies() {
|
|||||||
const cookieStore = await cookies()
|
const cookieStore = await cookies()
|
||||||
cookieStore.delete(TOKEN_COOKIE)
|
cookieStore.delete(TOKEN_COOKIE)
|
||||||
cookieStore.delete(USER_COOKIE)
|
cookieStore.delete(USER_COOKIE)
|
||||||
|
cookieStore.delete(WORKSPACE_COOKIE)
|
||||||
|
cookieStore.delete(API_BASE_COOKIE)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAuthCookies(): Promise<{
|
export async function getAuthCookies(): Promise<{
|
||||||
@ -53,3 +63,65 @@ export async function getAuthCookies(): Promise<{
|
|||||||
|
|
||||||
return { token, user }
|
return { token, user }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the per-session workspace context: which garage backend to proxy to,
|
||||||
|
* and which workspace UUID to send on every upstream call. Written by the
|
||||||
|
* SaaS → garage-erp activation handoff.
|
||||||
|
*/
|
||||||
|
export async function setWorkspaceContext(
|
||||||
|
apiBase: string,
|
||||||
|
workspaceUuid: string,
|
||||||
|
expiresIn: number = DEFAULT_EXPIRES_IN,
|
||||||
|
) {
|
||||||
|
const cookieStore = await cookies()
|
||||||
|
const expires = new Date(Date.now() + expiresIn * 1000)
|
||||||
|
|
||||||
|
cookieStore.set(API_BASE_COOKIE, apiBase, {
|
||||||
|
expires,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
httpOnly: true,
|
||||||
|
secure: isProd,
|
||||||
|
})
|
||||||
|
|
||||||
|
cookieStore.set(WORKSPACE_COOKIE, workspaceUuid, {
|
||||||
|
expires,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
httpOnly: true,
|
||||||
|
secure: isProd,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWorkspaceContext(): Promise<{
|
||||||
|
apiBase: string | undefined
|
||||||
|
workspaceUuid: string | undefined
|
||||||
|
}> {
|
||||||
|
const cookieStore = await cookies()
|
||||||
|
return {
|
||||||
|
apiBase: cookieStore.get(API_BASE_COOKIE)?.value,
|
||||||
|
workspaceUuid: cookieStore.get(WORKSPACE_COOKIE)?.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearWorkspaceContext() {
|
||||||
|
const cookieStore = await cookies()
|
||||||
|
cookieStore.delete(WORKSPACE_COOKIE)
|
||||||
|
cookieStore.delete(API_BASE_COOKIE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server-action used by the SaaS handoff page. Wraps cookie writes for both
|
||||||
|
* the auth token/user and the workspace context atomically (from the client's
|
||||||
|
* point of view).
|
||||||
|
*/
|
||||||
|
export async function applyHandoff(
|
||||||
|
token: string,
|
||||||
|
user: AuthUser,
|
||||||
|
apiBase: string,
|
||||||
|
workspaceUuid: string,
|
||||||
|
) {
|
||||||
|
await setAuthCookies(token, user)
|
||||||
|
await setWorkspaceContext(apiBase, workspaceUuid)
|
||||||
|
}
|
||||||
|
|||||||
@ -20,6 +20,12 @@ const nextConfig = {
|
|||||||
hostname: 'reparee.test',
|
hostname: 'reparee.test',
|
||||||
port: '',
|
port: '',
|
||||||
pathname: '/**',
|
pathname: '/**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: process.env.NEXT_PUBLIC_GARAGE_HOST_PATTERN ?? '*.reparee.com',
|
||||||
|
port: '',
|
||||||
|
pathname: '/**',
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,25 @@
|
|||||||
import { createApi, } from "@garage/api";
|
import { createApi } from "@garage/api"
|
||||||
import { useAuthStore } from "./stores/auth-store";
|
import { getAuthCookies, getWorkspaceContext } from "@/modules/auth/auth.actions"
|
||||||
import { getAuthCookies } from "@/modules/auth/auth.actions";
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an authenticated API client for server-side use (server actions, RSC).
|
||||||
|
*
|
||||||
|
* Server-side calls bypass the /api/proxy route — the server already has
|
||||||
|
* cookie access and can talk to the workspace's repareenew install directly.
|
||||||
|
* Browser-side code should use the singleton `api` from @garage/api, which
|
||||||
|
* defaults to baseUrl="/api/proxy" and gets workspace context from cookies
|
||||||
|
* via the proxy route.
|
||||||
|
*/
|
||||||
export const getAuthApi = async () => {
|
export const getAuthApi = async () => {
|
||||||
const { token } = await getAuthCookies();
|
const { token } = await getAuthCookies()
|
||||||
console.log(`Auth Token: ${token}`);
|
const { apiBase, workspaceUuid } = await getWorkspaceContext()
|
||||||
const api = createApi({ headers: token ? { Authorization: `Bearer ${token}` } : undefined });
|
|
||||||
return api;
|
const headers: Record<string, string> = {}
|
||||||
|
if (token) headers.Authorization = `Bearer ${token}`
|
||||||
|
if (workspaceUuid) headers["X-Workspace-UUID"] = workspaceUuid
|
||||||
|
|
||||||
|
return createApi({
|
||||||
|
baseUrl: apiBase,
|
||||||
|
headers: Object.keys(headers).length ? headers : undefined,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
24
apps/dashboard/shared/components/WorkspaceSessionMissing.tsx
Normal file
24
apps/dashboard/shared/components/WorkspaceSessionMissing.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import Link from "next/link"
|
||||||
|
import { AlertCircle, ExternalLink } from "lucide-react"
|
||||||
|
|
||||||
|
export function WorkspaceSessionMissing({ saasUrl }: { saasUrl?: string }) {
|
||||||
|
const fallbackUrl = saasUrl ?? process.env.NEXT_PUBLIC_SAAS_URL ?? "/"
|
||||||
|
return (
|
||||||
|
<div className="min-h-[60vh] flex items-center justify-center p-6">
|
||||||
|
<div className="max-w-md w-full text-center space-y-3 bg-white rounded-xl shadow-sm border p-8">
|
||||||
|
<AlertCircle className="size-10 mx-auto text-amber-500" />
|
||||||
|
<h1 className="text-lg font-semibold">Workspace session not found</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Please open your garage from the SaaS dashboard to get a fresh handoff link.
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href={fallbackUrl}
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm border bg-white hover:bg-muted/40 transition"
|
||||||
|
>
|
||||||
|
<ExternalLink className="size-3.5" />
|
||||||
|
Back to SaaS dashboard
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -60,70 +60,87 @@ import { InventoryCategoriesClient } from "./clients/inventory-categories"
|
|||||||
import { DocumentPrintClient } from "./clients/document-print"
|
import { DocumentPrintClient } from "./clients/document-print"
|
||||||
import { DocumentShareClient } from "./clients/document-share"
|
import { DocumentShareClient } from "./clients/document-share"
|
||||||
|
|
||||||
export function createApi(options?: ApiClientOptions) {
|
export type CreateApiOptions = ApiClientOptions & { baseUrl?: string }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default base URL used when no explicit override is passed. The dashboard
|
||||||
|
* runs every browser call through the Next.js server proxy at /api/proxy so
|
||||||
|
* the build-time NEXT_PUBLIC_API_URL never reaches the client.
|
||||||
|
*
|
||||||
|
* Server-side callers (server actions, RSC) can opt out by passing
|
||||||
|
* `{ baseUrl: <absolute repareenew URL> }` from a cookie.
|
||||||
|
*/
|
||||||
|
const DEFAULT_BASE_URL = "/api/proxy"
|
||||||
|
|
||||||
|
export function createApi(options?: CreateApiOptions) {
|
||||||
|
const baseUrl = options?.baseUrl ?? DEFAULT_BASE_URL
|
||||||
|
const restOptions: ApiClientOptions | undefined = options?.headers
|
||||||
|
? { headers: options.headers }
|
||||||
|
: undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
auth: new AuthClient(undefined, options),
|
auth: new AuthClient(baseUrl, restOptions),
|
||||||
customers: new CustomersClient(undefined, options),
|
customers: new CustomersClient(baseUrl, restOptions),
|
||||||
referralSources: new ReferralSourcesClient(undefined, options),
|
referralSources: new ReferralSourcesClient(baseUrl, restOptions),
|
||||||
vehicles: new VehiclesClient(undefined, options),
|
vehicles: new VehiclesClient(baseUrl, restOptions),
|
||||||
vehicleAttributes: new VehicleAttributesClient(undefined, options),
|
vehicleAttributes: new VehicleAttributesClient(baseUrl, restOptions),
|
||||||
vehicleDocuments: new VehicleDocumentsClient(undefined, options),
|
vehicleDocuments: new VehicleDocumentsClient(baseUrl, restOptions),
|
||||||
departments: new DepartmentsClient(undefined, options),
|
departments: new DepartmentsClient(baseUrl, restOptions),
|
||||||
employees: new EmployeesClient(undefined, options),
|
employees: new EmployeesClient(baseUrl, restOptions),
|
||||||
geo: new GeoClient(undefined, options),
|
geo: new GeoClient(baseUrl, restOptions),
|
||||||
paymentTerms: new PaymentTermsClient(undefined, options),
|
paymentTerms: new PaymentTermsClient(baseUrl, restOptions),
|
||||||
shopTypes: new ShopTypesClient(undefined, options),
|
shopTypes: new ShopTypesClient(baseUrl, restOptions),
|
||||||
inventory: new InventoryClient(undefined, options),
|
inventory: new InventoryClient(baseUrl, restOptions),
|
||||||
vendors: new VendorsClient(undefined, options),
|
vendors: new VendorsClient(baseUrl, restOptions),
|
||||||
inspections: new InspectionsClient(undefined, options),
|
inspections: new InspectionsClient(baseUrl, restOptions),
|
||||||
inspectionTemplates: new InspectionTemplatesClient(undefined, options),
|
inspectionTemplates: new InspectionTemplatesClient(baseUrl, restOptions),
|
||||||
labels: new LabelsClient(undefined, options),
|
labels: new LabelsClient(baseUrl, restOptions),
|
||||||
insuranceTypes: new InsuranceTypesClient(undefined, options),
|
insuranceTypes: new InsuranceTypesClient(baseUrl, restOptions),
|
||||||
estimates: new EstimatesClient(undefined, options),
|
estimates: new EstimatesClient(baseUrl, restOptions),
|
||||||
quickRemarks: new QuickRemarksClient(undefined, options),
|
quickRemarks: new QuickRemarksClient(baseUrl, restOptions),
|
||||||
quickNotes: new QuickNotesClient(undefined, options),
|
quickNotes: new QuickNotesClient(baseUrl, restOptions),
|
||||||
shopRecommendations: new ShopRecommendationsClient(undefined, options),
|
shopRecommendations: new ShopRecommendationsClient(baseUrl, restOptions),
|
||||||
jobCards: new JobCardsClient(undefined, options),
|
jobCards: new JobCardsClient(baseUrl, restOptions),
|
||||||
paymentModes: new PaymentModesClient(undefined, options),
|
paymentModes: new PaymentModesClient(baseUrl, restOptions),
|
||||||
paymentReceived: new PaymentReceivedClient(undefined, options),
|
paymentReceived: new PaymentReceivedClient(baseUrl, restOptions),
|
||||||
parts: new PartsClient(undefined, options),
|
parts: new PartsClient(baseUrl, restOptions),
|
||||||
purchaseOrders: new PurchaseOrdersClient(undefined, options),
|
purchaseOrders: new PurchaseOrdersClient(baseUrl, restOptions),
|
||||||
services: new ServicesClient(undefined, options),
|
services: new ServicesClient(baseUrl, restOptions),
|
||||||
serviceGroups: new ServiceGroupsClient(undefined, options),
|
serviceGroups: new ServiceGroupsClient(baseUrl, restOptions),
|
||||||
expenses: new ExpensesClient(undefined, options),
|
expenses: new ExpensesClient(baseUrl, restOptions),
|
||||||
tasks: new TasksClient(undefined, options),
|
tasks: new TasksClient(baseUrl, restOptions),
|
||||||
taskTypes: new TaskTypesClient(undefined, options),
|
taskTypes: new TaskTypesClient(baseUrl, restOptions),
|
||||||
taskSections: new TaskSectionsClient(undefined, options),
|
taskSections: new TaskSectionsClient(baseUrl, restOptions),
|
||||||
appointments: new AppointmentsClient(undefined, options),
|
appointments: new AppointmentsClient(baseUrl, restOptions),
|
||||||
shopTimings: new ShopTimingsClient(undefined, options),
|
shopTimings: new ShopTimingsClient(baseUrl, restOptions),
|
||||||
shopCalendars: new ShopCalendarsClient(undefined, options),
|
shopCalendars: new ShopCalendarsClient(baseUrl, restOptions),
|
||||||
holidayYears: new HolidayYearsClient(undefined, options),
|
holidayYears: new HolidayYearsClient(baseUrl, restOptions),
|
||||||
taxes: new TaxesClient(undefined, options),
|
taxes: new TaxesClient(baseUrl, restOptions),
|
||||||
invoices: new InvoicesClient(undefined, options),
|
invoices: new InvoicesClient(baseUrl, restOptions),
|
||||||
home: new HomeClient(undefined, options),
|
home: new HomeClient(baseUrl, restOptions),
|
||||||
bills: new BillsClient(undefined, options),
|
bills: new BillsClient(baseUrl, restOptions),
|
||||||
reasons: new ReasonsClient(undefined, options),
|
reasons: new ReasonsClient(baseUrl, restOptions),
|
||||||
holidays: new HolidaysClient(undefined, options),
|
holidays: new HolidaysClient(baseUrl, restOptions),
|
||||||
makeAndModels: new MakeAndModelsClient(undefined, options),
|
makeAndModels: new MakeAndModelsClient(baseUrl, restOptions),
|
||||||
timeSheets: new TimeSheetsClient(undefined, options),
|
timeSheets: new TimeSheetsClient(baseUrl, restOptions),
|
||||||
invoiceSequences: new InvoiceSequencesClient(undefined, options),
|
invoiceSequences: new InvoiceSequencesClient(baseUrl, restOptions),
|
||||||
creditNotes: new CreditNotesClient(undefined, options),
|
creditNotes: new CreditNotesClient(baseUrl, restOptions),
|
||||||
paymentMades: new PaymentMadesClient(undefined, options),
|
paymentMades: new PaymentMadesClient(baseUrl, restOptions),
|
||||||
vendorCredits: new VendorCreditsClient(undefined, options),
|
vendorCredits: new VendorCreditsClient(baseUrl, restOptions),
|
||||||
inventoryAdjustments: new InventoryAdjustmentsClient(undefined, options),
|
inventoryAdjustments: new InventoryAdjustmentsClient(baseUrl, restOptions),
|
||||||
serviceGroupIncludes: new ServiceGroupIncludesClient(undefined, options),
|
serviceGroupIncludes: new ServiceGroupIncludesClient(baseUrl, restOptions),
|
||||||
serviceGroupPricings: new ServiceGroupPricingsClient(undefined, options),
|
serviceGroupPricings: new ServiceGroupPricingsClient(baseUrl, restOptions),
|
||||||
serviceGroupServices: new ServiceGroupServicesClient(undefined, options),
|
serviceGroupServices: new ServiceGroupServicesClient(baseUrl, restOptions),
|
||||||
serviceGroupParts: new ServiceGroupPartsClient(undefined, options),
|
serviceGroupParts: new ServiceGroupPartsClient(baseUrl, restOptions),
|
||||||
settings: new SettingsClient(undefined, options),
|
settings: new SettingsClient(baseUrl, restOptions),
|
||||||
configurations: new ConfigurationsClient(undefined, options),
|
configurations: new ConfigurationsClient(baseUrl, restOptions),
|
||||||
autoGenerate: new AutoGenerateClient(undefined, options),
|
autoGenerate: new AutoGenerateClient(baseUrl, restOptions),
|
||||||
expenseItems: new ExpenseItemsClient(undefined, options),
|
expenseItems: new ExpenseItemsClient(baseUrl, restOptions),
|
||||||
inventoryCategories: new InventoryCategoriesClient(undefined, options),
|
inventoryCategories: new InventoryCategoriesClient(baseUrl, restOptions),
|
||||||
documentPrint: new DocumentPrintClient(undefined, options),
|
documentPrint: new DocumentPrintClient(baseUrl, restOptions),
|
||||||
documentShare: new DocumentShareClient(undefined, options),
|
documentShare: new DocumentShareClient(baseUrl, restOptions),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Unauthenticated singleton — use for public calls (login, register) */
|
/** Unauthenticated singleton — use for public calls (login, register). */
|
||||||
export const api = createApi()
|
export const api = createApi()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user