mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-30 21:01:54 +08:00
<!--
Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md
-->
<img width="1510" alt="image"
src="https://github.com/user-attachments/assets/99619123-6be5-4788-aebe-5fc2a9a36245"
/>
<img width="1510" alt="image"
src="https://github.com/user-attachments/assets/660677bf-f19a-4673-94c8-59ac50eb6ae5"
/>
<img width="1510" alt="image"
src="https://github.com/user-attachments/assets/11ae63c4-5813-4fd8-aa01-fa580d2103be"
/>
<!-- ELLIPSIS_HIDDEN -->
----
> [!IMPORTANT]
> Introduces API key management for users and teams, integrating with
existing project configurations and permissions, and adds comprehensive
tests and examples.
>
> - **API Key Management**:
> - Introduces `ProjectApiKey` model in `schema.prisma` for managing API
keys.
> - Adds `createApiKeyHandlers` in `handlers.tsx` to handle API key CRUD
operations.
> - Implements API key creation, revocation, and validation logic.
> - **Permissions and Configurations**:
> - Adds `allowUserApiKeys` and `allowTeamApiKeys` to `ProjectConfig` in
`schema.prisma`.
> - Updates `TeamSystemPermission` enum to include `MANAGE_API_KEYS`.
> - Ensures API key operations respect project configurations and
user/team permissions.
> - **Testing and Examples**:
> - Adds extensive tests in `api-keys.test.ts` to cover various API key
scenarios.
> - Updates example projects to demonstrate API key usage.
> - **Miscellaneous**:
> - Refactors existing code to integrate API key functionalities.
> - Updates documentation and type definitions to reflect new API key
features.
>
> <sup>This description was created by </sup>[<img alt="Ellipsis"
src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral)<sup>
for 96f60c57f0. It will automatically
update as commits are pushed.</sup>
<!-- ELLIPSIS_HIDDEN -->
---------
Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
127 lines
4.3 KiB
TypeScript
127 lines
4.3 KiB
TypeScript
'use client';
|
|
import { InternalApiKey } from '@stackframe/stack';
|
|
import { ActionCell, ActionDialog, BadgeCell, DataTable, DataTableColumnHeader, DataTableFacetedFilter, DateCell, SearchToolbarItem, TextCell, standardFilterFn } from "@stackframe/stack-ui";
|
|
import { ColumnDef, Row, Table } from "@tanstack/react-table";
|
|
import { useMemo, useState } from "react";
|
|
|
|
type ExtendedInternalApiKey = InternalApiKey & {
|
|
status: 'valid' | 'expired' | 'revoked',
|
|
};
|
|
|
|
function toolbarRender<TData>(table: Table<TData>) {
|
|
return (
|
|
<>
|
|
<SearchToolbarItem table={table} placeholder="Search table" />
|
|
<DataTableFacetedFilter
|
|
column={table.getColumn("status")}
|
|
title="Status"
|
|
options={['valid', 'expired', 'revoked'].map((provider) => ({
|
|
value: provider,
|
|
label: provider,
|
|
}))}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function RevokeDialog(props: {
|
|
apiKey: ExtendedInternalApiKey,
|
|
open: boolean,
|
|
onOpenChange: (open: boolean) => void,
|
|
}) {
|
|
return <ActionDialog
|
|
open={props.open}
|
|
onOpenChange={props.onOpenChange}
|
|
title="Revoke API Key"
|
|
danger
|
|
cancelButton
|
|
okButton={{ label: "Revoke Key", onClick: async () => { await props.apiKey.revoke(); } }}
|
|
confirmText="I understand this will unlink all the apps using this API key"
|
|
>
|
|
{`Are you sure you want to revoke client key *****${props.apiKey.publishableClientKey?.lastFour} and server key *****${props.apiKey.secretServerKey?.lastFour}?`}
|
|
</ActionDialog>;
|
|
}
|
|
|
|
function Actions({ row }: { row: Row<ExtendedInternalApiKey> }) {
|
|
const [isRevokeModalOpen, setIsRevokeModalOpen] = useState(false);
|
|
return (
|
|
<>
|
|
<RevokeDialog apiKey={row.original} open={isRevokeModalOpen} onOpenChange={setIsRevokeModalOpen} />
|
|
<ActionCell
|
|
invisible={row.original.status !== 'valid'}
|
|
items={[{
|
|
item: "Revoke",
|
|
danger: true,
|
|
onClick: () => setIsRevokeModalOpen(true),
|
|
}]}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
const columns: ColumnDef<ExtendedInternalApiKey>[] = [
|
|
{
|
|
accessorKey: "description",
|
|
header: ({ column }) => <DataTableColumnHeader column={column} columnTitle="Description" />,
|
|
cell: ({ row }) => <TextCell size={300}>{row.original.description}</TextCell>,
|
|
},
|
|
{
|
|
accessorKey: "status",
|
|
header: ({ column }) => <DataTableColumnHeader column={column} columnTitle="Status" />,
|
|
cell: ({ row }) => <BadgeCell badges={[row.original.status]} />,
|
|
filterFn: standardFilterFn,
|
|
},
|
|
{
|
|
id: "clientKey",
|
|
accessorFn: (row) => row.publishableClientKey?.lastFour,
|
|
header: ({ column }) => <DataTableColumnHeader column={column} columnTitle="Client Key" />,
|
|
cell: ({ row }) => <TextCell>*******{row.original.publishableClientKey?.lastFour}</TextCell>,
|
|
enableSorting: false,
|
|
},
|
|
{
|
|
id: "serverKey",
|
|
accessorFn: (row) => row.secretServerKey?.lastFour,
|
|
header: ({ column }) => <DataTableColumnHeader column={column} columnTitle="Server Key" />,
|
|
cell: ({ row }) => <TextCell>*******{row.original.secretServerKey?.lastFour}</TextCell>,
|
|
enableSorting: false,
|
|
},
|
|
{
|
|
accessorKey: "expiresAt",
|
|
header: ({ column }) => <DataTableColumnHeader column={column} columnTitle="Expires At" />,
|
|
cell: ({ row }) => <DateCell date={row.original.expiresAt} ignoreAfterYears={50} />
|
|
},
|
|
{
|
|
accessorKey: "createdAt",
|
|
header: ({ column }) => <DataTableColumnHeader column={column} columnTitle="Created At" />,
|
|
cell: ({ row }) => <DateCell date={row.original.createdAt} ignoreAfterYears={50} />
|
|
},
|
|
{
|
|
id: "actions",
|
|
cell: ({ row }) => <Actions row={row} />,
|
|
},
|
|
];
|
|
|
|
export function InternalApiKeyTable(props: { apiKeys: InternalApiKey[] }) {
|
|
const extendedApiKeys = useMemo(() => {
|
|
const keys = props.apiKeys.map((apiKey) => ({
|
|
...apiKey,
|
|
status: ({ 'valid': 'valid', 'manually-revoked': 'revoked', 'expired': 'expired' } as const)[apiKey.whyInvalid() || 'valid'],
|
|
} satisfies ExtendedInternalApiKey));
|
|
// first sort based on status, then by createdAt
|
|
return keys.sort((a, b) => {
|
|
if (a.status === b.status) {
|
|
return a.createdAt < b.createdAt ? 1 : -1;
|
|
}
|
|
return a.status === 'valid' ? -1 : 1;
|
|
});
|
|
}, [props.apiKeys]);
|
|
|
|
return <DataTable
|
|
data={extendedApiKeys}
|
|
columns={columns}
|
|
toolbarRender={toolbarRender}
|
|
defaultColumnFilters={[{ id: 'status', value: ['valid'] }]}
|
|
defaultSorting={[]}
|
|
/>;
|
|
}
|