Skip to content

Commit

Permalink
login compat with laravel backend
Browse files Browse the repository at this point in the history
  • Loading branch information
dinkelspiel committed Apr 27, 2024
1 parent d88c1c3 commit 32ffe87
Show file tree
Hide file tree
Showing 15 changed files with 268 additions and 20 deletions.
5 changes: 0 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1 @@
APP_URL="http://localhost:3000"

DATABASE_URL="mysql://root:prisma@mysql:3306/database"

GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
38 changes: 33 additions & 5 deletions app/(app)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,49 @@
import { Sidebar, SidebarButton } from '@/components/sidebar';
import { Home } from 'lucide-react';
import Logo from '@/components/icons/logo';
import { Sidebar, SidebarButton, SidebarFooter } from '@/components/sidebar';
import { validateSessionToken } from '@/server/auth/validateSession';
import { Home, KeyRound, UsersRound } from 'lucide-react';
import { redirect } from 'next/navigation';
import { ReactNode } from 'react';

const Layout = ({ children }: { children: ReactNode }) => {
const Layout = async ({ children }: { children: ReactNode }) => {
const user = await validateSessionToken();

if (!user) {
return redirect('/auth/login');
}

return (
<html lang="en">
<body
className={`grid grid-cols-1 grid-rows-[70px,1fr] lg:grid-rows-1 lg:grid-cols-[256px,1fr] min-h-[100dvh]`}
>
<Sidebar header={<>Medialog</>}>
<Sidebar
header={
<div className="flex gap-x-2">
<Logo className="size-9" />
<div className="flex justify-between flex-col pb-[1px]">
<div className="font-normal tracking-[-.005em] leading-none">
Medialog
</div>
<div className="text-sm font-normal text-muted-foreground leading-none tracking-[.01em]">
@{user?.username}
</div>
</div>
</div>
}
headerProps={{ className: '[&>svg]:size-7 p-0' }}
>
<SidebarButton href="/dashboard">
<Home size={20} />
Home
</SidebarButton>
<SidebarButton href="/community">
<UsersRound size={20} />
Community
</SidebarButton>
</Sidebar>

<main className="px-6 py-4 flex flex-col gap-4">{children}</main>
<main className="p-3 flex flex-col gap-4">{children}</main>
</body>
</html>
);
Expand Down
23 changes: 23 additions & 0 deletions app/auth/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { validateSessionToken } from '@/server/auth/validateSession';
import { redirect } from 'next/navigation';
import React, { ReactNode } from 'react';
import { Toaster } from 'sonner';

const Layout = async ({ children }: { children: ReactNode }) => {
const user = await validateSessionToken();

if (user !== null) {
return redirect('/dashboard');
}

return (
<html lang="en">
<body className={`min-h-[100dvh]`}>
{children}
<Toaster />
</body>
</html>
);
};

export default Layout;
48 changes: 47 additions & 1 deletion app/auth/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,51 @@
'use client';

import Logo from '@/components/icons/logo';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { login } from '@/server/auth/login';
import { useEffect } from 'react';
import { useFormState } from 'react-dom';
import { toast } from 'sonner';

const Page = () => {
return <div></div>;
const [state, formAction] = useFormState(login, {});

useEffect(() => {
if (state.message) {
toast.success(state.message);
}

if (state.error) {
toast.error(state.error);
}
}, [state]);

return (
<main className="grid items-center justify-center w-full bg-neutral-100 h-[100dvh]">
<form className="grid gap-8 w-[350px]" action={formAction}>
<div className="grid gap-2">
<Logo className="mb-2" />
<h3 className="font-bold text-[22px] leading-7 tracking-[-0.02em]">
Login to Medialog
</h3>
<p className="text-sm text-muted-foreground">
Welcome to <i>your</i> website for rating Movies, Books, and TV
Shows
</p>
</div>
<div className="grid gap-4">
<div className="grid gap-2">
<Input placeholder="Email" name="email" />
<Input placeholder="Password" type="password" name="password" />
</div>
<Button className="w-full" size="sm">
Log in
</Button>
</div>
</form>
</main>
);
};

export default Page;
2 changes: 1 addition & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Metadata, Viewport } from 'next';
import '../styles/globals.css';
import '@/styles/globals.css';

export default function RootLayout({
// Layouts must accept a children prop.
Expand Down
25 changes: 25 additions & 0 deletions components/icons/logo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { cn } from '@/lib/utils';
import React, { SVGProps } from 'react';

const Logo = ({ className, ...props }: SVGProps<SVGSVGElement>) => {
return (
<svg
width="64"
height="64"
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={cn('size-11 rounded-lg', className)}
{...props}
>
<rect width="64" height="64" rx="8" fill="#D74323" />
<rect width="64" height="60" rx="8" fill="#E05F43" />
<path
d="M18.2283 40H13L17.8261 20H24.462L28.6848 33.2673L32.9076 20H40.5489V35.6436H50V40H35.5217V25.5446L30.8967 40H25.6685L21.8478 25.5446L18.2283 40Z"
fill="white"
/>
</svg>
);
};

export default Logo;
13 changes: 10 additions & 3 deletions components/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const Sidebar = ({
{...props}
aria-label="Sidebar"
>
<div className="flex bg-neutral-100 shadow-[inset_0_0px_8px_0_rgb(0_0_0_/_0.02)] h-[75px] justify-center lg:justify-start border-b lg:border-b-0 lg:h-full flex-col overflow-y-auto border-slate-200 px-3 py-4 dark:border-slate-700 dark:bg-slate-900">
<div className="flex bg-neutral-100 shadow-[inset_0_0px_8px_0_rgb(0_0_0_/_0.02)] h-[75px] justify-center lg:justify-start border-b lg:border-b-0 lg:h-full flex-col overflow-y-auto border-slate-200 p-3 dark:border-slate-700 dark:bg-slate-900">
<SidebarHeader {...headerProps}>
{header}
<button className="lg:hidden w-full justify-end flex">
Expand Down Expand Up @@ -58,7 +58,7 @@ export const SidebarHeader = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
`lg:mb-12 flex whitespace-nowrap items-center rounded-lg px-3 py-2 text-slate-900 dark:text-white gap-3 text-base font-semibold [&>svg]:size-5`,
`lg:mb-12 flex whitespace-nowrap items-center rounded-lg text-slate-900 dark:text-white gap-3 text-base font-semibold [&>svg]:size-5`,
className
)}
{...props}
Expand Down Expand Up @@ -117,7 +117,14 @@ const ClientSidebarButton = ({
? (selectedVariant as any) ?? 'default'
: 'ghost'
}
className={cn(`w-full justify-start select-none`, className)}
className={cn(
`w-full justify-start select-none`,
{
'text-white': pathname.endsWith(href),
'text-muted-foreground': !pathname.endsWith(href),
},
className
)}
tabIndex={-1}
{...props}
>
Expand Down
6 changes: 3 additions & 3 deletions components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../../lib/utils';

const buttonVariants = cva(
'gap-3 [&>svg]:size-5 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
'gap-3 [&>svg]:size-5 inline-flex items-center justify-center whitespace-nowrap rounded-lg text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
Expand All @@ -22,8 +22,8 @@ const buttonVariants = cva(
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
sm: 'h-8 px-3',
lg: 'h-11 px-8',
icon: 'h-10 w-10',
},
},
Expand Down
2 changes: 1 addition & 1 deletion components/ui/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
'flex h-8 w-full rounded-lg bg-neutral-200/50 px-3 py-1 text-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
Expand Down
15 changes: 15 additions & 0 deletions lib/addMonths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const getDaysInMonth = (year: number, month: number) =>
new Date(year, month, 0).getDate();

export const addMonths = (input: Date, months: number) => {
const date = new Date(input);
date.setDate(1);
date.setMonth(date.getMonth() + months);
date.setDate(
Math.min(
input.getDate(),
getDaysInMonth(date.getFullYear(), date.getMonth() + 1)
)
);
return date;
};
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-table": "^8.15.3",
"@types/bcrypt": "^5.0.2",
"bcrypt": "^5.1.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"cmdk": "^0.2.1",
Expand Down
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ model User {
id Int @id @default(autoincrement()) @db.UnsignedInt
username String @unique
email String @unique
password String @db.VarChar(64)
sessions Session[]
ratingStyle RatingStyle @default(stars)
Expand Down
74 changes: 74 additions & 0 deletions server/auth/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
'use server';

import { z } from 'zod';
import prisma from '../db';
import bcrypt from 'bcrypt';
import { addMonths } from '@/lib/addMonths';
import { cookies, headers } from 'next/headers';
import { generateToken } from '@/lib/generateToken';

const genericLoginError = 'Invalid email or password';

export const login = async (
prevState: any,
formData: FormData
): Promise<{ error?: string; message?: string }> => {
const schema = z.object({
email: z.string(),
password: z.string(),
});

const validatedFields = schema.safeParse({
email: formData.get('email'),
password: formData.get('password'),
});

if (!validatedFields.success) {
return {
error: Object.entries(validatedFields.error.flatten().fieldErrors)
.map(entry => entry[1].map(error => error).join(' '))
.join(' '),
};
}

const email = validatedFields.data.email;
const password = validatedFields.data.password;

const user = await prisma.user.findFirst({
where: {
email,
},
});

if (!user) {
return {
error: genericLoginError,
};
}

if (!bcrypt.compareSync(password, user.password.replace(/^\$2y/, '$2a'))) {
return {
error: genericLoginError,
};
}

const session = await prisma.session.create({
data: {
userId: user.id,
expiry: addMonths(new Date(), 6),
ipAddress: (headers().get('x-forwarded-for') ?? '127.0.0.1').split(
','
)[0]!,
userAgent: headers().get('User-Agent') ?? 'No user agent found',
token: generateToken(64),
},
});

cookies().set('mlSessionToken', session.token, {
expires: new Date().getTime() + 1000 * 60 * 60 * 24 * 31 * 6,
});

return {
message: 'Login successfull',
};
};
32 changes: 32 additions & 0 deletions server/auth/validateSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { cookies } from 'next/headers';
import prisma from '../db';
import { cache } from 'react';

export const validateSessionToken = cache(async () => {
const sessionToken = cookies().get('mlSessionToken');

if (sessionToken === null || sessionToken === undefined) {
return null;
}

const session = await prisma.session.findFirst({
where: {
token: sessionToken.value,
},
});

if (session === null) {
return null;
}

if (session.expiry && session.expiry < new Date()) {
cookies().delete('mlSessionToken');
return null;
}

return await prisma.user.findFirst({
where: {
id: session.userId,
},
});
});
2 changes: 1 addition & 1 deletion styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
--secondary-foreground: 222.2 47.4% 11.2%;

--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--muted-foreground: 0 0% 45%;

--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
Expand Down

0 comments on commit 32ffe87

Please sign in to comment.