feat: add domain-based access control for API keys (#198)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Andreas Enemyr
2025-09-10 13:30:37 +02:00
committed by KM Koushik
parent dbc6996d9a
commit 0817b0c7a5
17 changed files with 250 additions and 27 deletions
@@ -27,11 +27,20 @@ import {
FormLabel,
FormMessage,
} from "@usesend/ui/src/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@unsend/ui/src/select";
const apiKeySchema = z.object({
name: z.string({ required_error: "Name is required" }).min(1, {
message: "Name is required",
}),
domainId: z.string().optional(),
});
export default function AddApiKey() {
@@ -40,6 +49,8 @@ export default function AddApiKey() {
const createApiKeyMutation = api.apiKey.createToken.useMutation();
const [isCopied, setIsCopied] = useState(false);
const [showApiKey, setShowApiKey] = useState(false);
const domainsQuery = api.domain.domains.useQuery();
const utils = api.useUtils();
@@ -47,6 +58,7 @@ export default function AddApiKey() {
resolver: zodResolver(apiKeySchema),
defaultValues: {
name: "",
domainId: "all",
},
});
@@ -55,6 +67,7 @@ export default function AddApiKey() {
{
name: values.name,
permission: "FULL",
domainId: values.domainId === "all" ? undefined : Number(values.domainId),
},
{
onSuccess: (data) => {
@@ -180,6 +193,33 @@ export default function AddApiKey() {
</FormItem>
)}
/>
<FormField
control={apiKeyForm.control}
name="domainId"
render={({ field }) => (
<FormItem>
<FormLabel>Domain access</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select domain access" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="all">All Domains</SelectItem>
{domainsQuery.data?.map((domain: { id: number; name: string }) => (
<SelectItem key={domain.id} value={domain.id.toString()}>
{domain.name}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
Choose which domain this API key can send emails from.
</FormDescription>
</FormItem>
)}
/>
<div className="flex justify-end">
<Button
className=" w-[100px] hover:bg-gray-100 focus:bg-gray-100"
@@ -25,6 +25,7 @@ export default function ApiList() {
<TableHead className="rounded-tl-xl">Name</TableHead>
<TableHead>Token</TableHead>
<TableHead>Permission</TableHead>
<TableHead>Domain Access</TableHead>
<TableHead>Last used</TableHead>
<TableHead>Created at</TableHead>
<TableHead className="rounded-tr-xl">Action</TableHead>
@@ -33,7 +34,7 @@ export default function ApiList() {
<TableBody>
{apiKeysQuery.isLoading ? (
<TableRow className="h-32">
<TableCell colSpan={6} className="text-center py-4">
<TableCell colSpan={7} className="text-center py-4">
<Spinner
className="w-6 h-6 mx-auto"
innerSvgClass="stroke-primary"
@@ -42,7 +43,7 @@ export default function ApiList() {
</TableRow>
) : apiKeysQuery.data?.length === 0 ? (
<TableRow className="h-32">
<TableCell colSpan={6} className="text-center py-4">
<TableCell colSpan={7} className="text-center py-4">
<p>No API keys added</p>
</TableCell>
</TableRow>
@@ -52,9 +53,14 @@ export default function ApiList() {
<TableCell>{apiKey.name}</TableCell>
<TableCell>{apiKey.partialToken}</TableCell>
<TableCell>{apiKey.permission}</TableCell>
<TableCell>
{apiKey.domainId
? apiKey.domain?.name ?? "Domain removed"
: "All domains"}
</TableCell>
<TableCell>
{apiKey.lastUsed
? formatDistanceToNow(apiKey.lastUsed)
? formatDistanceToNow(apiKey.lastUsed, { addSuffix: true })
: "Never"}
</TableCell>
<TableCell>