Skip to content
Merged
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
98 changes: 97 additions & 1 deletion apps/web/app/routes/auth/login.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";

import { authClient } from "~/api/auth-client";
import { Button } from "~/components/ui/button";
import {
Expand All @@ -9,14 +13,104 @@ import {
CardHeader,
CardTitle,
} from "~/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "~/components/ui/form";
import { Input } from "~/components/ui/input";
import { Separator } from "~/components/ui/separator";

const signInGoogle = () => {
void authClient.signIn.social({ provider: "google" });
};

function LoginSeparator() {
return (
<div className="flex items-center gap-3">
<Separator className="flex-1" />
<span className="text-sm text-muted-foreground">or</span>
<Separator className="flex-1" />
</div>
);
}

const signInEmailPasswordSchema = z.object({
email: z.string().email(),
password: z.string(),
});

function LoginEmailPassword() {
const form = useForm<z.infer<typeof signInEmailPasswordSchema>>({
resolver: zodResolver(signInEmailPasswordSchema),
defaultValues: {
email: "",
password: "",
},
});

const onSubmit = (data: z.infer<typeof signInEmailPasswordSchema>) => {
void authClient.signIn.email({
...data,
rememberMe: true,
callbackURL: "/workspaces",
});
};
Comment on lines +55 to +61
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the file structure and the specific file in question
fd auth -t f | grep -E "login|auth" | head -20

Repository: ctrlplanedev/ctrlplane

Length of output: 205


🏁 Script executed:

# Check if the file exists and examine its contents
cat -n apps/web/app/routes/auth/login.tsx | head -100

Repository: ctrlplanedev/ctrlplane

Length of output: 3334


🏁 Script executed:

# Search for authClient definition and usage pattern
rg "authClient" apps/web -A 3 -B 3 --max-count 20

Repository: ctrlplanedev/ctrlplane

Length of output: 3497


🏁 Script executed:

# Examine the auth-client implementation
cat -n apps/web/app/api/auth-client.ts

Repository: ctrlplanedev/ctrlplane

Length of output: 291


🏁 Script executed:

# Check if better-auth or similar is used - look for package.json
fd package.json -t f | head -5 | xargs grep -l "better-auth\|auth" 2>/dev/null

Repository: ctrlplanedev/ctrlplane

Length of output: 109


🏁 Script executed:

# Look for where signIn is actually implemented or what library provides it
rg "signIn\." apps/web/app/api/ -A 5

Repository: ctrlplanedev/ctrlplane

Length of output: 48


🏁 Script executed:

# Check the signup implementation to see how it properly handles authClient
cat -n apps/web/app/routes/auth/signup.tsx | grep -A 10 "onSubmit"

Repository: ctrlplanedev/ctrlplane

Length of output: 788


🏁 Script executed:

# Look for other signIn patterns in the codebase
rg "signIn\." apps/web -B 2 -A 5

Repository: ctrlplanedev/ctrlplane

Length of output: 1000


🏁 Script executed:

# Check package.json to understand better-auth version
cat apps/web/package.json | grep -A 2 -B 2 "better-auth"

Repository: ctrlplanedev/ctrlplane

Length of output: 244


🏁 Script executed:

# Check if there are any other onSubmit handlers or error handling patterns in auth routes
rg "onSubmit.*authClient" apps/web/app/routes/auth -A 8

Repository: ctrlplanedev/ctrlplane

Length of output: 48


🏁 Script executed:

# Check if there's any middleware or global error handler catching these promises
rg "unhandledRejection\|catch.*signIn\|onerror" apps/web -i

Repository: ctrlplanedev/ctrlplane

Length of output: 48


Handle sign‑in errors instead of fire‑and‑forget.

Line 55–61 discards the promise without error handling, creating silent failures and unhandled rejections. The same library (better-auth) is properly awaited in signup.tsx, and coding guidelines prefer async/await with explicit error handling.

🐛 Suggested fix with async/await + user feedback
-  const onSubmit = (data: z.infer<typeof signInEmailPasswordSchema>) => {
-    void authClient.signIn.email({
-      ...data,
-      rememberMe: true,
-      callbackURL: "/workspaces",
-    });
-  };
+  const onSubmit = async (data: z.infer<typeof signInEmailPasswordSchema>) => {
+    const { error } = await authClient.signIn.email({
+      ...data,
+      rememberMe: true,
+      callbackURL: "/workspaces",
+    });
+    if (error) {
+      form.setError("root", {
+        message: error.message ?? "Sign-in failed",
+      });
+      return;
+    }
+  };
@@
-        <div className="space-y-2">
+        <div className="space-y-2">
+          {form.formState.errors.root?.message && (
+            <p className="text-sm text-destructive">
+              {form.formState.errors.root.message}
+            </p>
+          )}
           <Button type="submit" className="w-full">
             Sign in
           </Button>
🤖 Prompt for AI Agents
In `@apps/web/app/routes/auth/login.tsx` around lines 55 - 61, The onSubmit
handler currently fire-and-forgets the authClient.signIn.email call causing
silent failures; convert onSubmit into an async function, await the promise
returned by authClient.signIn.email (the call that spreads data from
signInEmailPasswordSchema and sets rememberMe/callbackURL), and add error
handling (try/catch) to handle rejected promises and surface user feedback or
logging on failure; ensure you handle success vs failure paths consistently with
signup.tsx (e.g., show an error message or set form error on catch, and redirect
or proceed on success).


return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
Comment on lines +66 to +87
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Password field renders as plain text.
Line 86 uses the default input type; this exposes passwords in clear text. Please set type="password" and add email/password autocomplete hints.

🔒 Proposed fix
-                <Input {...field} />
+                <Input type="email" autoComplete="email" {...field} />
@@
-                <Input {...field} />
+                <Input
+                  type="password"
+                  autoComplete="current-password"
+                  {...field}
+                />
📝 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
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" autoComplete="email" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
autoComplete="current-password"
{...field}
/>
</FormControl>
🤖 Prompt for AI Agents
In `@apps/web/app/routes/auth/login.tsx` around lines 66 - 87, The password input
is rendering as plain text; update the FormField with name="password" so the
Input component used in its render has type="password" to mask characters, and
add appropriate autocomplete attributes: set autocomplete="email" on the
FormField/Input for name="email" (or the Input in that render) and
autocomplete="current-password" on the Input for name="password" to provide
browser autofill hints; ensure you modify the Input props referenced in the
render callbacks for those FormField components.

<FormMessage />
</FormItem>
)}
/>
<div className="space-y-2">
<Button type="submit" className="w-full">
Sign in
</Button>
<span className="text-xs text-muted-foreground">
Don't have an account?{" "}
<a
href="/sign-up"
className="cursor-pointer text-primary underline"
>
Sign up
</a>
</span>
</div>
</form>
</Form>
);
}

