Skip to content

Commit

Permalink
Add sum-sub provider example
Browse files Browse the repository at this point in the history
  • Loading branch information
Strnadj committed Nov 21, 2024
1 parent 09cd7c2 commit 4dca0db
Show file tree
Hide file tree
Showing 47 changed files with 5,901 additions and 381 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,6 @@ test-results

# Synpress
.cache-synpress

# Next cache
.next
2 changes: 0 additions & 2 deletions examples/issuer-sdk-demo/src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ export async function createCredentialViaGrantedIssuer(
credential_level: "human",
credential_type: "human",
credential_status: "pending",
encryption_public_key: ISSUER_PUBLIC_KEY,
human_id: humanId,
issuer: "DEMO ISSUER",
userEncryptionPublicKey,
Expand All @@ -97,7 +96,6 @@ export async function createCredentialViaSuperIssuer(
credential_level: "human",
credential_type: "human",
credential_status: "pending",
encryption_public_key: ISSUER_PUBLIC_KEY,
human_id: humanId,
issuer: "DEMO ISSUER",
});
Expand Down
12 changes: 12 additions & 0 deletions examples/provider-sumsub-demo/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
BASE_URL=

DATABASE_URL=

NEXT_PUBLIC_WALLET_CONNECT_ID=

NEXTAUTH_SECRET=
NEXTAUTH_URL=

SUM_SUB_API_KEY=
SUM_SUB_SECRET_KEY=
SUM_SUB_WEBHOOK_SECRET_KEY=
33 changes: 33 additions & 0 deletions examples/provider-sumsub-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# idOS SumSub provider demo

A demo application showcasing the "@idos-network/issuer-sdk-js" package.


## Getting started.

1. Clone the repository.
2. Install dependencies with `pnpm install`.
3. Create an `.env.local` file and add the following environment variables:

```
BASE_URL=<your-base-url>
DATABASE_URL=<your-postgresql-database-url>
NEXT_PUBLIC_WALLET_CONNECT_ID=<wallet-connect-id>
# SumSub configurations
SUM_SUB_API_KEY=
SUM_SUB_SECRET_KEY=
SUM_SUB_WEBHOOK_SECRET_KEY=
# NextAuth
NEXTAUTH_SECRET=
NEXTAUTH_URL=
# Wallet private key
NEXT_ISSUER_PRIVATE_KEY=
# Issuer (provider keys)
NEXT_ISSUER_SECRET_KEY=
```

4. Run the development server with `pnpm dev`.
125 changes: 125 additions & 0 deletions examples/provider-sumsub-demo/app/all/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"use client";

import { useRouter } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
import { parse } from "uri-template";
import Button from "../components/Button";

const LEVELS = ["basic+liveness", "plus+liveness"];

const authLink = (items: Record<string, string | null>) => {
const authTemplate = parse(
`${window.location.origin}{?level,redirect_uri,client,public_encryption_key,grantee}`,
);

return authTemplate.expand(items);
};

export default function All() {
const router = useRouter();

const [client, setClient] = useState<string>("Superapp!");
const [level, setLevel] = useState<string>("basic+liveness");
const [redirectUri, setRedirectUri] = useState<string>("https://google.com/");
const [publicEncryptionKey, setPublicEncryptionKey] = useState<string | null>(null);
const [grantee, setGrantee] = useState<string | null>(null);

const [link, setLink] = useState<string | null>(null);

useEffect(() => {
const link = authLink({
client,
level,
redirect_uri: redirectUri,
public_encryption_key: publicEncryptionKey,
grantee,
});
setLink(link);
}, [client, level, redirectUri, publicEncryptionKey, grantee]);

const changeLevel = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setLevel(e.target.value);
}, []);

