Skip to content

Commit 371dbf2

Browse files
anikdhabaldahal
andauthored
feat: email and password login feature (#260)
* feat: email and password login feature * fix: some updates * feat: add password input component * fix: build error * chore: some minor improvements --------- Co-authored-by: Puru D <[email protected]>
1 parent 422b7a3 commit 371dbf2

File tree

47 files changed

+1784
-204
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1784
-204
lines changed

package-lock.json

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@
6161
"@trpc/next": "^10.45.2",
6262
"@trpc/react-query": "^10.43.6",
6363
"@trpc/server": "^10.43.6",
64+
"@types/bcryptjs": "^2.4.6",
6465
"@types/papaparse": "^5.3.14",
6566
"@wojtekmaj/react-hooks": "^1.18.1",
67+
"bcryptjs": "^2.4.3",
6668
"class-variance-authority": "^0.7.0",
6769
"clsx": "^2.1.0",
6870
"cmdk": "^1.0.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
Warnings:
3+
4+
- The required column `id` was added to the `VerificationToken` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required.
5+
6+
*/
7+
-- AlterTable
8+
ALTER TABLE "User" ADD COLUMN "password" TEXT;
9+
10+
-- AlterTable
11+
ALTER TABLE "VerificationToken" ADD COLUMN "id" TEXT NOT NULL,
12+
ADD CONSTRAINT "VerificationToken_pkey" PRIMARY KEY ("id");
13+
14+
-- CreateTable
15+
CREATE TABLE "PasswordResetToken" (
16+
"id" TEXT NOT NULL,
17+
"email" TEXT NOT NULL,
18+
"token" TEXT NOT NULL,
19+
"expires" TIMESTAMP(3) NOT NULL,
20+
21+
CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id")
22+
);
23+
24+
-- CreateIndex
25+
CREATE UNIQUE INDEX "PasswordResetToken_token_key" ON "PasswordResetToken"("token");
26+
27+
-- CreateIndex
28+
CREATE UNIQUE INDEX "PasswordResetToken_email_token_key" ON "PasswordResetToken"("email", "token");

prisma/schema.prisma

+11
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ model User {
5151
id String @id @default(cuid())
5252
name String?
5353
email String? @unique
54+
password String?
5455
emailVerified DateTime?
5556
image String?
5657
@@ -60,13 +61,23 @@ model User {
6061
}
6162

6263
model VerificationToken {
64+
id String @id @default(cuid())
6365
identifier String
6466
token String @unique
6567
expires DateTime
6668
6769
@@unique([identifier, token])
6870
}
6971

72+
model PasswordResetToken {
73+
id String @id @default(cuid())
74+
email String
75+
token String @unique
76+
expires DateTime
77+
78+
@@unique([email, token])
79+
}
80+
7081
model Company {
7182
id String @id @default(cuid())
7283
name String

prisma/seeds/team.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { MemberStatusEnum } from "@/prisma/enums";
22
import { db } from "@/server/db";
33
import { faker } from "@faker-js/faker";
4+
import bcrypt from "bcryptjs";
45
import colors from "colors";
56
colors.enable();
67

@@ -63,11 +64,14 @@ const seedTeam = async () => {
6364

6465
team.forEach(async (t) => {
6566
// const { name, email, image, title, status, isOnboarded } = t
67+
const salt = await bcrypt.genSalt(10);
68+
const hashedPassword = await bcrypt.hash("P@ssw0rd!", salt);
6669
const { name, email, title, status, isOnboarded } = t;
6770
const user = await db.user.create({
6871
data: {
6972
name,
7073
email,
74+
password: hashedPassword,
7175
// image,
7276
},
7377
});

src/app/(authenticated)/layout.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default async function AuthenticatedLayout({
99
const session = await getServerAuthSession();
1010

1111
if (!session) {
12-
redirect("/login");
12+
redirect("/signin");
1313
}
1414
return <>{children}</>;
1515
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { type Metadata } from "next";
2+
import CheckEmailComponent from "@/components/onboarding/check-email";
3+
export const metadata: Metadata = {
4+
title: "Check Email",
5+
};
6+
export default function CheckEmail() {
7+
return <CheckEmailComponent />;
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import EmailSent from "@/components/onboarding/email-sent";
2+
3+
import { type Metadata } from "next";
4+
5+
export const metadata: Metadata = {
6+
title: "Email Sent",
7+
};
8+
export default function EmailSentPage() {
9+
return <EmailSent />;
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import ForgotPassword from "@/components/onboarding/forgot-password";
2+
import { type Metadata } from "next";
3+
4+
export const metadata: Metadata = {
5+
title: "Forgot Password",
6+
};
7+
export default function ForgotPasswordPage() {
8+
return <ForgotPassword />;
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { Metadata } from "next";
2+
import Link from "next/link";
3+
import { Button } from "@/components/ui/button";
4+
import { RiCheckboxCircleLine } from "@remixicon/react";
5+
6+
export const metadata: Metadata = {
7+
title: "Password Updated",
8+
};
9+
10+
export default function PasswordUpdated() {
11+
return (
12+
<div className="flex h-screen items-center justify-center bg-gradient-to-br from-indigo-50 via-white to-cyan-100">
13+
<div className="grid w-full max-w-md grid-cols-1 gap-5 rounded-xl border bg-white p-10 shadow">
14+
<div className="flex flex-col gap-y-2 text-center">
15+
<RiCheckboxCircleLine className="h-10 w-auto" />
16+
<h1 className="text-2xl font-semibold tracking-tight">
17+
Password Updated
18+
</h1>
19+
20+
<p className="text-sm text-muted-foreground">
21+
Your password has been updated successfully.
22+
</p>
23+
24+
<Link href="/" className="mt-4">
25+
<Button size="lg">Return to sign in</Button>
26+
</Link>
27+
</div>
28+
</div>
29+
</div>
30+
);
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { type Metadata } from "next";
2+
import { ResetPasswordForm } from "@/components/onboarding/reset-password";
3+
4+
export const metadata: Metadata = {
5+
title: "Reset Password",
6+
};
7+
8+
export type PageProps = {
9+
params: {
10+
token: string;
11+
};
12+
};
13+
14+
export default async function ResetPasswordPage({
15+
params: { token },
16+
}: PageProps) {
17+
return <ResetPasswordForm token={token} />;
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { Metadata } from "next";
2+
import Link from "next/link";
3+
import { Button } from "@/components/ui/button";
4+
5+
export const metadata: Metadata = {
6+
title: "Reset Password'",
7+
};
8+
9+
export default function EmailVerificationWithoutTokenPage() {
10+
return (
11+
<div className="flex h-screen items-center justify-center bg-gradient-to-br from-indigo-50 via-white to-cyan-100">
12+
<div className="grid w-full max-w-md grid-cols-1 gap-5 rounded-xl border bg-white p-10 shadow">
13+
<div className="flex flex-col gap-y-2 text-center">
14+
<h1 className="text-2xl font-semibold tracking-tight">
15+
Uh oh! Looks like you&apos;re missing a token
16+
</h1>
17+
18+
<p className="text-sm text-muted-foreground">
19+
It seems that there is no token provided, if you are trying to reset
20+
your password please follow the link in your email.
21+
</p>
22+
23+
<Link href="/" className="mt-4">
24+
<Button size="lg">Go back home</Button>
25+
</Link>
26+
</div>
27+
</div>
28+
</div>
29+
);
30+
}
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import SignInForm from "@/components/onboarding/signin";
2+
import { IS_GOOGLE_AUTH_ENABLED } from "@/constants/auth";
3+
import { getServerAuthSession } from "@/server/auth";
4+
import type { Metadata } from "next";
5+
import { redirect } from "next/navigation";
6+
7+
export const metadata: Metadata = {
8+
title: "Sign In",
9+
description: "Sign In to Captable, Inc.",
10+
};
11+
12+
export default async function SignIn() {
13+
const session = await getServerAuthSession();
14+
15+
if (session?.user) {
16+
if (session?.user?.companyPublicId) {
17+
return redirect(`/${session.user.companyPublicId}`);
18+
} else {
19+
return redirect("/onboarding");
20+
}
21+
}
22+
23+
return <SignInForm isGoogleAuthEnabled={IS_GOOGLE_AUTH_ENABLED} />;
24+
}
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import SignUpForm from "@/components/onboarding/signup";
2+
import { IS_GOOGLE_AUTH_ENABLED } from "@/constants/auth";
3+
import { getServerAuthSession } from "@/server/auth";
4+
import type { Metadata } from "next";
5+
import { redirect } from "next/navigation";
6+
7+
export const metadata: Metadata = {
8+
title: "Sign Up",
9+
description: "Sign Up to Captable, Inc.",
10+
};
11+
12+
export default async function SignIn() {
13+
const session = await getServerAuthSession();
14+
15+
if (session?.user) {
16+
if (session?.user?.companyPublicId) {
17+
return redirect(`/${session.user.companyPublicId}`);
18+
} else {
19+
return redirect("/onboarding");
20+
}
21+
}
22+
23+
return <SignUpForm isGoogleAuthEnabled={IS_GOOGLE_AUTH_ENABLED} />;
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { type Metadata } from "next";
2+
import VerifyEmail from "@/components/onboarding/verify-email";
3+
4+
export const metadata: Metadata = {
5+
title: "Verify Email",
6+
};
7+
8+
export type PageProps = {
9+
params: {
10+
token: string;
11+
};
12+
};
13+
14+
export default async function VerifyEmailPage({
15+
params: { token },
16+
}: PageProps) {
17+
return <VerifyEmail token={token} />;
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { Metadata } from "next";
2+
import Link from "next/link";
3+
import { Button } from "@/components/ui/button";
4+
5+
export const metadata: Metadata = {
6+
title: "Verify Email",
7+
};
8+
9+
export default function EmailVerificationWithoutTokenPage() {
10+
return (
11+
<div className="flex h-screen items-center justify-center bg-gradient-to-br from-indigo-50 via-white to-cyan-100">
12+
<div className="grid w-full max-w-md grid-cols-1 gap-5 rounded-xl border bg-white p-10 shadow">
13+
<div className="flex flex-col gap-y-2 text-center">
14+
<h1 className="text-2xl font-semibold tracking-tight">
15+
Uh oh! Looks like you&apos;re missing a token
16+
</h1>
17+
18+
<p className="text-sm text-muted-foreground">
19+
It seems that there is no token provided, if you are trying to
20+
verify your email please follow the link in your email.
21+
</p>
22+
23+
<Link href="/" className="mt-4">
24+
<Button size="lg">Go back home</Button>
25+
</Link>
26+
</div>
27+
</div>
28+
</div>
29+
);
30+
}

src/app/login/page.tsx

-21
This file was deleted.

src/app/page.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ export default async function HomePage() {
88
return redirect(`/${session.user.companyPublicId}`);
99
}
1010

11-
return redirect("/login");
11+
return redirect("/signin");
1212
}

src/components/common/logo.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import Image from "next/image";
55
export const CaptableLogo = ({ className }: { className?: string }) => {
66
return (
77
<Image
8-
className={cn("rounded", className)}
8+
className={cn(className)}
99
height={500}
1010
width={500}
1111
src={logo}

0 commit comments

Comments
 (0)