initial commit
This commit is contained in:
196
convex/comdirect/client.ts
Normal file
196
convex/comdirect/client.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
const BASE_URL = "https://api.comdirect.de";
|
||||
|
||||
export function createRequestId(): string {
|
||||
return Math.floor(Math.random() * 1_000_000_000)
|
||||
.toString()
|
||||
.padStart(9, "0");
|
||||
}
|
||||
|
||||
export function buildRequestInfo(sessionUuid: string, requestId?: string): string {
|
||||
return JSON.stringify({
|
||||
clientRequestId: {
|
||||
sessionId: sessionUuid,
|
||||
requestId: requestId ?? createRequestId(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function oauthToken(
|
||||
body: Record<string, string>,
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
): Promise<{ access_token: string; refresh_token: string; expires_in: number }> {
|
||||
const params = new URLSearchParams({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
...body,
|
||||
});
|
||||
const response = await fetch(`${BASE_URL}/oauth/token`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: params.toString(),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`OAuth fehlgeschlagen: ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function getSessions(
|
||||
accessToken: string,
|
||||
sessionUuid: string,
|
||||
): Promise<Array<{ identifier: string; sessionTanActive: boolean; activated2FA: boolean }>> {
|
||||
const response = await fetch(`${BASE_URL}/api/session/clients/user/v1/sessions`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"x-http-request-info": buildRequestInfo(sessionUuid),
|
||||
},
|
||||
});
|
||||
if (!response.ok) throw new Error(`Sessions-Abruf fehlgeschlagen: ${response.status}`);
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function validateSession(
|
||||
accessToken: string,
|
||||
sessionUuid: string,
|
||||
identifier: string,
|
||||
): Promise<{ challengeId: string; challengeType: string; challenge?: string }> {
|
||||
const response = await fetch(
|
||||
`${BASE_URL}/api/session/clients/user/v1/sessions/${identifier}/validate`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"x-http-request-info": buildRequestInfo(sessionUuid),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
identifier,
|
||||
sessionTanActive: true,
|
||||
activated2FA: true,
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (response.status !== 201) {
|
||||
throw new Error(`Session-Validierung fehlgeschlagen: ${response.status}`);
|
||||
}
|
||||
const authInfo = response.headers.get("x-once-authentication-info");
|
||||
if (!authInfo) throw new Error("Keine TAN-Challenge erhalten");
|
||||
const parsed = JSON.parse(authInfo) as { id: string; typ: string; challenge?: string };
|
||||
return {
|
||||
challengeId: parsed.id,
|
||||
challengeType: parsed.typ,
|
||||
challenge: parsed.challenge,
|
||||
};
|
||||
}
|
||||
|
||||
export async function activateSession(
|
||||
accessToken: string,
|
||||
sessionUuid: string,
|
||||
identifier: string,
|
||||
challengeId: string,
|
||||
tan?: string,
|
||||
): Promise<void> {
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"x-http-request-info": buildRequestInfo(sessionUuid),
|
||||
"x-once-authentication-info": JSON.stringify({ id: challengeId }),
|
||||
};
|
||||
const body: Record<string, unknown> = {
|
||||
identifier,
|
||||
sessionTanActive: true,
|
||||
activated2FA: true,
|
||||
};
|
||||
if (tan) {
|
||||
body.tan = tan;
|
||||
}
|
||||
const response = await fetch(
|
||||
`${BASE_URL}/api/session/clients/user/v1/sessions/${identifier}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Session-Aktivierung fehlgeschlagen: ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSecondaryToken(
|
||||
accessToken: string,
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
): Promise<{ access_token: string; refresh_token: string }> {
|
||||
return await oauthToken(
|
||||
{ grant_type: "cd_secondary", token: accessToken },
|
||||
clientId,
|
||||
clientSecret,
|
||||
);
|
||||
}
|
||||
|
||||
export async function refreshAccessToken(
|
||||
refreshToken: string,
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
): Promise<{ access_token: string; refresh_token: string }> {
|
||||
return await oauthToken(
|
||||
{ grant_type: "refresh_token", refresh_token: refreshToken },
|
||||
clientId,
|
||||
clientSecret,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getAccountBalances(
|
||||
accessToken: string,
|
||||
sessionUuid: string,
|
||||
): Promise<{ values: Array<Record<string, unknown>> }> {
|
||||
const response = await fetch(`${BASE_URL}/api/banking/clients/user/v2/accounts/balances`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"x-http-request-info": buildRequestInfo(sessionUuid),
|
||||
},
|
||||
});
|
||||
if (!response.ok) throw new Error(`Salden-Abruf fehlgeschlagen: ${response.status}`);
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function getTransactions(
|
||||
accessToken: string,
|
||||
sessionUuid: string,
|
||||
accountId: string,
|
||||
params: {
|
||||
transactionState: "BOOKED" | "NOTBOOKED";
|
||||
pagingFirst: number;
|
||||
minBookingDate: string;
|
||||
maxBookingDate: string;
|
||||
},
|
||||
): Promise<{ paging: { index: number; matches: number }; values: Array<Record<string, unknown>> }> {
|
||||
const query = new URLSearchParams({
|
||||
transactionState: params.transactionState,
|
||||
"paging-first": String(params.pagingFirst),
|
||||
"paging-count": "50",
|
||||
"min-bookingDate": params.minBookingDate,
|
||||
"max-bookingDate": params.maxBookingDate,
|
||||
});
|
||||
const response = await fetch(
|
||||
`${BASE_URL}/api/banking/v1/accounts/${accountId}/transactions?${query.toString()}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"x-http-request-info": buildRequestInfo(sessionUuid),
|
||||
},
|
||||
},
|
||||
);
|
||||
if (!response.ok) throw new Error(`Umsatz-Abruf fehlgeschlagen: ${response.status}`);
|
||||
return await response.json();
|
||||
}
|
||||
Reference in New Issue
Block a user