Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

.env

# dependencies
/node_modules
/.pnp
Expand Down
48 changes: 48 additions & 0 deletions app/api/_lib/incode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const getRequiredEnv = (value: string | undefined, name: string) => {
if (!value) {
throw new Error(`${name} is not set`);
}

return value;
};

const getApiBaseUrl = () => getRequiredEnv(process.env.INCODE_API_URL, "INCODE_API_URL");

const getApiHeaders = (token?: string) => ({
"Content-Type": "application/json",
"api-version": "1.0",
"x-api-key": getRequiredEnv(process.env.INCODE_API_KEY, "INCODE_API_KEY"),
...(token ? { "X-Incode-Hardware-Id": token } : {}),
});

const postJson = async <T>(path: string, body: unknown, token?: string): Promise<T> => {
const response = await fetch(`${getApiBaseUrl()}${path}`, {
method: "POST",
headers: getApiHeaders(token),
body: JSON.stringify(body),
});
const data = await response.json();

if (!response.ok) {
throw new Error(data.message || `Request failed with code ${response.status}`);
}

return data as T;
};

const getJson = async <T>(path: string, token: string): Promise<T> => {
const response = await fetch(`${getApiBaseUrl()}${path}`, {
method: "GET",
headers: getApiHeaders(token),
});
const data = await response.json();

if (!response.ok) {
throw new Error(data.message || `Request failed with code ${response.status}`);
}

return data as T;
};

export { getJson, postJson };
export { getRequiredEnv };
78 changes: 78 additions & 0 deletions app/api/auth/results/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { NextResponse } from "next/server";
import { getJson, postJson } from "../../_lib/incode";

type ResultsRequest = {
token?: string;
identityId?: string;
};

type ScoreResponse = {
authentication?: {
identityId?: string;
};
overall?: {
status?: string;
};
};

const finishStatus = (token: string) =>
postJson<{ redirectionUrl?: string; action?: string }>(
"finish-status",
{},
token
);

const closeSession = (token: string) =>
postJson<{ sessionStatus?: string }>(
"session/status/set?action=Closed",
{},
token
);

export async function POST(request: Request) {
const body = (await request.json()) as ResultsRequest;
const token = body.token?.trim();
const identityId = body.identityId?.trim();

if (!token || !identityId) {
return NextResponse.json(
{ message: "token and identityId are required" },
{ status: 400 }
);
}

await finishStatus(token);
await closeSession(token);

const score = await getJson<ScoreResponse>("get/score", token);
const scoreIdentityId = score.authentication?.identityId;
const scoreStatus = score.overall?.status;

if (scoreIdentityId !== identityId) {
return NextResponse.json(
{
message: "candidate does not match score identityId",
isValid: false,
identityId: scoreIdentityId,
},
{ status: 200 }
);
}

if (scoreStatus !== "OK") {
return NextResponse.json(
{
message: "Score for this session is not OK",
isValid: false,
identityId: scoreIdentityId,
},
{ status: 200 }
);
}

return NextResponse.json({
message: "Successful validation",
isValid: true,
identityId: scoreIdentityId,
});
}
37 changes: 37 additions & 0 deletions app/api/auth/start/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NextResponse } from "next/server";
import { postJson } from "../../_lib/incode";
import { getRequiredEnv } from "../../_lib/incode";

type StartAuthRequest = {
identityId?: string;
};

type StartAuthResponse = {
token: string;
identityId?: string;
};

export async function POST(request: Request) {
const body = (await request.json()) as StartAuthRequest;
const identityId = body.identityId?.trim();

if (!identityId) {
return NextResponse.json(
{ message: "identityId is required" },
{ status: 400 }
);
}

const session = await postJson<{ token: string }>("start", {
countryCode: "ALL",
configurationId: getRequiredEnv(
process.env.INCODE_AUTH_CONFIG_ID || process.env.INCODE_CONFIG_ID,
"INCODE_AUTH_CONFIG_ID (or INCODE_CONFIG_ID)"
),
});

return NextResponse.json({
token: session.token,
identityId,
} satisfies StartAuthResponse);
}
47 changes: 47 additions & 0 deletions app/api/incode/[...path]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { NextRequest, NextResponse } from "next/server";

