Skip to content

Commit

Permalink
feat[AUTH]: Setup usersessions and login, signup page
Browse files Browse the repository at this point in the history
User sessions and Lucia auth, user creation (old code from zephyr-old)
  • Loading branch information
parazeeknova committed Oct 6, 2024
1 parent 122588c commit b875873
Show file tree
Hide file tree
Showing 34 changed files with 1,252 additions and 70 deletions.
3 changes: 2 additions & 1 deletion apps/web/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const nextConfig = {
},
compiler: {
styledComponents: true
}
},
serverExternalPackages: ["@node-rs/argon2", "@prisma/client"]
};

export default nextConfig;
10 changes: 8 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@node-rs/argon2": "^2.0.0",
"@prisma/client": "^5.20.0",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-icons": "^1.3.0",
Expand All @@ -19,17 +21,21 @@
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-tooltip": "^1.1.3",
"@zephyr/auth": "workspace:*",
"@zephyr/db": "workspace:*",
"@zephyr/ui": "workspace:*",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cn": "^0.1.1",
"framer-motion": "^11.11.1",
"lucia": "^3.2.0",
"lucide-react": "^0.447.0",
"next": "15.0.0-rc.0",
"react": "19.0.0-rc-459fd418-20241001",
"react-dom": "19.0.0-rc-459fd418-20241001",
"react-hook-form": "^7.53.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@tailwindcss/aspect-ratio": "0.4.2",
Expand Down
Binary file added apps/web/public/Banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions apps/web/src/app/(auth)/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use server";

import { lucia, validateRequest } from "@zephyr/auth/auth";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

export async function logout() {
const { session } = await validateRequest();

if (!session) {
throw new Error("Unauthorized");
}

await lucia.invalidateSession(session.id);

const sessionCookie = lucia.createBlankSessionCookie();

cookies().set(
sessionCookie.name,
sessionCookie.value,
sessionCookie.attributes
);

return redirect("/login");
}
13 changes: 13 additions & 0 deletions apps/web/src/app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { redirect } from "next/navigation";

import { validateRequest } from "@zephyr/auth/auth";

export default async function Layout({
children
}: { children: React.ReactNode }) {
const { user } = await validateRequest();

if (user) redirect("/");

return <>{children}</>;
}
55 changes: 55 additions & 0 deletions apps/web/src/app/(auth)/login/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use server";

import { verify } from "@node-rs/argon2";
import { lucia } from "@zephyr/auth/auth";
import { type LoginValues, loginSchema } from "@zephyr/auth/validation";
import prisma from "@zephyr/db/prisma";
import { isRedirectError } from "next/dist/client/components/redirect";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

export async function login(
credentials: LoginValues
): Promise<{ error: string }> {
try {
const { username, password } = loginSchema.parse(credentials);

const existingUser = await prisma.user.findFirst({
where: {
username: {
equals: username,
mode: "insensitive"
}
}
});

if (!existingUser || !existingUser.passwordHash) {
return { error: "Incorrect username or password" };
}

const validPassword = await verify(existingUser.passwordHash, password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1
});

if (!validPassword) {
return { error: "Incorrect username or password" };
}

const session = await lucia.createSession(existingUser.id, {});
const sessionCookie = lucia.createSessionCookie(session.id);
cookies().set(
sessionCookie.name,
sessionCookie.value,
sessionCookie.attributes
);

return redirect("/");
} catch (error) {
if (isRedirectError(error)) throw error;
console.error(error);
return { error: "Something went wrong. Please try again." };
}
}
59 changes: 59 additions & 0 deletions apps/web/src/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";

import loginImage from "@zephyr-assets/login-image.jpg";
import LoginForm from "@zephyr-ui/Auth/LoginForm";

export const metadata: Metadata = {
title: "Login"
};

export default function LoginPage() {
return (
<div className="relative flex min-h-screen bg-background">
<div className="-translate-y-1/2 absolute top-1/2 left-4 hidden lg:block">
<h1 className="-rotate-90 transform whitespace-nowrap font-bold text-7xl text-primary opacity-25 lg:text-9xl">
LOGIN
</h1>
</div>

<div className="flex flex-1 items-center justify-center p-4 sm:p-8">
<div className="relative z-10 flex min-h-[600px] w-full max-w-5xl flex-col overflow-hidden rounded-2xl bg-card shadow-xl lg:flex-row">
<div className="relative hidden w-1/2 bg-primary lg:block">
<div className="relative h-full w-full">
<Image
src={loginImage}
alt="Login illustration"
fill
style={{ objectFit: "cover" }}
/>
</div>
</div>
<div className="flex w-full flex-col justify-center p-6 sm:p-8 lg:w-1/2">
<h2 className="mb-2 text-center font-bold text-3xl text-primary sm:mb-4 sm:text-4xl">
Welcome Back
</h2>
<LoginForm />

<div className="flex flex-col items-center justify-center">
<Link
href="/signup"
className="text-primary text-sm hover:underline"
>
Don&apos;t have an account? Sign Up
</Link>
</div>
</div>
</div>
</div>
<div className="pointer-events-none absolute right-2 bottom-2 font-bold text-8xl text-primary opacity-50 sm:right-4 sm:bottom-4 sm:text-6xl">
ZEPHYR.
</div>
<div
className="absolute top-0 right-0 hidden h-full w-1/2 bg-center bg-cover opacity-10 lg:block"
style={{ backgroundImage: `url(${loginImage.src})` }}
/>
</div>
);
}
84 changes: 84 additions & 0 deletions apps/web/src/app/(auth)/signup/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"use server";

