Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: better t3 auth #348

Merged
merged 31 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2b34be9
fix: improper account dropdown
Vandivier Nov 9, 2024
1500162
feat: migrate community page
Vandivier Nov 10, 2024
b0328af
feat: migrate public profiles
Vandivier Nov 10, 2024
8650609
chore: install react-final-form
Vandivier Nov 10, 2024
0f15bd7
feat: SettingsForm
Vandivier Nov 10, 2024
fa45250
feat: settings trpc api
Vandivier Nov 10, 2024
687d21a
feat: form doesn't use blitz
Vandivier Nov 10, 2024
bca9d8a
feat: broken wip settings page migrate
Vandivier Nov 10, 2024
eae5349
chore: install react-select
Vandivier Nov 10, 2024
5175186
feat: tell user their ID
Vandivier Nov 11, 2024
a56b27d
feat: LadderlySession
Vandivier Nov 11, 2024
a577b04
feat: auth router
Vandivier Nov 11, 2024
474460e
feat: SocialSignIn
Vandivier Nov 11, 2024
73a8f5d
feat: pass session from login page
Vandivier Nov 11, 2024
ee36a30
feat: easier login w goog
Vandivier Nov 11, 2024
0ca762b
feat: tell ppl if they're already logged in
Vandivier Nov 15, 2024
d6da634
feat: forgot pass page visible
Vandivier Nov 15, 2024
9dc1b16
feat: refresh page on logout
Vandivier Nov 15, 2024
80590d6
chore: install argon
Vandivier Nov 15, 2024
214d2c0
forgot password wip
Vandivier Nov 18, 2024
23a2886
feat: reset pass
Vandivier Nov 18, 2024
afb0704
feat: better signin logging
Vandivier Nov 30, 2024
a7903b5
feat: broken signup migration
Vandivier Nov 30, 2024
a4a0fd2
feat: explicit jwt strategy
Vandivier Dec 1, 2024
2c59f0f
fix: TRPCClientError: Invalid response or stream interrupted
Vandivier Dec 1, 2024
2efbedf
feat: user lookup by email
Vandivier Dec 1, 2024
816b6ec
feat: jwt()
Vandivier Dec 1, 2024
1f93c1e
feat: getCurrentUser by email
Vandivier Dec 1, 2024
40cf590
feat: better adapter types
Vandivier Dec 1, 2024
3f3b682
feat: simpler adapter
Vandivier Dec 1, 2024
bdc28ef
feat: refresh session after login
Vandivier Dec 5, 2024
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
834 changes: 763 additions & 71 deletions ladderly-io/package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions ladderly-io/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,15 @@
"@trpc/client": "^11.0.0-rc.446",
"@trpc/react-query": "^11.0.0-rc.446",
"@trpc/server": "^11.0.0-rc.446",
"argon2": "^0.41.1",
"geist": "^1.3.0",
"next": "^14.2.4",
"next-auth": "^4.24.7",
"postmark": "^4.0.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-final-form": "^6.5.9",
"react-select": "^5.8.3",
"server-only": "^0.0.1",
"superjson": "^2.2.1",
"zod": "^3.23.3"
Expand Down
45 changes: 45 additions & 0 deletions ladderly-io/src/app/(auth)/components/ForgotPasswordForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client";

import { Form, FORM_ERROR } from "~/app/core/components/Form";
import LabeledTextField from "~/app/core/components/LabeledTextField";
import { ForgotPassword } from "../schemas";
import { api } from "~/trpc/react";

export function ForgotPasswordForm() {
const forgotPasswordMutation = api.auth.forgotPassword.useMutation();

return (
<>
<h1>Forgot your password?</h1>
<>
{forgotPasswordMutation.isSuccess ? (
<div>
<h2>Request Submitted</h2>
<p>
If your email is in our system, you will receive instructions to
reset your password shortly.
</p>
</div>
) : (
<Form
submitText="Send Reset Password Instructions"
schema={ForgotPassword}
initialValues={{ email: "" }}
onSubmit={async (values: any) => {
try {
await forgotPasswordMutation.mutateAsync(values);
} catch (error: any) {
return {
[FORM_ERROR]:
"Sorry, we had an unexpected error. Please try again.",
};
}
}}
>
<LabeledTextField name="email" label="Email" placeholder="Email" />
</Form>
)}
</>
</>
);
}
85 changes: 85 additions & 0 deletions ladderly-io/src/app/(auth)/components/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"use client";

