Skip to content

Commit

Permalink
feat: add api image generation
Browse files Browse the repository at this point in the history
  • Loading branch information
Palid committed Nov 22, 2023
1 parent c11ca98 commit 7b51364
Show file tree
Hide file tree
Showing 8 changed files with 323 additions and 88 deletions.
Binary file added public/fonts/inter/Inter-Bold.ttf
Binary file not shown.
Binary file added public/fonts/inter/Inter-Regular.ttf
Binary file not shown.
41 changes: 41 additions & 0 deletions src/app/LoginHeaders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { formatDistance } from "date-fns";
import { jwtDecode } from "jwt-decode";

export type AKProxy = {
user_attributes: {
ldap_uniq: string;
distinguishedName: string;
cardId: string;
membershipExpiration: string;
membershipExpirationDate: string;
membershipExpirationTimestamp: number;
};
is_superuser: boolean;
};

export type JWTContents = {
email: string;
name: string;
given_name: string;
preferred_username: string;
nickname: string;
groups: string[];
ak_proxy: AKProxy;
};

export function getBarData(jwt: string) {
const parsedJwt = jwtDecode<JWTContents>(jwt);

const { groups, preferred_username, ak_proxy } = parsedJwt;
const expirationTimestamp =
ak_proxy.user_attributes.membershipExpirationTimestamp * 1000;

const relative = formatDistance(new Date(expirationTimestamp), Date.now(), {
addSuffix: true,
});
return {
groups,
nickname: preferred_username,
expiration: relative,
};
}
235 changes: 235 additions & 0 deletions src/app/api/images/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import { jwtDecode } from "jwt-decode";
import { ImageResponse } from "next/og";
import { formatDistance } from "date-fns";

import { readFileSync } from "fs";
import { join } from "path";
import { WhoamiProps } from "@/components/whoami";
import getConfig from "next/config";
import { IconUsercircle } from "@/components/icons/IconUserCircle";
import { IconClock } from "@/components/icons/IconClock";
import { JWTContents, getBarData } from "@/app/LoginHeaders";
import { headers } from "next/headers";

function WhoamiImage({
nickname,
expiration,
groups,
theme,
}: WhoamiProps & {
theme: "dark" | "light";
}) {
const styles = {
dark: {
bg: "rgb(31, 41, 55)",
text: "#fff",
textLabel: "rgb(209, 213, 219)",
},
light: {
bg: "rgb(255, 255, 255)",
badgeBorder: "rgb(229, 231, 235)",
text: "#121212",
textLabel: "rgb(31, 41, 55)",
},
};

const currentTheme = styles[theme];

return (
<div
style={{
fontFamily: "Inter",
display: "flex",
flexDirection: "column",
backgroundColor: currentTheme.bg,
padding: "16px",
color: currentTheme.text,
boxShadow: "0 4px 8px 0 rgba(0, 0, 0, 0.2)",
maxWidth: "800px",
margin: "auto",
height: "100%",
width: "100%",
justifyContent: "center",
alignItems: "center",
gap: "16px",
}}
>
<h1
style={{
marginTop: "-4px",
}}
>
Current user data:
</h1>
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
fontSize: "16px",
gap: "8px",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
paddingBottom: "8px",
fontSize: "20px",
}}
>
<IconUsercircle className="h-5 w-5 text-gray-500 dark:text-gray-400" />

<span
style={{
fontFamily: "Inter",
fontWeight: "400",
display: "block",
color: currentTheme.textLabel,
}}
>
Preferred Name:{" "}
</span>
<span
style={{
display: "block",
fontWeight: "bold",
}}
>
{nickname}
</span>
</div>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
fontSize: "20px",
}}
>
<IconClock className="h-5 w-5 text-gray-500 dark:text-gray-400" />
<span
style={{
display: "block",
color: currentTheme.textLabel,
}}
>
Access Token Expiry:{" "}
</span>
<span
style={{
display: "block",
fontWeight: "bold",
}}
>
{expiration}
</span>
</div>
{groups.length > 0 && (
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
fontSize: "16px",
}}
>
<span
style={{
display: "block",
fontWeight: "bold",
fontSize: "16px",
paddingBottom: "8px",
}}
>
Groups:
</span>
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: "8px",
}}
>
{[...groups].map((group, index) => (
<span
key={group}
style={{
display: "block",
padding: "4px 8px",
borderRadius: "8px",
fontWeight: "bold",
fontSize: "14px",
color: currentTheme.text,
border: "1px solid rgb(229, 231, 235)",
}}
>
{group}
</span>
))}
</div>
</div>
)}
</div>
</div>
);
}

const publicPath = join(process.cwd(), "./public", "fonts", "inter");
const interRegular = readFileSync(join(publicPath, "Inter-Regular.ttf"));
const interBold = readFileSync(join(publicPath, "Inter-Bold.ttf"));

export const dynamic = "force-dynamic"; // defaults to force-static
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);

const theme =
searchParams.get("theme")?.toLowerCase() === "dark" ? "dark" : "light";
const headersList = headers();
const jwt = headersList.get("X-Authentik-Jwt");

if (!jwt) {
return new Response(
JSON.stringify({
status: 401,
message: "Unauthorized",
}),
{
status: 401,
headers: {
"Content-Type": "application/json",
},
}
);
}

const { groups, nickname, expiration } = getBarData(jwt);

return new ImageResponse(
(
<WhoamiImage
groups={groups}
nickname={nickname}
expiration={expiration}
theme={theme}
/>
),
{
fonts: [
{
name: "Inter",
weight: 400,
data: interRegular,
},
{
name: "Inter",
weight: 800,
data: interBold,
},
],
width: 400,
height: 250,
}
);
}
49 changes: 4 additions & 45 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,9 @@ import { Whoami } from "@/components/whoami";
import { headers } from "next/headers";
import { jwtDecode } from "jwt-decode";

import { format, formatDistance, subDays } from "date-fns";
import { formatDistance } from "date-fns";
import { NotAuthorized } from "@/components/not-authorized";

type AKProxy = {
user_attributes: {
ldap_uniq: string;
distinguishedName: string;
cardId: string;
membershipExpiration: string;
membershipExpirationDate: string;
membershipExpirationTimestamp: number;
};
is_superuser: boolean;
};
import { JWTContents, getBarData } from "./LoginHeaders";

export default function Home() {
const headersList = headers();
Expand All @@ -28,37 +17,7 @@ export default function Home() {
return <NotAuthorized />;
}

const parsedJwt = jwtDecode<{
email: string;
name: string;
given_name: string;
preferred_username: string;
nickname: string;
groups: string[];
ak_proxy: AKProxy;
}>(jwt);

const {
email,
groups,
name,
given_name,
nickname,
preferred_username,
ak_proxy,
} = parsedJwt;
const expirationTimestamp =
ak_proxy.user_attributes.membershipExpirationTimestamp * 1000;

const relative = formatDistance(new Date(expirationTimestamp), Date.now(), {
addSuffix: true,
});
const { groups, nickname, expiration } = getBarData(jwt);

return (
<Whoami
groups={groups}
nickname={preferred_username}
expiration={relative}
/>
);
return <Whoami groups={groups} nickname={nickname} expiration={expiration} />;
}
19 changes: 19 additions & 0 deletions src/components/icons/IconClock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export function IconClock(props: { className?: string }) {
return (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
);
}
20 changes: 20 additions & 0 deletions src/components/icons/IconUserCircle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export function IconUsercircle(props: { className?: string }) {
return (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<circle cx="12" cy="10" r="3" />
<path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662" />
</svg>
);
}
Loading

0 comments on commit 7b51364

Please sign in to comment.