Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions apps/dashboard/app/(app)/api/auth/refresh/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ export async function POST(request: Request) {
const currentToken = request.headers.get("x-current-token");
if (!currentToken) {
console.error("Session refresh failed: no current token");
return Response.json({ success: false, error: "Failed to refresh session" }, { status: 401 });
return Response.json(
{ success: false, error: "Failed to refresh session" },
{ status: 401 }
);
}
// Call refreshSession logic here and get new token
const { newToken, expiresAt } = await auth.refreshSession(currentToken);
Expand All @@ -19,14 +22,16 @@ export async function POST(request: Request) {
name: UNKEY_SESSION_COOKIE,
value: newToken,
options: {
...getAuthCookieOptions(),
maxAge: Math.floor((expiresAt.getTime() - Date.now()) / 1000), // seconds

},
});

return Response.json({ success: true });
} catch (error) {
console.error("Session refresh failed:", error);
return Response.json({ success: false, error: "Failed to refresh session" }, { status: 401 });
return Response.json(
{ success: false, error: "Failed to refresh session" },
{ status: 401 }
);
}
}
35 changes: 19 additions & 16 deletions apps/dashboard/app/new/hooks/use-workspace-step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { StackPerspective2 } from "@unkey/icons";
import { Button, FormInput, toast } from "@unkey/ui";
import { useRouter } from "next/navigation";
import type React from "react";
import { useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
Expand All @@ -22,7 +23,7 @@ const workspaceSchema = z.object({
.max(64, "Workspace slug must be 64 characters or less")
.regex(
/^[a-z0-9]+(?:-[a-z0-9]+)*$/,
"Use lowercase letters, numbers, and single hyphens (no leading/trailing hyphens).",
"Use lowercase letters, numbers, and single hyphens (no leading/trailing hyphens)."
),
});

Expand All @@ -47,9 +48,7 @@ export const useWorkspaceStep = (): OnboardingStep => {
return;
}

await setSessionCookie({
token: sessionData.token,
expiresAt: sessionData.expiresAt,

});
},
onError: (error) => {
Expand Down Expand Up @@ -90,6 +89,19 @@ export const useWorkspaceStep = (): OnboardingStep => {
},
});

const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const name = e.target.value;
const currentSlug = form.getValues("slug");
const isSlugDirty = form.formState.dirtyFields.slug;

// Only auto-generate if slug is empty, not dirty, and hasn't been manually edited
if (!currentSlug && !isSlugDirty && !slugManuallyEdited) {
form.setValue("slug", slugify(name), {
shouldValidate: true,
});
}
};

Comment on lines +92 to +104
Copy link
Contributor

@coderabbitai coderabbitai bot Sep 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Auto-slug stops after the first update; keep syncing until manual edit.

The !currentSlug guard prevents further updates once the slug is first set. Also mark the field dirty.

-  const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-    const name = e.target.value;
-    const currentSlug = form.getValues("slug");
-    const isSlugDirty = form.formState.dirtyFields.slug;
-
-    // Only auto-generate if slug is empty, not dirty, and hasn't been manually edited
-    if (!currentSlug && !isSlugDirty && !slugManuallyEdited) {
-      form.setValue("slug", slugify(name), {
-        shouldValidate: true,
-      });
-    }
-  };
+  const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => {
+    const name = e.target.value;
+    if (!slugManuallyEdited) {
+      form.setValue("slug", slugify(name), {
+        shouldValidate: true,
+        shouldDirty: true,
+      });
+    }
+  };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/dashboard/app/new/hooks/use-workspace-step.tsx around lines 103 to 115,
the auto-slug logic stops updating after the first auto-fill because of the
`!currentSlug` guard and doesn't mark the field as dirty; remove the
`!currentSlug` check so the slug continues to sync on every name change until
the user manually edits it, and when calling form.setValue for "slug" include
the option to mark the field dirty (e.g., shouldDirty: true) so react-hook-form
knows it was programmatically modified.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@perkinsjr Thank you for addressing the auto-slug synchronization issue! I'm glad the logic has been updated to continue syncing the slug with the workspace name until the user manually edits it.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

