Skip to content

Commit

Permalink
chore: add auth credentials list, posibility to create auth credential
Browse files Browse the repository at this point in the history
  • Loading branch information
Oleksandr Raspopov committed Dec 18, 2024
1 parent 315eab6 commit a66fece
Show file tree
Hide file tree
Showing 13 changed files with 629 additions and 77 deletions.
67 changes: 67 additions & 0 deletions ui/src/adapters/api/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from "src/adapters/parsers";
import {
Credential,
CredentialStatusType,
DisplayMethod,
Env,
IssuedMessage,
Expand All @@ -41,6 +42,7 @@ type CredentialInput = Pick<Credential, "id" | "revoked" | "schemaHash"> & {
} & Record<string, unknown>;
credentialStatus: {
revocationNonce: number;
type: CredentialStatusType;
} & Record<string, unknown>;
credentialSubject: Record<string, unknown>;
displayMethod?: DisplayMethod | null;
Expand Down Expand Up @@ -68,6 +70,7 @@ export const credentialParser = getStrictParser<CredentialInput, Credential>()(
credentialStatus: z
.object({
revocationNonce: z.number(),
type: z.nativeEnum(CredentialStatusType),
})
.and(z.record(z.unknown())),
credentialSubject: z.record(z.unknown()),
Expand Down Expand Up @@ -107,6 +110,7 @@ export const credentialParser = getStrictParser<CredentialInput, Credential>()(
const [, schemaType] = type;

return {
credentialStatus,
credentialSubject,
displayMethod,
expirationDate,
Expand Down Expand Up @@ -201,6 +205,40 @@ export async function getCredentials({
}
}

export async function getCredentialsByIDs({
env,
identifier,
IDs,
signal,
}: {
IDs: Array<string>;
env: Env;
identifier: string;
signal?: AbortSignal;
}): Promise<Response<List<Credential>>> {
try {
const promises = IDs.map((id) =>
axios({
baseURL: env.api.url,
headers: {
Authorization: buildAuthorizationHeader(env),
},
method: "GET",
signal,
url: `${API_VERSION}/identities/${identifier}/credentials/${id}`,
})
);

const credentials = await Promise.all(promises);

return buildSuccessResponse(
getListParser(credentialParser).parse(credentials.map((credential) => credential.data))
);
} catch (error) {
return buildErrorResponse(error);
}
}

export type CreateCredential = {
credentialSchema: string;
credentialSubject: Json;
Expand Down Expand Up @@ -560,3 +598,32 @@ export async function getIssuedMessages({
return buildErrorResponse(error);
}
}

export type CreateAuthCredential = {
keyID: string;
};

export async function createAuthCredential({
env,
identifier,
payload,
}: {
env: Env;
identifier: string;
payload: CreateAuthCredential;
}): Promise<Response<ID>> {
try {
const response = await axios({
baseURL: env.api.url,
data: payload,
headers: {
Authorization: buildAuthorizationHeader(env),
},
method: "POST",
url: `${API_VERSION}/identities/${identifier}/create-auth-credential`,
});
return buildSuccessResponse(IDParser.parse(response.data));
} catch (error) {
return buildErrorResponse(error);
}
}
1 change: 1 addition & 0 deletions ui/src/adapters/api/identities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export async function createIdentity({

export const identityDetailsParser = getStrictParser<IdentityDetails>()(
z.object({
authCredentialsIDs: z.array(z.string()),
credentialStatusType: z.nativeEnum(CredentialStatusType),
displayName: z.string().nullable(),
identifier: z.string(),
Expand Down
4 changes: 3 additions & 1 deletion ui/src/adapters/api/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ const keyParser = getStrictParser<Key>()(
export async function getKeys({
env,
identifier,
params: { maxResults, page },
params: { maxResults, page, type },
signal,
}: {
env: Env;
identifier: string;
params: {
maxResults?: number;
page?: number;
type?: KeyType;
};
signal?: AbortSignal;
}): Promise<Response<Resource<Key>>> {
Expand All @@ -42,6 +43,7 @@ export async function getKeys({
params: new URLSearchParams({
...(maxResults !== undefined ? { max_results: maxResults.toString() } : {}),
...(page !== undefined ? { page: page.toString() } : {}),
...(type !== undefined ? { type } : {}),
}),
signal,
url: `${API_VERSION}/identities/${identifier}/keys`,
Expand Down
152 changes: 152 additions & 0 deletions ui/src/components/credentials/CreateAuthCredential.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { App, Button, Card, Divider, Flex, Form, Select, Space } from "antd";
import { useCallback, useEffect, useState } from "react";
import { generatePath, useNavigate } from "react-router-dom";
import {
CreateAuthCredential as CreateAuthCredentialType,
createAuthCredential,
} from "src/adapters/api/credentials";
import { getKeys } from "src/adapters/api/keys";
import { ErrorResult } from "src/components/shared/ErrorResult";
import { LoadingResult } from "src/components/shared/LoadingResult";

import { SiderLayoutContent } from "src/components/shared/SiderLayoutContent";
import { useEnvContext } from "src/contexts/Env";
import { useIdentityContext } from "src/contexts/Identity";
import { AppError, Key, KeyType } from "src/domain";
import { ROUTES } from "src/routes";
import {
AsyncTask,
hasAsyncTaskFailed,
isAsyncTaskDataAvailable,
isAsyncTaskStarting,
} from "src/utils/async";
import { isAbortedError, makeRequestAbortable } from "src/utils/browser";
import { VALUE_REQUIRED } from "src/utils/constants";
import { notifyParseErrors } from "src/utils/error";

export function CreateAuthCredential() {
const env = useEnvContext();
const { identifier } = useIdentityContext();
const [form] = Form.useForm<CreateAuthCredentialType>();
const navigate = useNavigate();
const { message } = App.useApp();

const [keys, setKeys] = useState<AsyncTask<Key[], AppError>>({
status: "pending",
});

const handleSubmit = (formValues: CreateAuthCredentialType) => {
return void createAuthCredential({
env,
identifier,
payload: formValues,
}).then((response) => {
if (response.success) {
void message.success("Auth credential added successfully");
navigate(generatePath(ROUTES.identityDetails.path, { identityID: identifier }));
} else {
void message.error(response.error.message);
}
});
};

const fetchKeys = useCallback(
async (signal?: AbortSignal) => {
setKeys((previousKeys) =>
isAsyncTaskDataAvailable(previousKeys)
? { data: previousKeys.data, status: "reloading" }
: { status: "loading" }
);

const response = await getKeys({
env,
identifier,
params: {
type: KeyType.babyjubJub,
},
signal,
});
if (response.success) {
setKeys({
data: response.data.items.successful,
status: "successful",
});

notifyParseErrors(response.data.items.failed);
} else {
if (!isAbortedError(response.error)) {
setKeys({ error: response.error, status: "failed" });
}
}
},
[env, identifier]
);

useEffect(() => {
const { aborter } = makeRequestAbortable(fetchKeys);

return aborter;
}, [fetchKeys]);

return (
<SiderLayoutContent
description="Create a new auth credential"
showBackButton
showDivider
title="Add new auth credential"
>
{(() => {
if (hasAsyncTaskFailed(keys)) {
return (
<Card className="centered">
<ErrorResult error={keys.error.message} />;
</Card>
);
} else if (isAsyncTaskStarting(keys)) {
return (
<Card className="centered">
<LoadingResult />
</Card>
);
} else {
return (
<Card className="centered" title="Auth credential details">
<Space direction="vertical" size="large">
<Form
form={form}
initialValues={{
keyID: "",
}}
layout="vertical"
onFinish={handleSubmit}
>
<Form.Item
label="Key name"
name="keyID"
rules={[{ message: VALUE_REQUIRED, required: true }]}
>
<Select className="full-width" placeholder="Type">
{keys.data.map(({ id, name }) => (
<Select.Option key={id} value={id}>
{name}
</Select.Option>
))}
</Select>
</Form.Item>

<Divider />

<Flex justify="flex-end">
<Button htmlType="submit" type="primary">
Submit
</Button>
</Flex>
</Form>
</Space>
</Card>
);
}
})()}
</SiderLayoutContent>
);
}
Loading

0 comments on commit a66fece

Please sign in to comment.