Integrate address sources with credential envelopes
This commit is contained in:
@@ -216,6 +216,22 @@ export type AddressCardDavAddressBook = {
|
||||
sync_token?: string | null;
|
||||
};
|
||||
|
||||
export type AddressCredentialEnvelope = {
|
||||
id: string;
|
||||
scope_type: string;
|
||||
scope_id?: string | null;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
credential_kind: string;
|
||||
public_data: Record<string, unknown>;
|
||||
secret_keys: string[];
|
||||
secret_configured: boolean;
|
||||
allowed_modules: string[];
|
||||
inherit_to_lower_scopes: boolean;
|
||||
is_active: boolean;
|
||||
revision: string;
|
||||
};
|
||||
|
||||
export type AddressSyncPlanStats = {
|
||||
created: number;
|
||||
updated: number;
|
||||
@@ -305,6 +321,10 @@ type AddressCardDavDiscoveryResponse = {
|
||||
address_books: AddressCardDavAddressBook[];
|
||||
};
|
||||
|
||||
type AddressCredentialEnvelopeListResponse = {
|
||||
credentials: AddressCredentialEnvelope[];
|
||||
};
|
||||
|
||||
type AddressSyncDiagnosticListResponse = {
|
||||
diagnostics: AddressSyncDiagnostic[];
|
||||
};
|
||||
@@ -450,7 +470,14 @@ export async function listAddressSyncSources(
|
||||
|
||||
export function discoverCardDavAddressBooks(
|
||||
settings: ApiSettings,
|
||||
payload: { url: string; auth_type: "none" | "basic" | "bearer"; username?: string | null; password?: string | null; bearer_token?: string | null }
|
||||
payload: {
|
||||
url: string;
|
||||
auth_type: "none" | "basic" | "bearer";
|
||||
username?: string | null;
|
||||
password?: string | null;
|
||||
bearer_token?: string | null;
|
||||
credential_ref?: string | null;
|
||||
}
|
||||
): Promise<AddressCardDavAddressBook[]> {
|
||||
return apiFetch<AddressCardDavDiscoveryResponse>(settings, "/api/v1/addresses/carddav/discover", {
|
||||
method: "POST",
|
||||
@@ -468,6 +495,7 @@ export function createCardDavSyncSource(
|
||||
username?: string | null;
|
||||
password?: string | null;
|
||||
bearer_token?: string | null;
|
||||
credential_ref?: string | null;
|
||||
sync_direction: "read_only" | "import" | "export" | "two_way";
|
||||
read_only?: boolean | null;
|
||||
sync_token?: string | null;
|
||||
@@ -481,6 +509,17 @@ export function createCardDavSyncSource(
|
||||
});
|
||||
}
|
||||
|
||||
export async function listAddressCredentials(
|
||||
settings: ApiSettings,
|
||||
sourceId?: string | null
|
||||
): Promise<AddressCredentialEnvelope[]> {
|
||||
const response = await apiFetch<AddressCredentialEnvelopeListResponse>(
|
||||
settings,
|
||||
`/api/v1/addresses/credentials${queryString({ source_id: sourceId })}`
|
||||
);
|
||||
return response.credentials;
|
||||
}
|
||||
|
||||
export function updateAddressSyncSource(
|
||||
settings: ApiSettings,
|
||||
syncSourceId: string,
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
exportContactVcard,
|
||||
importAddressBookVcards,
|
||||
listAddressBooks,
|
||||
listAddressCredentials,
|
||||
listAddressListEntries,
|
||||
listAddressLists,
|
||||
listAddressSyncConflicts,
|
||||
@@ -59,6 +60,7 @@ import {
|
||||
type AddressCardDavAddressBook,
|
||||
type AddressBook,
|
||||
type AddressBookScope,
|
||||
type AddressCredentialEnvelope,
|
||||
type AddressList,
|
||||
type AddressListEntry,
|
||||
type AddressSyncConflict,
|
||||
@@ -144,6 +146,7 @@ type CardDavFormState = {
|
||||
collection_url: string;
|
||||
display_name: string;
|
||||
auth_type: "none" | "basic" | "bearer";
|
||||
credential_envelope_id: string;
|
||||
username: string;
|
||||
password: string;
|
||||
bearer_token: string;
|
||||
@@ -200,6 +203,7 @@ const EMPTY_CARDDAV_FORM: CardDavFormState = {
|
||||
collection_url: "",
|
||||
display_name: "",
|
||||
auth_type: "basic",
|
||||
credential_envelope_id: "",
|
||||
username: "",
|
||||
password: "",
|
||||
bearer_token: "",
|
||||
@@ -643,6 +647,8 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
||||
const [cardDavOpen, setCardDavOpen] = useState(false);
|
||||
const [cardDavForm, setCardDavForm] = useState<CardDavFormState>(EMPTY_CARDDAV_FORM);
|
||||
const [cardDavDiscovery, setCardDavDiscovery] = useState<AddressCardDavAddressBook[]>([]);
|
||||
const [cardDavCredentials, setCardDavCredentials] = useState<AddressCredentialEnvelope[]>([]);
|
||||
const [cardDavCredentialsError, setCardDavCredentialsError] = useState("");
|
||||
const [syncInspector, setSyncInspector] = useState<SyncInspectorState>(null);
|
||||
const [syncPlan, setSyncPlan] = useState<AddressSyncPlan | null>(null);
|
||||
const [syncDiagnostics, setSyncDiagnostics] = useState<AddressSyncDiagnostic[]>([]);
|
||||
@@ -662,6 +668,37 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
||||
const canReadSync = hasScope(auth, "addresses:sync:read");
|
||||
const canWriteSync = hasScope(auth, "addresses:sync:write");
|
||||
|
||||
useEffect(() => {
|
||||
if (!cardDavOpen || !canWriteSync) {
|
||||
setCardDavCredentials([]);
|
||||
setCardDavCredentialsError("");
|
||||
return;
|
||||
}
|
||||
let active = true;
|
||||
listAddressCredentials(settings)
|
||||
.then((credentials) => {
|
||||
if (active) {
|
||||
setCardDavCredentials(credentials);
|
||||
setCardDavCredentialsError("");
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (active) {
|
||||
setCardDavCredentials([]);
|
||||
setCardDavCredentialsError(errorMessage(err));
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [
|
||||
canWriteSync,
|
||||
cardDavOpen,
|
||||
settings.accessToken,
|
||||
settings.apiBaseUrl,
|
||||
settings.apiKey
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (auth.groups_loaded || !onAuthChange) return;
|
||||
let active = true;
|
||||
@@ -1513,16 +1550,21 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
||||
function openCardDavDialog() {
|
||||
setCardDavForm(EMPTY_CARDDAV_FORM);
|
||||
setCardDavDiscovery([]);
|
||||
setCardDavCredentialsError("");
|
||||
setCardDavOpen(true);
|
||||
}
|
||||
|
||||
function cardDavPayload() {
|
||||
const credentialRef = cardDavForm.auth_type !== "none" && cardDavForm.credential_envelope_id
|
||||
? `credential-envelope:${cardDavForm.credential_envelope_id}`
|
||||
: null;
|
||||
return {
|
||||
url: cardDavForm.collection_url,
|
||||
auth_type: cardDavForm.auth_type,
|
||||
username: cardDavForm.username || null,
|
||||
password: cardDavForm.password || null,
|
||||
bearer_token: cardDavForm.bearer_token || null
|
||||
password: credentialRef ? null : cardDavForm.password || null,
|
||||
bearer_token: credentialRef ? null : cardDavForm.bearer_token || null,
|
||||
credential_ref: credentialRef
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1561,8 +1603,11 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
||||
display_name: cardDavForm.display_name || selectedBook.name,
|
||||
auth_type: cardDavForm.auth_type,
|
||||
username: cardDavForm.username || null,
|
||||
password: cardDavForm.password || null,
|
||||
bearer_token: cardDavForm.bearer_token || null,
|
||||
password: cardDavForm.credential_envelope_id ? null : cardDavForm.password || null,
|
||||
bearer_token: cardDavForm.credential_envelope_id ? null : cardDavForm.bearer_token || null,
|
||||
credential_ref: cardDavForm.auth_type !== "none" && cardDavForm.credential_envelope_id
|
||||
? `credential-envelope:${cardDavForm.credential_envelope_id}`
|
||||
: null,
|
||||
sync_direction: cardDavForm.sync_direction,
|
||||
read_only: cardDavForm.sync_direction === "read_only" || cardDavForm.sync_direction === "import"
|
||||
});
|
||||
@@ -2261,17 +2306,45 @@ export default function AddressBookPage({ settings, auth, onAuthChange }: Props)
|
||||
<input value={cardDavForm.display_name} onChange={(event) => setCardDavForm((current) => ({ ...current, display_name: event.target.value }))} placeholder={selectedBook?.name ?? "CardDAV"} />
|
||||
</FormField>
|
||||
</div>
|
||||
{cardDavForm.auth_type !== "none" &&
|
||||
<FormField label="Reusable credential">
|
||||
<select
|
||||
value={cardDavForm.credential_envelope_id}
|
||||
onChange={(event) => {
|
||||
const credentialId = event.target.value;
|
||||
const credential = cardDavCredentials.find((item) => item.id === credentialId);
|
||||
const username = credential?.public_data?.username;
|
||||
setCardDavForm((current) => ({
|
||||
...current,
|
||||
credential_envelope_id: credentialId,
|
||||
username: credentialId && username ? String(username) : "",
|
||||
password: "",
|
||||
bearer_token: ""
|
||||
}));
|
||||
}}>
|
||||
<option value="">Enter source-specific credentials below</option>
|
||||
{cardDavCredentials.map((credential) => (
|
||||
<option key={credential.id} value={credential.id}>
|
||||
{credential.name}{credential.public_data.username ? ` (${String(credential.public_data.username)})` : ""}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
}
|
||||
{cardDavCredentialsError && <DismissibleAlert tone="danger" resetKey={cardDavCredentialsError}>{cardDavCredentialsError}</DismissibleAlert>}
|
||||
{cardDavForm.auth_type === "basic" &&
|
||||
<div className="form-grid two">
|
||||
<FormField label="Username">
|
||||
<input value={cardDavForm.username} onChange={(event) => setCardDavForm((current) => ({ ...current, username: event.target.value }))} />
|
||||
</FormField>
|
||||
<FormField label="Password">
|
||||
<PasswordField value={cardDavForm.password} onValueChange={(value) => setCardDavForm((current) => ({ ...current, password: value }))} autoComplete="new-password" />
|
||||
<input value={cardDavForm.username} onChange={(event) => setCardDavForm((current) => ({ ...current, username: event.target.value }))} disabled={Boolean(cardDavForm.credential_envelope_id)} />
|
||||
</FormField>
|
||||
{!cardDavForm.credential_envelope_id &&
|
||||
<FormField label="Password">
|
||||
<PasswordField value={cardDavForm.password} onValueChange={(value) => setCardDavForm((current) => ({ ...current, password: value }))} autoComplete="new-password" />
|
||||
</FormField>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{cardDavForm.auth_type === "bearer" &&
|
||||
{cardDavForm.auth_type === "bearer" && !cardDavForm.credential_envelope_id &&
|
||||
<FormField label="Bearer token">
|
||||
<PasswordField value={cardDavForm.bearer_token} onValueChange={(value) => setCardDavForm((current) => ({ ...current, bearer_token: value }))} autoComplete="new-password" />
|
||||
</FormField>
|
||||
|
||||
Reference in New Issue
Block a user