import { FORM_ERROR } from "final-form";
import { signIn } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Login as LoginSchema } from "~/app/(auth)/schemas";
import { Form } from "~/app/core/components/Form";
import { LabeledTextField } from "~/app/core/components/LabeledTextField";

export const LoginForm = () => {
const router = useRouter();

const handleSubmit = async (values: { email: string; password: string }) => {
const result = await signIn("credentials", {
redirect: false,
email: values.email,
password: values.password,
});

if (result?.error) {
return { [FORM_ERROR]: result.error };
}

if (result?.ok) {
router.push("/?refresh_current_user=true");
router.refresh();
}
};

return (
<div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
<h1 className="mb-4 text-2xl font-bold text-gray-800">Log In</h1>

<button
onClick={() => signIn("google", { callbackUrl: "/" })}
className="mb-6 flex w-full items-center justify-center gap-2 rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-700 shadow-sm hover:bg-gray-50"
>
<img
src="https://www.google.com/favicon.ico"
alt="Google"
className="h-5 w-5"
/>
Sign in with Google
</button>

<div className="relative mb-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-300"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="bg-white px-2 text-gray-500">Or continue with</span>
</div>
</div>

<Form
className="space-y-4"
submitText="Log In with Email"
schema={LoginSchema}
initialValues={{ email: "", password: "" }}
onSubmit={handleSubmit}
>
<LabeledTextField name="email" label="Email" placeholder="Email" />
<LabeledTextField
name="password"
label="Password"
placeholder="Password"
type="password"
/>
<div className="mt-4 text-left">
<Link className="underline" href="/forgot-password">
Forgot your password?
</Link>
</div>
</Form>

<div className="mt-4">
Need to create an account?{" "}
<Link className="underline" href="/signup">
Sign up here!
</Link>
</div>
</div>
);
};
64 changes: 64 additions & 0 deletions ladderly-io/src/app/(auth)/components/SignupForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* TODO: migrate from blitzjs to tRPC */

"use client";
import { useMutation } from "@blitzjs/rpc";
import Link from "next/link";
import signup from "src/app/(auth)/mutations/signup";
import { Signup } from "src/app/(auth)/schemas";
import { Form, FORM_ERROR } from "src/core/components/Form";
import { LabeledTextField } from "src/core/components/LabeledTextField";

type SignupFormProps = {
onSuccess?: () => void;
};

export const SignupForm = (props: SignupFormProps) => {
const [signupMutation] = useMutation(signup);
return (
<div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
<h1 className="mb-4 text-2xl font-bold text-gray-800">
Create an Account
</h1>

<Form
submitText="Create Account"
schema={Signup}
initialValues={{ email: "", password: "" }}
className="space-y-4"
onSubmit={async (values) => {
try {
await signupMutation(values);
props.onSuccess?.();
} catch (error: any) {
if (
error.code === "P2002" &&
error.meta?.target?.includes("email")
) {
// This error comes from Prisma
return { email: "This email is already being used" };
} else {
return { [FORM_ERROR]: error.toString() };
}
}
}}
>
<LabeledTextField name="email" label="Email" placeholder="Email" />
<LabeledTextField
name="password"
label="Password"
placeholder="Password"
type="password"
/>
</Form>

<div className="mt-4">
Already signed up?{" "}
<Link className="underline" href="/login">
Log in here!
</Link>
</div>
</div>
);
};

export default SignupForm;
65 changes: 65 additions & 0 deletions ladderly-io/src/app/(auth)/forgot-password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"use client";

