Skip to content

Commit

Permalink
feat: Add default bytes and prefix (unkeyed#2146)
Browse files Browse the repository at this point in the history
* Add default bytes and prefix

* adding bytes and prefix columns in harness

* fmt

* Await

* Resolved changes

* typo

* Capital and Extra bracket

* [autofix.ci] apply automated fixes

* feat: mobile-sidbar-sync-with-desktop (unkeyed#2180)

* feat/mobile-sidbar-sync-with-desktop

* fix(billing): add missing subscription fields and audit logging to upgrade flow (unkeyed#2179)

* fix(billing): add missing subscription fields and audit logging to upgrade flow

* fix context properties

* refactor: query audit logs from planetscale (unkeyed#2181)

* refactor: query audit logs from planetscale

* fix: sort logs

* [autofix.ci] apply automated fixes

* chore: remove csv export

* Update apps/dashboard/app/(app)/audit/[bucket]/page.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fmt: add comma

* ci: remove wrong lint command

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* chore: Unkey PDF Viewer template [SIDE QUEST] (unkeyed#2191)

* chore: Unkey PDF Viewer template

* feat: add template

---------

Co-authored-by: chronark <[email protected]>

* perf: add database indices (unkeyed#2192)

* fix: add header again

* docs: Removing pnpm test:routes (unkeyed#2184)

* fix: revalidate /apis after creating new API (unkeyed#2183)

* fix: revalidate /apis after creating new API key

* fix: show success message after revalidate suceeds

* fix: revalidate cache before routing

* chore(deps-dev): bump @types/react-dom from 18.2.25 to 18.3.0 (unkeyed#2187)

Bumps [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) from 18.2.25 to 18.3.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: "@types/react-dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Refactor/workspace-navigation

* ran fmt

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: Meg Stepp <[email protected]>
Co-authored-by: Andreas Thomas <[email protected]>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Nazar Poshtarenko <[email protected]>
Co-authored-by: Harsh Shrikant Bhat <[email protected]>
Co-authored-by: Gerald Maboshe <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix: update go sdk code examples to the current sdk version (unkeyed#2200)

* fix: update go sdk code examples to the current sdk version

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Andreas Thomas <[email protected]>

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Akshay <[email protected]>
Co-authored-by: Meg Stepp <[email protected]>
Co-authored-by: Andreas Thomas <[email protected]>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Nazar Poshtarenko <[email protected]>
Co-authored-by: Gerald Maboshe <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
  • Loading branch information
9 people authored Oct 5, 2024
1 parent faf6db7 commit ea60984
Show file tree
Hide file tree
Showing 11 changed files with 429 additions and 5 deletions.
4 changes: 4 additions & 0 deletions apps/api/src/pkg/testutil/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ export abstract class Harness {
createdAtM: Date.now(),
updatedAtM: null,
deletedAtM: null,
defaultPrefix: null,
defaultBytes: null,
};
const userKeyAuth: KeyAuth = {
id: newId("test"),
Expand All @@ -297,6 +299,8 @@ export abstract class Harness {
createdAtM: Date.now(),
updatedAtM: null,
deletedAtM: null,
defaultPrefix: null,
defaultBytes: null,
};

const unkeyApi: Api = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,11 @@ const formSchema = z.object({
type Props = {
apiId: string;
keyAuthId: string;
defaultBytes: number | null;
defaultPrefix: string | null;
};

export const CreateKey: React.FC<Props> = ({ apiId, keyAuthId }) => {
export const CreateKey: React.FC<Props> = ({ apiId, keyAuthId, defaultBytes, defaultPrefix }) => {
const router = useRouter();

const form = useForm<z.infer<typeof formSchema>>({
Expand All @@ -158,7 +160,8 @@ export const CreateKey: React.FC<Props> = ({ apiId, keyAuthId }) => {
shouldFocusError: true,
delayError: 100,
defaultValues: {
bytes: 16,
prefix: defaultPrefix || undefined,
bytes: defaultBytes || 16,
expireEnabled: false,
limitEnabled: false,
metaEnabled: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,12 @@ export default async function CreateKeypage(props: {
return notFound();
}

return <CreateKey keyAuthId={keyAuth.id} apiId={props.params.apiId} />;
return (
<CreateKey
keyAuthId={keyAuth.id}
apiId={props.params.apiId}
defaultBytes={keyAuth.defaultBytes}
defaultPrefix={keyAuth.defaultPrefix}
/>
);
}
109 changes: 109 additions & 0 deletions apps/dashboard/app/(app)/apis/[apiId]/settings/default-bytes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"use client";
import { Loading } from "@/components/dashboard/loading";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { FormField } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { toast } from "@/components/ui/toaster";
import { trpc } from "@/lib/trpc/client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { z } from "zod";
const formSchema = z.object({
keyAuthId: z.string(),
workspaceId: z.string(),
defaultBytes: z
.number()
.min(8, "Byte size needs to be at least 8")
.max(255, "Byte size cannot exceed 255")
.optional(),
});

type Props = {
keyAuth: {
id: string;
workspaceId: string;
defaultBytes: number | undefined | null;
};
};

export const DefaultBytes: React.FC<Props> = ({ keyAuth }) => {
const router = useRouter();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
defaultBytes: keyAuth.defaultBytes ?? undefined,
keyAuthId: keyAuth.id,
workspaceId: keyAuth.workspaceId,
},
});

const setDefaultBytes = trpc.api.setDefaultBytes.useMutation({
onSuccess() {
toast.success("Default Byte length for this API is updated!");
router.refresh();
},
onError(err) {
console.error(err);
toast.error(err.message);
},
});

async function onSubmit(values: z.infer<typeof formSchema>) {
if (values.defaultBytes === keyAuth.defaultBytes || !values.defaultBytes) {
return toast.error(
"Please provide a different byte-size than already existing one as default",
);
}
await setDefaultBytes.mutateAsync(values);
}

return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<Card>
<CardHeader>
<CardTitle>Default Bytes</CardTitle>
<CardDescription>Set default Bytes for the keys under this API.</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col space-y-2">
<input type="hidden" name="workspaceId" value={keyAuth.workspaceId} />
<input type="hidden" name="keyAuthId" value={keyAuth.id} />
<label className="hidden sr-only">Default Bytes</label>
<FormField
control={form.control}
name="defaultBytes"
render={({ field }) => (
<Input
className="max-w-sm"
{...field}
autoComplete="off"
onChange={(e) => field.onChange(Number(e.target.value))}
/>
)}
/>
</div>
</CardContent>
<CardFooter className="justify-end">
<Button
variant={
form.formState.isValid && !form.formState.isSubmitting ? "primary" : "disabled"
}
disabled={!form.formState.isValid || form.formState.isSubmitting}
type="submit"
>
{form.formState.isSubmitting ? <Loading /> : "Save"}
</Button>
</CardFooter>
</Card>
</form>
);
};
98 changes: 98 additions & 0 deletions apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"use client";
import { Loading } from "@/components/dashboard/loading";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { FormField } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { toast } from "@/components/ui/toaster";
import { trpc } from "@/lib/trpc/client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { z } from "zod";
const formSchema = z.object({
keyAuthId: z.string(),
workspaceId: z.string(),
defaultPrefix: z.string(),
});

type Props = {
keyAuth: {
id: string;
workspaceId: string;
defaultPrefix: string | undefined | null;
};
};

export const DefaultPrefix: React.FC<Props> = ({ keyAuth }) => {
const router = useRouter();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
defaultPrefix: keyAuth.defaultPrefix ?? undefined,
keyAuthId: keyAuth.id,
workspaceId: keyAuth.workspaceId,
},
});

const setDefaultPrefix = trpc.api.setDefaultPrefix.useMutation({
onSuccess() {
toast.success("Default prefix for this API is updated!");
router.refresh();
},
onError(err) {
console.error(err);
toast.error(err.message);
},
});
async function onSubmit(values: z.infer<typeof formSchema>) {
if (values.defaultPrefix.length > 8) {
return toast.error("Default prefix is too long, maximum length is 8 characters.");
}
if (values.defaultPrefix === keyAuth.defaultPrefix) {
return toast.error("Please provide a different prefix than already existing one as default");
}
await setDefaultPrefix.mutateAsync(values);
}

return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<Card>
<CardHeader>
<CardTitle>Default Prefix</CardTitle>
<CardDescription>Set default prefix for the keys under this API.</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col space-y-2">
<input type="hidden" name="workspaceId" value={keyAuth.workspaceId} />
<input type="hidden" name="keyAuthId" value={keyAuth.id} />
<label className="hidden sr-only">Default Prefix</label>
<FormField
control={form.control}
name="defaultPrefix"
render={({ field }) => <Input className="max-w-sm" {...field} autoComplete="off" />}
/>
</div>
</CardContent>
<CardFooter className="justify-end">
<Button
variant={
form.formState.isValid && !form.formState.isSubmitting ? "primary" : "disabled"
}
disabled={!form.formState.isValid || form.formState.isSubmitting}
type="submit"
>
{form.formState.isSubmitting ? <Loading /> : "Save"}
</Button>
</CardFooter>
</Card>
</form>
);
};
15 changes: 15 additions & 0 deletions apps/dashboard/app/(app)/apis/[apiId]/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Code } from "@/components/ui/code";
import { getTenantId } from "@/lib/auth";
import { and, db, eq, isNull, schema, sql } from "@/lib/db";
import { notFound, redirect } from "next/navigation";
import { DefaultBytes } from "./default-bytes";
import { DefaultPrefix } from "./default-prefix";
import { DeleteApi } from "./delete-api";
import { DeleteProtection } from "./delete-protection";
import { UpdateApiName } from "./update-api-name";
Expand Down Expand Up @@ -41,10 +43,23 @@ export default async function SettingsPage(props: Props) {
.from(schema.keys)
.where(and(eq(schema.keys.keyAuthId, api.keyAuthId!), isNull(schema.keys.deletedAt)))
.then((rows) => Number.parseInt(rows.at(0)?.count ?? "0"));
const keyAuth = await db.query.keyAuth.findFirst({
where: (table, { eq, and, isNull }) =>
and(eq(table.id, api.keyAuthId!), isNull(table.deletedAt)),
with: {
workspace: true,
api: true,
},
});
if (!keyAuth || keyAuth.workspace.tenantId !== tenantId) {
return notFound();
}

return (
<div className="flex flex-col gap-8 mb-20 ">
<UpdateApiName api={api} />
<DefaultBytes keyAuth={keyAuth} />
<DefaultPrefix keyAuth={keyAuth} />
<UpdateIpWhitelist api={api} workspace={workspace} />
<Card>
<CardHeader>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const UpdateApiName: React.FC<Props> = ({ api }) => {
if (values.name === api.name || !values.name) {
return toast.error("Please provide a valid name before saving.");
}
updateName.mutateAsync(values);
await updateName.mutateAsync(values);
}

return (
Expand Down
93 changes: 93 additions & 0 deletions apps/dashboard/lib/trpc/routers/api/setDefaultBytes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";

import { insertAuditLogs } from "@/lib/audit";
import { db, eq, schema } from "@/lib/db";
import { ingestAuditLogsTinybird } from "@/lib/tinybird";
import { rateLimitedProcedure, ratelimit } from "@/lib/trpc/ratelimitProcedure";

export const setDefaultApiBytes = rateLimitedProcedure(ratelimit.update)
.input(
z.object({
defaultBytes: z
.number()
.min(8, "Byte size needs to be at least 8")
.max(255, "Byte size cannot exceed 255")
.optional(),
keyAuthId: z.string(),
workspaceId: z.string(),
}),
)
.mutation(async ({ ctx, input }) => {
const keyAuth = await db.query.keyAuth
.findFirst({
where: (table, { eq }) => eq(table.id, input.keyAuthId),
})
.catch((_err) => {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message:
"We were unable to find the KeyAuth. Please contact support using [email protected].",
});
});
if (!keyAuth || keyAuth.workspaceId !== input.workspaceId) {
throw new TRPCError({
code: "NOT_FOUND",
message:
"We are unable to find the correct keyAuth. Please contact support using [email protected]",
});
}
await db.transaction(async (tx) => {
await tx
.update(schema.keyAuth)
.set({
defaultBytes: input.defaultBytes,
})
.where(eq(schema.keyAuth.id, input.keyAuthId))
.catch((_err) => {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message:
"We were unable to update the API default bytes. Please contact support using [email protected].",
});
});
await insertAuditLogs(tx, {
workspaceId: keyAuth.workspaceId,
actor: {
type: "user",
id: ctx.user.id,
},
event: "api.update",
description: `Changed ${keyAuth.workspaceId} default byte size for keys from ${keyAuth.defaultBytes} to ${input.defaultBytes}`,
resources: [
{
type: "keyAuth",
id: keyAuth.id,
},
],
context: {
location: ctx.audit.location,
userAgent: ctx.audit.userAgent,
},
});
});
await ingestAuditLogsTinybird({
workspaceId: keyAuth.workspaceId,
actor: {
type: "user",
id: ctx.user.id,
},
event: "api.update",
description: `Changed ${keyAuth.id} default byte size for keys from ${keyAuth.defaultBytes} to ${input.defaultBytes}`,
resources: [
{
type: "keyAuth",
id: keyAuth.id,
},
],
context: {
location: ctx.audit.location,
userAgent: ctx.audit.userAgent,
},
});
});
Loading

0 comments on commit ea60984

Please sign in to comment.