const upstreamBaseUrl = () => process.env.INCODE_API_URL;

const forward = async (
request: NextRequest,
params: { path: string[] }
) => {
const baseUrl = upstreamBaseUrl();

if (!baseUrl) {
return NextResponse.json(
{ message: "INCODE_API_URL is not set" },
{ status: 500 }
);
}

const upstreamUrl = new URL(params.path.join("/"), baseUrl);
upstreamUrl.search = request.nextUrl.search;

const headers = new Headers(request.headers);
headers.delete("host");

const response = await fetch(upstreamUrl, {
method: request.method,
headers,
body: request.method === "GET" || request.method === "HEAD" ? undefined : request.body,
duplex: "half",
} as RequestInit);

const responseHeaders = new Headers(response.headers);
responseHeaders.delete("content-encoding");
responseHeaders.delete("transfer-encoding");
responseHeaders.delete("connection");

return new NextResponse(response.body, {
status: response.status,
headers: responseHeaders,
});
};

export const GET = forward;
export const POST = forward;
export const PUT = forward;
export const PATCH = forward;
export const DELETE = forward;
export const OPTIONS = forward;
5 changes: 5 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ body {
rgb(var(--background-start-rgb));
}

.incode-sdk-surface,
.incode-sdk-surface > div {
background-color: #fff !important;
}

@layer utilities {
.text-balance {
text-wrap: balance;
Expand Down
82 changes: 82 additions & 0 deletions app/incode/demo-shell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
'use client';
import { useState } from "react";
import { Box, Button, Paper, Stack, ToggleButton, ToggleButtonGroup, Typography } from "@mui/material";
import { FaceAuth } from "./face-auth";
import { Incode, type SessionType } from "./incode";

type DemoShellProps = {
baseUrl: string;
};

function DemoShell({ baseUrl }: DemoShellProps) {
const [flow, setFlow] = useState<"onboarding" | "face-auth" | null>(null);
const [session, setSession] = useState<SessionType | null>(null);
const [onboardingStatus, setOnboardingStatus] = useState("Idle");

const startOnboarding = async () => {
setOnboardingStatus("Starting onboarding session...");

const response = await fetch("/api/start", { method: "GET" });
const data = await response.json();

if (!response.ok) {
throw new Error(data.error || data.message || "Unable to start onboarding session");
}

setSession(data);
setOnboardingStatus("Onboarding session started.");
};

return (
<Stack spacing={3}>
<Paper elevation={2} sx={{ p: 2 }}>
<Stack spacing={1}>
<Typography variant="subtitle2" color="text.secondary">
Select flow
</Typography>
<ToggleButtonGroup
value={flow}
exclusive
onChange={(_, nextFlow) => {
setFlow(nextFlow);
}}
color="primary"
size="small"
>
<ToggleButton value="onboarding">Onboarding</ToggleButton>
<ToggleButton value="face-auth">Face authentication</ToggleButton>
</ToggleButtonGroup>
</Stack>
</Paper>

{flow ? (
<Box key={flow}>
{flow === "onboarding" ? (
<Stack spacing={2}>
<Button
variant="contained"
onClick={() =>
startOnboarding().catch((error: Error) => {
setOnboardingStatus(error.message);
})
}
>
{session ? "Start new onboarding session" : "Start onboarding"}
</Button>
<Typography color="text.secondary">{onboardingStatus}</Typography>
{session && <Incode session={session} baseUrl={baseUrl} />}
</Stack>
) : (
<FaceAuth baseUrl={baseUrl} />
)}
</Box>
) : (
<Typography color="text.secondary">
Choose a flow to begin.
</Typography>
)}
</Stack>
);
}

export { DemoShell };
Loading