import { hash } from "@node-rs/argon2";
import { generateIdFromEntropySize } from "lucia";
import { isRedirectError } from "next/dist/client/components/redirect";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

import { lucia } from "@zephyr/auth/auth";
import { type SignUpValues, signUpSchema } from "@zephyr/auth/validation";
import prisma from "@zephyr/db/prisma";

export async function signUp(
credentials: SignUpValues
): Promise<{ error: string }> {
try {
const { username, email, password } = signUpSchema.parse(credentials);

const passwordHash = await hash(password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1
});

const userId = generateIdFromEntropySize(10);

const existingUsername = await prisma.user.findFirst({
where: {
username: {
equals: username,
mode: "insensitive"
}
}
});

if (existingUsername) {
return {
error: "Username already taken"
};
}

const existingEmail = await prisma.user.findFirst({
where: {
email: {
equals: email,
mode: "insensitive"
}
}
});

if (existingEmail) {
return {
error: "Email already taken"
};
}

await prisma.user.create({
data: {
id: userId,
username,
displayName: username,
email,
passwordHash
}
});

const session = await lucia.createSession(userId, {});
const sessionCookie = lucia.createSessionCookie(session.id);
cookies().set(
sessionCookie.name,
sessionCookie.value,
sessionCookie.attributes
);

return redirect("/");
} catch (error) {
if (isRedirectError(error)) throw error;
console.error(error);
return {
error: "Something went wrong. Please try again."
};
}
}
59 changes: 59 additions & 0 deletions apps/web/src/app/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";

import signupImage from "@zephyr-assets/signup-image.jpg";
import SignUpForm from "@zephyr-ui/Auth/SignUpForm";

export const metadata: Metadata = {
title: "Sign Up"
};

export default function SignupPage() {
return (
<div className="relative flex min-h-screen bg-background">
<div className="-translate-y-1/2 absolute top-1/2 right-4 hidden lg:block">
<h1 className="rotate-90 transform whitespace-nowrap font-bold text-7xl text-primary opacity-25 lg:text-9xl">
SIGN UP
</h1>
</div>

<div className="flex flex-1 items-center justify-center p-4 sm:p-8">
<div className="relative z-10 flex min-h-[600px] w-full max-w-5xl flex-col overflow-hidden rounded-2xl bg-card shadow-xl lg:flex-row">
<div className="flex w-full flex-col justify-center p-6 sm:p-8 lg:w-1/2">
<h2 className="mb-2 text-center font-bold text-3xl text-primary sm:mb-4 sm:text-4xl">
Launch Your Journey
</h2>
<SignUpForm />

<div className="flex flex-col items-center justify-center">
<Link
href="/login"
className="text-primary text-sm hover:underline"
>
Already have an account? Login
</Link>
</div>
</div>
<div className="relative hidden w-1/2 bg-primary lg:block">
<div className="relative h-full w-full">
<Image
src={signupImage}
alt="Signup illustration"
fill
style={{ objectFit: "cover" }}
/>
</div>
</div>
</div>
</div>
<div className="pointer-events-none absolute bottom-2 left-2 font-bold text-8xl text-primary opacity-50 sm:bottom-4 sm:left-4 sm:text-6xl">
ZEPHYR.
</div>
<div
className="absolute top-0 left-0 hidden h-full w-1/2 bg-center bg-cover opacity-10 lg:block"
style={{ backgroundImage: `url(${signupImage.src})` }}
/>
</div>
);
}
29 changes: 29 additions & 0 deletions apps/web/src/app/(main)/SessionProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use client";

import type { Session, User } from "lucia";
import type React from "react";
import { createContext, useContext } from "react";

interface SessionContext {
user: User;
session: Session;
}

const SessionContext = createContext<SessionContext | null>(null);

export default function SessionProvider({
children,
value
}: React.PropsWithChildren<{ value: SessionContext }>) {
return (
<SessionContext.Provider value={value}>{children}</SessionContext.Provider>
);
}

export function useSession() {
const context = useContext(SessionContext);
if (!context) {
throw new Error("useSession must be used within a SessionProvider");
}
return context;
}
18 changes: 18 additions & 0 deletions apps/web/src/app/(main)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { redirect } from "next/navigation";

import { validateRequest } from "@zephyr/auth/auth";
import SessionProvider from "./SessionProvider";

export default async function Layout({
children
}: { children: React.ReactNode }) {
const session = await validateRequest();

if (!session.user) redirect("/login");

return (
<SessionProvider value={session}>
<div className="flex h-screen flex-col font-custom">{children}</div>
</SessionProvider>
);
}
File renamed without changes.
Binary file added apps/web/src/app/assets/avatar-placeholder.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/src/app/assets/login-image.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/web/src/app/assets/signup-image.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit b875873

Please sign in to comment.