import { LabeledTextField } from "~/app/core/components/LabeledTextField";
import { Form, FORM_ERROR } from "~/app/core/components/Form";
import { ForgotPassword } from "src/app/(auth)/schemas";
import { api } from "~/trpc/react";
import Link from "next/link";

const ForgotPasswordPage = () => {
const forgotPasswordMutation = api.auth.forgotPassword.useMutation();

return (
<div className="relative min-h-screen">
<nav className="border-ladderly-light-purple flex border bg-ladderly-off-white px-4 py-1 text-ladderly-violet-700">
<Link
href="/"
className="ml-auto text-gray-800 hover:text-ladderly-pink"
>
Back to Home
</Link>
</nav>
<div className="flex min-h-[calc(100vh-4rem)] items-center justify-center">
<div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
<h1 className="mb-4 text-2xl font-bold text-gray-800">
Forgot your password?
</h1>

{forgotPasswordMutation.isSuccess ? (
<div>
<h2>Request Submitted</h2>
<p>
If your email is in our system, you will receive instructions to
reset your password shortly.
</p>
</div>
) : (
<Form
submitText="Send Reset Password Instructions"
schema={ForgotPassword}
initialValues={{ email: "" }}
onSubmit={async (values) => {
try {
await forgotPasswordMutation.mutateAsync(values);
} catch (error: any) {
return {
[FORM_ERROR]:
"Sorry, we had an unexpected error. Please try again.",
};
}
}}
>
<LabeledTextField
name="email"
label="Email"
placeholder="Email"
/>
</Form>
)}
</div>
</div>
</div>
);
};

export default ForgotPasswordPage;
30 changes: 30 additions & 0 deletions ladderly-io/src/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Link from "next/link";
import { LoginForm } from "~/app/(auth)/components/LoginForm";
import { getServerAuthSession } from "~/server/auth";
import { LadderlySession } from "~/server/auth";

export const metadata = {
title: "Log In",
};

const LoginPage = async () => {
const session: LadderlySession | null = await getServerAuthSession();

return (
<div className="relative min-h-screen">
<nav className="flex border border-ladderly-light-purple-1 bg-ladderly-off-white px-4 py-1 text-ladderly-violet-700">
<Link
href="/"
className="ml-auto text-gray-800 hover:text-ladderly-pink"
>
Back to Home
</Link>
</nav>
<div className="flex min-h-[calc(100vh-4rem)] items-center justify-center">
{session?.user ? <p>You are already logged in.</p> : <LoginForm />}
</div>
</div>
);
};

export default LoginPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"use client";

import { LabeledTextField } from "~/app/core/components/LabeledTextField";
import { Form, FORM_ERROR } from "~/app/core/components/Form";
import { ResetPassword } from "src/app/(auth)/schemas";
import { api } from "~/trpc/react";
import { useSearchParams } from "next/navigation";
import Link from "next/link";

const ResetPasswordClientPageClient = () => {
const searchParams = useSearchParams();
const token = searchParams?.get("token")?.toString();
const resetPasswordMutation = api.auth.resetPassword.useMutation();

return (
<>
{resetPasswordMutation.isSuccess ? (
<div>
<h2>Password Reset Successfully</h2>
<p>
Go to the <Link href="/">homepage</Link>
</p>
</div>
) : (
<Form
submitText="Reset Password"
schema={ResetPassword}
initialValues={{
password: "",
passwordConfirmation: "",
token,
}}
onSubmit={async (values) => {
try {
if (!token) throw new Error("Token is required.");
await resetPasswordMutation.mutateAsync({
...values,
token,
});
} catch (error: any) {
return {
[FORM_ERROR]:
error.message ||
"Sorry, we had an unexpected error. Please try again.",
};
}
}}
>
<LabeledTextField
name="password"
label="New Password"
type="password"
/>
<LabeledTextField
name="passwordConfirmation"
label="Confirm New Password"
type="password"
/>
</Form>
)}
</>
);
};

export default ResetPasswordClientPageClient;
Loading
Loading