Files
starface-outlook-sync-addin/src/services/starface-api.ts
T
Stefan Hacker 37ad745546 first commit
2026-04-03 09:38:48 +02:00

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;
}
}