export default function Login() {
return (
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-background via-background to-muted/20 p-4">
<div className="bg-linear-to-br flex min-h-screen items-center justify-center from-background via-background to-muted/20 p-4">
<div className="mx-auto w-full" style={{ maxWidth: "400px" }}>
{/* Logo */}
<div className="mb-6 flex items-center justify-center">
Expand Down Expand Up @@ -75,6 +169,8 @@ export default function Login() {
</svg>
<span>Continue with Google</span>
</Button>
<LoginSeparator />
<LoginEmailPassword />
</div>
</CardContent>
</Card>
Expand Down
142 changes: 141 additions & 1 deletion apps/web/app/routes/auth/signup.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,143 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
Comment on lines +1 to +3
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the signup and login files
fd -t f "signup\.(ts|tsx)" apps/web
fd -t f "login\.(ts|tsx)" apps/web

Repository: ctrlplanedev/ctrlplane

Length of output: 136


🏁 Script executed:

# Check the signup file content
cat -n apps/web/app/routes/auth/signup.tsx | head -50

# Check the login file content for comparison
cat -n apps/web/app/routes/auth/login.tsx | head -50

Repository: ctrlplanedev/ctrlplane

Length of output: 3244


🏁 Script executed:

# Get full signup.tsx to see if there are other client-side features
wc -l apps/web/app/routes/auth/signup.tsx

# Check what the Form component is and if it requires client
rg "export.*Form" apps/web/app/components/ui/form -A 3

Repository: ctrlplanedev/ctrlplane

Length of output: 194


🏁 Script executed:

# Find the Form component
fd -t f "form\.(ts|tsx)" apps/web/app/components

# Also check the structure
ls -la apps/web/app/components/ui/ | head -20

Repository: ctrlplanedev/ctrlplane

Length of output: 1291


🏁 Script executed:

# Check the form.tsx file
cat -n apps/web/app/components/ui/form.tsx | head -30

Repository: ctrlplanedev/ctrlplane

Length of output: 1115


🏁 Script executed:

# Verify the structure - is signup.tsx in app router context?
cat -n apps/web/app/routes/auth/signup.tsx | tail -50

# Also check if signup is exported as a default component
grep -n "export" apps/web/app/routes/auth/signup.tsx | head -10

Repository: ctrlplanedev/ctrlplane

Length of output: 2135


🏁 Script executed:

# Check if there's a pattern in the codebase - do other route components have "use client"?
fd -t f "\.tsx$" apps/web/app/routes --exec head -1 {} + | grep -B 1 "use client" | head -20

# Check the app structure - is there a layout.tsx?
cat -n apps/web/app/layout.tsx | head -20

Repository: ctrlplanedev/ctrlplane

Length of output: 6966


🏁 Script executed:

# Check app structure
find apps/web/app -name "layout.tsx" -type f

# Also check for global configuration that might make routes client by default
rg "use client" apps/web/app --files-with-matches | head -10

Repository: ctrlplanedev/ctrlplane

Length of output: 456


🏁 Script executed:

# Check if there's a custom useForm or form initialization pattern in ctrlplane
rg "useForm.*schema:" apps/web --context 3 | head -30

# Also check the signup form more carefully
grep -n "const form = " apps/web/app/routes/auth/signup.tsx -A 5

Repository: ctrlplanedev/ctrlplane

Length of output: 253


Add "use client" to enable hook usage.

This file uses the useForm hook from react-hook-form; without the client directive it will fail in a Server Component context. The login.tsx file correctly includes this directive.

Proposed fix
+"use client";
+
 import { zodResolver } from "@hookform/resolvers/zod";
 import { useForm } from "react-hook-form";
 import { z } from "zod";
📝 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
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
🤖 Prompt for AI Agents
In `@apps/web/app/routes/auth/signup.tsx` around lines 1 - 3, Add the "use client"
directive as the very first line of the module (before any imports) so the
component using the useForm hook from react-hook-form runs as a Client
Component; this ensures calls to useForm (and other client hooks) will be
allowed—update the top of the signup module that imports zodResolver, useForm,
and z accordingly.


import { authClient } from "~/api/auth-client";
import { Button } from "~/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "~/components/ui/form";
import { Input } from "~/components/ui/input";

const signUpSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
});

function SignupForm() {
const form = useForm<z.infer<typeof signUpSchema>>({
resolver: zodResolver(signUpSchema),
defaultValues: {
name: "",
email: "",
password: "",
},
});

const onSubmit = async (data: z.infer<typeof signUpSchema>) => {
const { error } = await authClient.signUp.email({ ...data });
if (!error) window.location.href = "/workspaces";
};

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-2">
<Button type="submit" className="w-full">
Sign up
</Button>
<span className="text-xs text-muted-foreground">
Already have an account?{" "}
<a href="/login" className="cursor-pointer text-primary underline">
Sign in
</a>
</span>
</div>
</form>
</Form>
);
}

