Vectrum
Sign In
102 lines · 3.9 KB · TypeScript
import type { Metadata } from "next";
import { Label as MuLabel } from "@unlocalhosted/metalui";
import { deleteTokenAction } from "@/app/actions/settings";
import { ConfirmButton } from "@/components/confirm-button";
import { Expiry } from "@/components/expiry";
import { ShieldLockIcon } from "@/components/icons";
import { SettingsSection } from "@/components/metal";
import { ListRow, PlateSection, RowGlyph } from "@/components/settings-plate";
import { Blankslate, Label, PageTitle, RefChip } from "@/components/ui";
import { requireViewer } from "@/lib/auth";
import { timeAgo } from "@/lib/format";
import { canWrite, listReposForUser, repoAccess } from "@/lib/repos";
import { serverOrigin } from "@/lib/request-origin";
import { listTokens, TOKEN_PREFIX } from "@/lib/tokens";
import { NewTokenForm } from "./form";

export const metadata: Metadata = { title: "API tokens" };

export default async function TokensPage() {
  const viewer = await requireViewer();
  const tokens = await listTokens(viewer.id);
  const repos = await Promise.all(
    (await listReposForUser(viewer.id)).map(async (r) => ({
      id: r.id,
      fullName: `${r.owner}/${r.name}`,
      writable: canWrite(await repoAccess(r, viewer.id)),
    })),
  );
  const origin = await serverOrigin();

  return (
    <div>
      <PageTitle
        title="API tokens"
        description="Tokens work like passwords for git over HTTPS. Use one as the password when git asks for credentials."
      />
      <div className="flex flex-col gap-7">
        {tokens.length === 0 ? (
          <PlateSection title="Your tokens">
            <Blankslate icon={<ShieldLockIcon />} title="No tokens yet">
              Generate a token below to clone private repositories or push over HTTPS.
            </Blankslate>
          </PlateSection>
        ) : (
          <SettingsSection title={`Your tokens · ${tokens.length}`}>
            {tokens.map((t) => (
              <ListRow
                key={t.id}
                lead={
                  <RowGlyph>
                    <ShieldLockIcon />
                  </RowGlyph>
                }
                title={
                  <>
                    <span className="truncate">{t.name}</span>
                    <Label tone={t.permission === "write" ? "accent" : "default"}>
                      {t.permission === "write" ? "Read & write" : "Read-only"}
                    </Label>
                    {t.expired && <Label tone="danger">Expired</Label>}
                    <RefChip>
                      {TOKEN_PREFIX}…{t.hint}
                    </RefChip>
                  </>
                }
                meta={
                  <>
                    <span className="truncate type-body text-ink2">
                      {t.allRepos ? "All repositories" : t.repos.map((r) => r.fullName).join(", ") || "No repositories (deleted)"}
                    </span>
                    <MuLabel variant="engraved">
                      <Expiry expiresAt={t.expiresAt} expired={t.expired} /> ·{" "}
                      {t.lastUsedAt ? (
                        <>
                          Last used <b>{timeAgo(t.lastUsedAt)}</b>
                        </>
                      ) : (
                        "Never used"
                      )}
                    </MuLabel>
                  </>
                }
              >
                <ConfirmButton
                  action={deleteTokenAction.bind(null, t.id)}
                  message={`Delete the token “${t.name}”? Anything using it will stop working immediately.`}
                  variant="default"
                >
                  Delete Token
                </ConfirmButton>
              </ListRow>
            ))}
          </SettingsSection>
        )}

        <PlateSection title="Generate new token">
          <NewTokenForm repos={repos} origin={origin} username={viewer.username} />
        </PlateSection>
      </div>
    </div>
  );
}