const onSubmit = async (data: WorkspaceFormData) => {
if (workspaceCreated) {
// Workspace already created, just proceed
Expand Down Expand Up @@ -144,20 +156,11 @@ export const useWorkspaceStep = (): OnboardingStep => {
{/* <div className="space-y-4 pt-7"> */}
<div className="space-y-4 p-1">
<FormInput
{...form.register("workspaceName")}
{...form.register("workspaceName", {
onChange: handleNameChange,
})}
placeholder="Enter workspace name"
label="Workspace name"
onBlur={(evt) => {
const currentSlug = form.getValues("slug");
const isSlugDirty = form.formState.dirtyFields.slug;

// Only auto-generate if slug is empty, not dirty, and hasn't been manually edited
if (!currentSlug && !isSlugDirty && !slugManuallyEdited) {
form.setValue("slug", slugify(evt.currentTarget.value), {
shouldValidate: true,
});
}
}}
required
error={form.formState.errors.workspaceName?.message}
disabled={isLoading || workspaceCreated}
Expand Down
16 changes: 9 additions & 7 deletions apps/dashboard/lib/auth/cookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ export interface Cookie {
/**
* Get a cookie value by name
*/
export async function getCookie(name: string, request?: NextRequest): Promise<string | null> {
export async function getCookie(
name: string,
request?: NextRequest
): Promise<string | null> {
const cookieStore = request?.cookies || cookies();
return cookieStore.get(name)?.value ?? null;
Comment on lines +27 to 32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Minor: drop unnecessary async from getCookie.

This function is synchronous; removing async tightens types and avoids implicit Promise wrapping.

-export async function getCookie(
+export function getCookie(
   name: string,
   request?: NextRequest
-): Promise<string | null> {
+): string | null {
   const cookieStore = request?.cookies || cookies()
   return cookieStore.get(name)?.value ?? null
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function getCookie(
name: string,
request?: NextRequest
): Promise<string | null> {
const cookieStore = request?.cookies || cookies();
return cookieStore.get(name)?.value ?? null;
export function getCookie(
name: string,
request?: NextRequest
): string | null {
const cookieStore = request?.cookies || cookies();
return cookieStore.get(name)?.value ?? null;
}
🤖 Prompt for AI Agents
In apps/dashboard/lib/auth/cookies.ts around lines 26 to 31, the getCookie
function is marked async despite being fully synchronous; remove the async
keyword and update the return type from Promise<string | null> to string | null
so the function returns a plain value and avoids unnecessary Promise wrapping,
then run TypeScript checks to ensure callers still handle the non-Promise return
(adjust call sites if they awaited the function).

}
Expand Down Expand Up @@ -64,7 +67,7 @@ export async function deleteCookie(name: string): Promise<void> {
export async function updateCookie(
cookieName: string,
value: string | null | undefined,
reason?: string,
reason?: string
): Promise<void> {
if (value) {
await setCookie({
Expand All @@ -89,7 +92,7 @@ export async function updateCookie(
*/
export async function setCookiesOnResponse(
response: NextResponse,
cookieList: Cookie[],
cookieList: Cookie[]
): Promise<NextResponse> {
for (const cookie of cookieList) {
response.cookies.set(cookie.name, cookie.value, cookie.options);
Expand All @@ -111,14 +114,12 @@ export async function setSessionCookie(params: {
name: UNKEY_SESSION_COOKIE,
value: token,
options: {
...getDefaultCookieOptions(),
maxAge: Math.floor((expiresAt.getTime() - Date.now()) / 1000),
},
});
}

export async function getCookieOptionsAsString(
options: Partial<CookieOptions> = {},
options: Partial<CookieOptions> = {}
): Promise<string> {
// Set defaults if not provided
const defaultOptions: CookieOptions = getDefaultCookieOptions();
Expand All @@ -138,7 +139,8 @@ export async function getCookieOptionsAsString(

if (mergedOptions.sameSite) {
const capitalizedSameSite =
mergedOptions.sameSite.charAt(0).toUpperCase() + mergedOptions.sameSite.slice(1);
mergedOptions.sameSite.charAt(0).toUpperCase() +
mergedOptions.sameSite.slice(1);
cookieString += `; SameSite=${capitalizedSameSite}`;
}

Expand Down