359 lines
10 KiB
TypeScript
359 lines
10 KiB
TypeScript
import {
|
|
StarfaceConnection,
|
|
StarfaceAddressBook,
|
|
UnifiedContact,
|
|
emptyContact,
|
|
} from "../models/types";
|
|
|
|
interface LoginResponse {
|
|
loginType: string;
|
|
nonce: string;
|
|
secret: string | null;
|
|
}
|
|
|
|
interface StarfaceContactAttribute {
|
|
displayKey: string;
|
|
name: string;
|
|
value: string;
|
|
i18nDisplayName?: string;
|
|
additionalValues?: Record<string, string>;
|
|
}
|
|
|
|
interface StarfaceContactBlock {
|
|
name: string;
|
|
resourceKey: string;
|
|
attributes: StarfaceContactAttribute[];
|
|
}
|
|
|
|
interface StarfaceContact {
|
|
id: string;
|
|
blocks: StarfaceContactBlock[];
|
|
}
|
|
|
|
interface StarfaceTag {
|
|
id: string;
|
|
name: string;
|
|
}
|
|
|
|
async function sha512(input: string): Promise<string> {
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(input);
|
|
const hashBuffer = await crypto.subtle.digest("SHA-512", data);
|
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
}
|
|
|
|
export class StarfaceApiClient {
|
|
private baseUrl: string;
|
|
private token: string | null = null;
|
|
private connection: StarfaceConnection;
|
|
|
|
constructor(connection: StarfaceConnection) {
|
|
this.connection = connection;
|
|
const protocol = connection.useSsl ? "https" : "http";
|
|
const portPart =
|
|
(connection.useSsl && connection.port === 443) ||
|
|
(!connection.useSsl && connection.port === 80)
|
|
? ""
|
|
: `:${connection.port}`;
|
|
this.baseUrl = `${protocol}://${connection.host}${portPart}/rest`;
|
|
}
|
|
|
|
private headers(withAuth = true): Record<string, string> {
|
|
const h: Record<string, string> = {
|
|
"Content-Type": "application/json",
|
|
"X-Version": "2",
|
|
};
|
|
if (withAuth && this.token) {
|
|
h["authToken"] = this.token;
|
|
}
|
|
return h;
|
|
}
|
|
|
|
async login(): Promise<boolean> {
|
|
try {
|
|
// Step 1: get nonce
|
|
const nonceResp = await fetch(`${this.baseUrl}/login`, {
|
|
method: "GET",
|
|
headers: this.headers(false),
|
|
});
|
|
if (!nonceResp.ok) throw new Error(`Login GET failed: ${nonceResp.status}`);
|
|
const loginData: LoginResponse = await nonceResp.json();
|
|
|
|
// Step 2: calculate secret
|
|
const { loginId, password } = this.connection;
|
|
const passwordHash = await sha512(password);
|
|
const combined = loginId + loginData.nonce + passwordHash;
|
|
const combinedHash = await sha512(combined);
|
|
const secret = `${loginId}:${combinedHash}`;
|
|
|
|
// Step 3: post login
|
|
const loginResp = await fetch(`${this.baseUrl}/login`, {
|
|
method: "POST",
|
|
headers: this.headers(false),
|
|
body: JSON.stringify({
|
|
loginType: loginData.loginType,
|
|
nonce: loginData.nonce,
|
|
secret,
|
|
}),
|
|
});
|
|
if (!loginResp.ok) throw new Error(`Login POST failed: ${loginResp.status}`);
|
|
const tokenData = await loginResp.json();
|
|
this.token = tokenData.token;
|
|
return true;
|
|
} catch (err) {
|
|
console.error("Starface login failed:", err);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async logout(): Promise<void> {
|
|
if (!this.token) return;
|
|
try {
|
|
await fetch(`${this.baseUrl}/login`, {
|
|
method: "DELETE",
|
|
headers: this.headers(),
|
|
});
|
|
} finally {
|
|
this.token = null;
|
|
}
|
|
}
|
|
|
|
async getCurrentUserId(): Promise<string | null> {
|
|
try {
|
|
const resp = await fetch(`${this.baseUrl}/users/me`, {
|
|
headers: this.headers(),
|
|
});
|
|
if (!resp.ok) return null;
|
|
const user = await resp.json();
|
|
return user.id || null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async getTags(): Promise<StarfaceTag[]> {
|
|
try {
|
|
const resp = await fetch(`${this.baseUrl}/contacts/tags`, {
|
|
headers: this.headers(),
|
|
});
|
|
if (!resp.ok) return [];
|
|
return await resp.json();
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async getAvailableAddressBooks(): Promise<StarfaceAddressBook[]> {
|
|
const books: StarfaceAddressBook[] = [];
|
|
|
|
// Central address book
|
|
books.push({
|
|
type: "central",
|
|
name: "Zentrales Adressbuch",
|
|
});
|
|
|
|
// Current user's private address book
|
|
const userId = await this.getCurrentUserId();
|
|
if (userId) {
|
|
books.push({
|
|
type: "user",
|
|
userId,
|
|
name: "Persönliches Adressbuch",
|
|
});
|
|
}
|
|
|
|
// Tags as virtual address books
|
|
const tags = await this.getTags();
|
|
for (const tag of tags) {
|
|
books.push({
|
|
type: "tag",
|
|
tagId: tag.id,
|
|
name: `Tag: ${tag.name}`,
|
|
});
|
|
}
|
|
|
|
return books;
|
|
}
|
|
|
|
async getContacts(
|
|
addressBook: StarfaceAddressBook,
|
|
page = 0,
|
|
pageSize = 200
|
|
): Promise<UnifiedContact[]> {
|
|
const params = new URLSearchParams({
|
|
page: page.toString(),
|
|
pagesize: pageSize.toString(),
|
|
});
|
|
|
|
if (addressBook.type === "user" && addressBook.userId) {
|
|
params.set("userId", addressBook.userId);
|
|
}
|
|
if (addressBook.type === "tag" && addressBook.tagId) {
|
|
params.set("tags", addressBook.tagId);
|
|
}
|
|
|
|
const allContacts: UnifiedContact[] = [];
|
|
let currentPage = page;
|
|
let hasMore = true;
|
|
|
|
while (hasMore) {
|
|
params.set("page", currentPage.toString());
|
|
const resp = await fetch(`${this.baseUrl}/contacts?${params}`, {
|
|
headers: this.headers(),
|
|
});
|
|
if (!resp.ok) break;
|
|
|
|
const contacts: StarfaceContact[] = await resp.json();
|
|
if (contacts.length === 0) {
|
|
hasMore = false;
|
|
break;
|
|
}
|
|
|
|
for (const sc of contacts) {
|
|
allContacts.push(this.mapFromStarface(sc));
|
|
}
|
|
|
|
if (contacts.length < pageSize) {
|
|
hasMore = false;
|
|
} else {
|
|
currentPage++;
|
|
}
|
|
}
|
|
|
|
return allContacts;
|
|
}
|
|
|
|
async createContact(
|
|
contact: UnifiedContact,
|
|
addressBook: StarfaceAddressBook
|
|
): Promise<UnifiedContact | null> {
|
|
const starfaceContact = this.mapToStarface(contact, addressBook);
|
|
const params = new URLSearchParams();
|
|
if (addressBook.type === "user" && addressBook.userId) {
|
|
params.set("userId", addressBook.userId);
|
|
}
|
|
|
|
const url = `${this.baseUrl}/contacts${params.toString() ? "?" + params : ""}`;
|
|
const resp = await fetch(url, {
|
|
method: "POST",
|
|
headers: this.headers(),
|
|
body: JSON.stringify(starfaceContact),
|
|
});
|
|
if (!resp.ok) return null;
|
|
const created: StarfaceContact = await resp.json();
|
|
return this.mapFromStarface(created);
|
|
}
|
|
|
|
async updateContact(
|
|
contactId: string,
|
|
contact: UnifiedContact,
|
|
addressBook: StarfaceAddressBook
|
|
): Promise<boolean> {
|
|
const starfaceContact = this.mapToStarface(contact, addressBook);
|
|
starfaceContact.id = contactId;
|
|
const params = new URLSearchParams();
|
|
if (addressBook.type === "user" && addressBook.userId) {
|
|
params.set("userId", addressBook.userId);
|
|
}
|
|
|
|
const url = `${this.baseUrl}/contacts/${contactId}${params.toString() ? "?" + params : ""}`;
|
|
const resp = await fetch(url, {
|
|
method: "PUT",
|
|
headers: this.headers(),
|
|
body: JSON.stringify(starfaceContact),
|
|
});
|
|
return resp.ok;
|
|
}
|
|
|
|
async deleteContact(contactId: string): Promise<boolean> {
|
|
const resp = await fetch(`${this.baseUrl}/contacts/${contactId}`, {
|
|
method: "DELETE",
|
|
headers: this.headers(),
|
|
});
|
|
return resp.ok;
|
|
}
|
|
|
|
private mapFromStarface(sc: StarfaceContact): UnifiedContact {
|
|
const contact = emptyContact();
|
|
contact.starfaceId = sc.id;
|
|
|
|
const attrs: Record<string, string> = {};
|
|
for (const block of sc.blocks || []) {
|
|
for (const attr of block.attributes || []) {
|
|
if (attr.value) {
|
|
attrs[attr.displayKey] = attr.value;
|
|
}
|
|
}
|
|
}
|
|
|
|
contact.firstName = attrs["NAME"] || "";
|
|
contact.lastName = attrs["SURNAME"] || "";
|
|
contact.company = attrs["COMPANY"] || "";
|
|
contact.jobTitle = attrs["JOB_TITLE"] || "";
|
|
contact.email = attrs["EMAIL"] || "";
|
|
contact.phoneWork = attrs["OFFICE_PHONE_NUMBER"] || "";
|
|
contact.phoneMobile = attrs["MOBILE_PHONE_NUMBER"] || "";
|
|
contact.phoneHome = attrs["PRIVATE_PHONE_NUMBER"] || "";
|
|
contact.fax = attrs["FAX_NUMBER"] || "";
|
|
contact.street = attrs["STREET"] || "";
|
|
contact.city = attrs["CITY"] || "";
|
|
contact.postalCode = attrs["POSTAL_CODE"] || "";
|
|
contact.state = attrs["STATE"] || "";
|
|
contact.country = attrs["COUNTRY"] || "";
|
|
contact.website = attrs["URL"] || "";
|
|
contact.notes = attrs["NOTE"] || "";
|
|
contact.salutation = attrs["SALUTATION"] || "";
|
|
contact.title = attrs["TITLE"] || "";
|
|
contact.birthday = attrs["BIRTHDAY"] || "";
|
|
|
|
return contact;
|
|
}
|
|
|
|
private mapToStarface(
|
|
contact: UnifiedContact,
|
|
addressBook: StarfaceAddressBook
|
|
): StarfaceContact {
|
|
const attributes: StarfaceContactAttribute[] = [];
|
|
|
|
const addAttr = (displayKey: string, name: string, value: string) => {
|
|
if (value) {
|
|
attributes.push({ displayKey, name: name.toLowerCase(), value });
|
|
}
|
|
};
|
|
|
|
addAttr("NAME", "firstName", contact.firstName);
|
|
addAttr("SURNAME", "lastName", contact.lastName);
|
|
addAttr("COMPANY", "company", contact.company);
|
|
addAttr("JOB_TITLE", "jobTitle", contact.jobTitle);
|
|
addAttr("EMAIL", "email", contact.email);
|
|
addAttr("OFFICE_PHONE_NUMBER", "businessPhone", contact.phoneWork);
|
|
addAttr("MOBILE_PHONE_NUMBER", "mobilePhone", contact.phoneMobile);
|
|
addAttr("PRIVATE_PHONE_NUMBER", "homePhone", contact.phoneHome);
|
|
addAttr("FAX_NUMBER", "fax", contact.fax);
|
|
addAttr("STREET", "street", contact.street);
|
|
addAttr("CITY", "city", contact.city);
|
|
addAttr("POSTAL_CODE", "postalCode", contact.postalCode);
|
|
addAttr("STATE", "state", contact.state);
|
|
addAttr("COUNTRY", "country", contact.country);
|
|
addAttr("URL", "website", contact.website);
|
|
addAttr("NOTE", "notes", contact.notes);
|
|
addAttr("SALUTATION", "salutation", contact.salutation);
|
|
addAttr("TITLE", "title", contact.title);
|
|
addAttr("BIRTHDAY", "birthday", contact.birthday);
|
|
|
|
const sc: StarfaceContact = {
|
|
id: contact.starfaceId || "",
|
|
blocks: [
|
|
{
|
|
name: "contact",
|
|
resourceKey: "contact",
|
|
attributes,
|
|
},
|
|
],
|
|
};
|
|
|
|
return sc;
|
|
}
|
|
}
|