return (
<div className="h-full bg-slate-400 p-5">
<h1 className="mb-3 text-2xl">Link builder</h1>

<h2 className="mb-2">KYC Level</h2>

<div className="mb-2 flex flex-col gap-1">
{LEVELS.map((l) => (
<label key={l} className="block cursor-pointer text-sm">
<input
type="radio"
name="level"
className="mr-1"
value={l}
checked={level === l}
onChange={changeLevel}
/>
{l}
</label>
))}
</div>

<h2 className="mb-2">Client name</h2>

<input
type="text"
className="mb-3 w-2/5 bg-white px-4 py-2"
value={client ?? ""}
onChange={(e) => setClient(e.target.value)}
/>

<h2 className="mb-2">Redirect URI</h2>

<input
type="text"
className="mb-3 w-2/5 bg-white px-4 py-2"
value={redirectUri ?? ""}
onChange={(e) => setRedirectUri(e.target.value)}
/>

<h2 className="mb-2">Grantee DAG - address</h2>

<input
type="text"
className="mb-3 w-2/5 bg-white px-4 py-2"
value={grantee ?? ""}
onChange={(e) => setGrantee(e.target.value)}
/>

<h2 className="mb-2">Grantee DAG - Public encryption key</h2>

<input
type="text"
className="mb-3 w-2/5 bg-white px-4 py-2"
value={publicEncryptionKey ?? ""}
onChange={(e) => setPublicEncryptionKey(e.target.value)}
/>

<h2 className="mb-3">Generated URL</h2>

<textarea
className="mb-2 block cursor-default overflow-auto border-2 border-gray-400 bg-white"
disabled={true}
rows={6}
cols={160}
value={link ?? ""}
/>

<div className="flex flex-row gap-2 align-middle">
<Button onClick={() => (link ? router.push(link) : "")}>Continue</Button>
<div className="mt-5 mb-3 inline-block">or</div>
<Button
onClick={() => {
window.navigator.clipboard.writeText(link ?? "");
}}
>
Copy to clipboard
</Button>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { GET, POST } from "../../../auth";
41 changes: 41 additions & 0 deletions examples/provider-sumsub-demo/app/api/current/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import prisma from "@/app/lib/db";
import { cookies } from "next/headers";
import { auth } from "../../auth";

export async function GET() {
const cookieStore = await cookies();

const client = cookieStore.get("client")?.value;

if (!client) return Response.json({ error: "Client is missing" }, { status: 404 });

const level = cookieStore.get("level")?.value;
const publicEncryptionKey = cookieStore.get("publicEncryptionKey")?.value;
const redirectUri = cookieStore.get("redirectUri")?.value;
const grantee = cookieStore.get("grantee")?.value;

// Step /init
const application = {
client,
level,
publicEncryptionKey,
redirectUri,
grantee,
};

// Step /wallet
const currentUser = await auth();

// Fetch DB data
const user = await prisma.user.findFirst({
// @ts-expect-error Not yet fully typed
where: { address: currentUser?.user?.address },
});

// No DB user means that there is no user logged in
if (!user) {
return Response.json({ application, loggedIn: false });
}

return Response.json({ application, user, loggedIn: !!currentUser });
}
86 changes: 86 additions & 0 deletions examples/provider-sumsub-demo/app/api/idos/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import prisma from "@/app/lib/db";
import { createCredentials, createHumanProfile, insertDAG } from "@/app/lib/idos/backend";
import type { Prisma } from "@prisma/client";
import type { NextRequest } from "next/server";
import { auth } from "../../auth";

export const maxDuration = 60; // 1 minute max duration

export async function POST(request: NextRequest) {
const currentUser = await auth();
if (!currentUser?.user)
return Response.json({ error: "User is not authenticated" }, { status: 401 });

const data: Record<string, string | number> = await request.json();

const update: Prisma.UserUpdateInput = {};

// Pick just known keys to be sure
const keys = [
"idosPubKey",
"idosGrantOwner",
"idosGrantGrantee",
"idosGrantDataId",
"idosGrantLockedUntil",
"idosGrantMessage",
"idosGrantSignature",
];

for (const key of keys) {
if (data[key]) update[key] = data[key];
}

if (Object.keys(update).length > 0) {
await prisma.user.update({
// @ts-expect-error Not yet fully typed
where: { address: currentUser.user.address },
data: update,
});
}

// Reload user
const dbUser = await prisma.user.findFirstOrThrow({
// @ts-expect-error Not yet fully typed
where: { address: currentUser.user.address },
});

// If public key is provided, but no idos profile, then we can create a new one
if (dbUser.idosPubKey && !dbUser.idosHumanId) {
const { idosHumanId, idosWalletId } = await createHumanProfile(dbUser);

await prisma.user.update({
// @ts-expect-error Not yet fully typed
where: { address: currentUser.user.address },
data: { idosHumanId, idosWalletId },
});

// Set to not re-fetch user from the database
dbUser.idosHumanId = idosHumanId;
dbUser.idosWalletId = idosWalletId;
}

// If data.publicKey is provided, then we can update the user's IDOS human profile
if (dbUser.idosPubKey && !dbUser.idosCredentialId) {
// The object is not re-fetched from the database, so we are passing
// required human id to credentials.
const idosCredentialId = await createCredentials(dbUser);

await prisma.user.update({
// @ts-expect-error Not yet fully typed
where: { address: currentUser.user.address },
data: { idosCredentialId },
});
}

if (!dbUser.idosGrantTransactionId && dbUser.idosGrantMessage && dbUser.idosGrantSignature) {
const transactionId = await insertDAG(dbUser);

await prisma.user.update({
// @ts-expect-error Not yet fully typed
where: { address: currentUser.user.address },
data: { idosGrantTransactionId: transactionId },
});
}

return Response.json({ response: "ok" });
}
19 changes: 19 additions & 0 deletions examples/provider-sumsub-demo/app/api/init/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { cookies } from "next/headers";
import type { NextRequest } from "next/server";

export async function POST(request: NextRequest) {
const { client, level, redirectUri, publicEncryptionKey, grantee } = await request.json();
const cookieStore = await cookies();

if (!client || !level || !redirectUri) {
return Response.json({ error: "Client id, level, redirectUri are required." }, { status: 400 });
}

cookieStore.set("client", client, { httpOnly: true });
cookieStore.set("level", level, { httpOnly: true });
cookieStore.set("redirectUri", redirectUri, { httpOnly: true });
cookieStore.set("publicEncryptionKey", publicEncryptionKey, { httpOnly: true });
cookieStore.set("grantee", grantee, { httpOnly: true });

return Response.json({}, { status: 202 });
}
19 changes: 19 additions & 0 deletions examples/provider-sumsub-demo/app/api/token/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createAccessToken } from "@/app/lib/sumSub";
import { cookies } from "next/headers";
import { auth } from "../../auth";

export async function POST() {
const cookieStore = await cookies();

const level = cookieStore.get("level")?.value;
if (!level) return Response.json({ error: "Level is missing" }, { status: 404 });

const currentUser = await auth();
if (!currentUser?.user)
return Response.json({ error: "User is not authenticated" }, { status: 401 });

// @ts-expect-error Not yet fully typed
const sumSubToken = await createAccessToken(currentUser.user.address, level ?? "basic+liveness");

return Response.json({ token: sumSubToken });
}
Loading

0 comments on commit 4dca0db

Please sign in to comment.