export default function Signup() {
return <div>Signup</div>;
return (
<div className="bg-linear-to-br flex min-h-screen items-center justify-center from-background via-background to-muted/20 p-4">
<div className="mx-auto w-full" style={{ maxWidth: "400px" }}>
<div className="mb-6 flex items-center justify-center">
<div className="relative flex h-16 w-16 items-center justify-center rounded-full bg-primary/10 p-3">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="h-10 w-10 text-primary transition-all"
>
<path d="M12 2L2 7l10 5 10-5-10-5z" />
<path d="M2 17l10 5 10-5" />
<path d="M2 12l10 5 10-5" />
</svg>
<div className="absolute inset-0 animate-pulse rounded-full border border-primary/20"></div>
</div>
</div>

<Card className="overflow-hidden border-border/30 bg-card/60 shadow-xl backdrop-blur-sm">
<CardHeader className="space-y-1">
<CardTitle className="text-center text-2xl">
Welcome to Ctrlplane
</CardTitle>
<CardDescription className="text-center">
Create an account to continue
</CardDescription>
</CardHeader>

<CardContent className="space-y-6 px-6">
<SignupForm />
</CardContent>
</Card>
</div>
</div>
);
}
3 changes: 3 additions & 0 deletions packages/auth/src/better/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export const auth = betterAuth({
clientSecret: env.AUTH_GOOGLE_CLIENT_SECRET ?? "",
},
},
emailAndPassword: {
enabled: true,
},
trustedOrigins: [env.BASE_URL, "http://localhost:5173"],
advanced: {
database: {
Expand Down
Loading