mirror of
https://github.com/Sea-Haven-Industries/seahaven-door-unlock-api.git
synced 2026-09-30 16:23:12 +00:00
Some checks failed
Deploy / deploy (push) Has been cancelled
* feat(3cx): sync office department BLFs via XAPI Keep unlock and lockdown keys in the templates and write colleague plus shared-parking BLFs per extension so phones skip their own line. * fix(3cx): preserve parking BLF IDs and fail the job on PATCH errors
107 lines
3.6 KiB
TypeScript
107 lines
3.6 KiB
TypeScript
export interface ThreeCxConfig {
|
|
domain: string;
|
|
clientId: string;
|
|
clientSecret: string;
|
|
}
|
|
|
|
export class ThreeCxClient {
|
|
private accessToken: string | undefined;
|
|
private readonly baseUrl: string;
|
|
|
|
constructor(private readonly config: ThreeCxConfig) {
|
|
this.baseUrl = `https://${config.domain.replace(/^https?:\/\//, "")}`;
|
|
}
|
|
|
|
async getAccessToken(): Promise<string> {
|
|
if (this.accessToken) {
|
|
return this.accessToken;
|
|
}
|
|
const res = await fetch(`${this.baseUrl}/connect/token`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
body: new URLSearchParams({
|
|
grant_type: "client_credentials",
|
|
client_id: this.config.clientId,
|
|
client_secret: this.config.clientSecret,
|
|
}),
|
|
});
|
|
if (!res.ok) {
|
|
throw new Error(`3CX token request failed: ${res.status}`);
|
|
}
|
|
const body = (await res.json()) as { access_token?: string };
|
|
if (!body.access_token) {
|
|
throw new Error("3CX token response missing access_token");
|
|
}
|
|
this.accessToken = body.access_token;
|
|
return this.accessToken;
|
|
}
|
|
|
|
async getJson<T>(path: string): Promise<{ status: number; body: T }> {
|
|
const token = await this.getAccessToken();
|
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
|
|
});
|
|
const text = await res.text();
|
|
const body = (text ? JSON.parse(text) : {}) as T;
|
|
return { status: res.status, body };
|
|
}
|
|
|
|
async patchJson<T>(path: string, payload: unknown): Promise<{ status: number; body: T }> {
|
|
const token = await this.getAccessToken();
|
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
method: "PATCH",
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
Accept: "application/json",
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
const text = await res.text();
|
|
const body = (text ? JSON.parse(text) : {}) as T;
|
|
return { status: res.status, body };
|
|
}
|
|
|
|
async listUsers<T>(): Promise<T[]> {
|
|
const users: T[] = [];
|
|
let path =
|
|
"/xapi/v1/Users?$top=100&$select=Id,Number,FirstName,LastName,Blfs,PrimaryGroupId&$expand=Groups($select=Id,Name),Phones";
|
|
for (let i = 0; i < 20; i++) {
|
|
const page = await this.getJson<{ value?: T[]; "@odata.nextLink"?: string }>(path);
|
|
if (page.status !== 200) {
|
|
throw new Error(`3CX Users list failed: ${page.status}`);
|
|
}
|
|
users.push(...(page.body.value ?? []));
|
|
const next = page.body["@odata.nextLink"];
|
|
if (!next) {
|
|
break;
|
|
}
|
|
path = next.replace(this.baseUrl, "");
|
|
}
|
|
return users;
|
|
}
|
|
|
|
async getPeerByNumber(number: string): Promise<{ Id: number; Number: string; Name?: string } | undefined> {
|
|
const encoded = number.replace(/'/g, "''");
|
|
const page = await this.getJson<{ value?: Array<{ Id: number; Number: string; Name?: string }> }>(
|
|
`/xapi/v1/Peers?$filter=Number eq '${encoded}'&$top=1`
|
|
);
|
|
if (page.status !== 200) {
|
|
return undefined;
|
|
}
|
|
return page.body.value?.[0];
|
|
}
|
|
|
|
async patchUserBlfs(userId: number, blfs: string): Promise<{ status: number }> {
|
|
const path = `/xapi/v1/Users(${userId})`;
|
|
const current = await this.getJson<Record<string, unknown>>(path);
|
|
if (current.status !== 200) {
|
|
return { status: current.status };
|
|
}
|
|
const payload = { ...current.body };
|
|
delete payload["@odata.context"];
|
|
payload.Blfs = blfs;
|
|
const patched = await this.patchJson<Record<string, unknown>>(path, payload);
|
|
return { status: patched.status };
|
|
}
|
|
}
|